A technical guide to building bridging middleware for legacy systems: endpoint mapping, a Node.js 20 + Fastify adapter layer, timeouts, idempotency, circuit breakers, and an audit-ready audit trail.
Integration into legacy systems rarely fails because the new application code is bad. Failure more often occurs in the middle layer: an old SIMRS that only provides a 400-line stored procedure on SQL Server 2012, a supporting application from a different vendor that exposes SOAP without documentation, or BPJS VClaim bridging that requires date format yyyy-MM-dd while the internal database stores dd/MM/yyyy. Bridging middleware works precisely at that point as a translator: normalizing data formats, managing timeouts, recording audit trails, without forcing the hospital to replace systems that are already running and used by 200+ staff every day. This article discusses concrete steps for building such middleware—from endpoint mapping, creating an adapter layer, to failure handling with idempotency keys—with Node.js code examples that can be run directly.
1. Map Integration Points Before Writing a Single Line of Code
Start with inventory, not with a framework. Create a spreadsheet with columns: system name, protocol, endpoint, authentication method, date/number format, SLA promised by the vendor, and contact person in charge. In one SIMRS project at a type C hospital with 180 beds, the initial inventory found 7 connected systems with 23 endpoints, and 4 of them had no documentation at all—including a laboratory endpoint that was only "inherited" through an old WSDL file.
Do not trust documentation 100%. Test every endpoint with Postman or curl, save the original response examples to the fixtures/ folder, then use them as unit test material. A common experience: the NAMA_PASIEN field that the document says is required can actually be null for newborn patients, and TGL_LAHIR in one installation contains an empty string, not a default date.
After the map is complete, define one internal contract—a canonical model in JSON—that becomes the "neutral language" of the entire middleware. All adapters must return the same shape: { noRm, nama, tglLahir, nik }. Use FHIR R4 (Patient, Encounter, Observation) as a field naming reference, because if the hospital later needs SatuSehat bridging, remapping will be much cheaper.
One iron rule: middleware must not write directly to the legacy database. All access goes through APIs or vendor-approved stored procedures, or at most read-only views. This keeps the vendor support guarantee valid and prevents incidents that are difficult to trace during audits.
2. Build an Adapter Layer with Node.js 20, Fastify 4.28, and Undici
For middleware handling 1,200–3,000 requests per day, Node.js 20 LTS with Fastify 4.28 is more than enough: high throughput, a mature library ecosystem, and easy deployment as a container. For the HTTP client use undici 6.19 (not axios) because it supports native AbortController and has low overhead; for payload validation use zod 3.23 so the internal contract is truly enforced at runtime.
Apply the adapter pattern: one file per system, one function per domain operation. Adapter functions are only responsible for calling legacy endpoints, translating fields, and throwing typed errors. All business logic stays in the service layer, so when the SIMRS vendor changes an endpoint, you only touch one file.
// adapters/simrs.adapter.js — Node.js 20, undici 6.19
import { request } from 'undici';
export async function getPasienByRm(noRm, { timeoutMs = 3000 } = {}) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const res = await request(`http://10.10.10.21:8081/api/pasien/${noRm}`, {
signal: ctrl.signal,
headers: { 'X-Api-Key': process.env.SIMRS_KEY }
});
if (res.statusCode === 404) return null;
if (res.statusCode >= 500) throw new Error(`SIMRS_5XX_${res.statusCode}`);
const raw = await res.body.json();
return { noRm: raw.NO_RM, nama: raw.NAMA_PASIEN, tglLahir: raw.TGL_LAHIR, nik: raw.NIK };
} finally { clearTimeout(timer); }
}After the adapter is running, measure before adding features. In a real implementation, the SIMRS patient endpoint responded in 6–9 seconds during peak hours (08.00–11.00) because the stored procedure joins 5 tables. By adding a Redis 7.2 cache for 300 seconds specifically for master data (clinic, doctor, ICD-10), calls to SIMRS dropped 78% and the internal endpoint p95 dropped from 8,400 ms to 420 ms.
3. Handle Failures: Retry, Idempotency, and Audit Trail
Assume the network and legacy systems will fail. Apply retry with exponential backoff of 200 ms, 400 ms, then 800 ms plus jitter, maximum 3 attempts, and only for 5xx statuses, timeouts, or ECONNRESET. Never retry 400/422 errors—a wrong payload will not change its result, and in visit registration cases such retries actually create duplicate data.
For all POST operations, require an Idempotency-Key header containing a UUID v4, store it in a PostgreSQL 16 table with a unique index and a 24-hour TTL. Without this, a single double-click on a registration form once produced two SEP numbers in BPJS and had to be canceled manually by staff—an incident that could be avoided with 20 lines of code.
For synchronization that does not need an immediate answer (sending reports to SatuSehat, updating stock to ERP), use the outbox pattern: write the event to the outbox table in the same transaction, then a BullMQ 5.x worker processes it asynchronously with its own retry. The UI does not need to wait 9 seconds; users get a response under 300 ms.
Complete it with a circuit breaker (opossum 8.x): if 50% of requests fail within a 10-second window, the breaker opens for 30 seconds so the legacy system does not sink further. All requests and errors are recorded via pino as structured logs to Loki with 90-day retention—this is important because electronic medical records must have a traceable audit trail.
Best Practices
- Contract first, code later. Write the JSON/zod schema and example payload in the repository before implementation; all adapters follow that schema, not the other way around.
- Always set an explicit timeout. The default without a timeout will hang requests until the load balancer drops the connection and leaves the worker hanging.
- Idempotency for every POST. Store the idempotency key for at least 24 hours; this is the cheapest defense against duplicate data in SIMRS and BPJS claims.
- Store credentials in a secret manager (Vault, AWS Secrets Manager, or at minimum an encrypted environment variable), not in the repository or a configuration file shared via chat.
- Provide a mock server for testing. Record real responses as fixtures, so tests can run without touching the hospital's production system.
FAQ
Is bridging middleware required to be built with Node.js?
No. Node.js 20 + Fastify is suitable for teams accustomed to JavaScript and needing high I/O concurrency. If your team is strong in PHP, Laravel 11 with Guzzle and Horizon is also very viable; the key is not the language, but the discipline of adapters, timeouts, idempotency, and logging.
What if the SIMRS vendor refuses to provide API access?
Submit a written request with the required endpoint specifications, offer the option of a vendor-approved read-only view or stored procedure, and prepare a long-term option in the form of an RFP for system replacement. Middleware is not designed to bypass licensing restrictions—that is a legal and operational risk.
How long does it take to build middleware like this?
For 5–10 endpoints with two legacy systems, the mapping stage takes 3–5 working days and implementation takes 2–3 weeks, including testing and observability. Bridging BPJS VClaim or SatuSehat that involves validation and claim queues usually takes 4–6 weeks.
Will middleware slow the system down?
Quite the opposite when measured. An additional layer does add 5–20 ms, but caching, parallel calls, and asynchronous calls to slow services cut waiting time from seconds to hundreds of milliseconds. The key is profiling from the start, not guessing.
Building reliable bridging middleware is not about choosing the newest framework, but about the discipline of mapping contracts, measuring latency, and handling failures explicitly. With a thin adapter layer, clear timeouts, idempotency on every POST, and a tidy circuit breaker and audit trail, legacy systems can still be maintained while opening the way to BPJS VClaim, SatuSehat FHIR, ERP, and modern POS. If your IT team is preparing bridging for SIMRS, SIM Klinik, ERP, or a website and needs technical assistance, our IT consulting and application development services cover the Yogyakarta, DIY, and Magelang areas. Discuss your architecture early so implementation does not guess in production.
This article is part of the knowledge base of Nugroho Setiawan, an IT consultant in Jogja (Yogyakarta). Related solutions: SIMRS (Hospital Information System) BPJS & SatuSehat Integration. Need a free consultation?
Komentar
Belum ada komentar. Jadilah yang pertama!
Saya membantu konsultasi IT & pengembangan aplikasi untuk rumah sakit, klinik, dan bisnis di Yogyakarta (DIY), Magelang, dan sekitarnya.