Sending HL7v2 messages
Open an MLLP connection, send an HL7v2 message, read the ACK response, and process exceptions.
Glion offers a thin client @glion/mllp-client to send HL7v2 messages over MLLP. The client handles the TCP connection, MLLP framing, and ACK parsing. It works against any standard-compliant MLLP server, not just Glion's.
The client supports Node.js, Bun, and Cloudflare Workers. It does not run in a browser — browsers do not expose raw TCP sockets; they only speak HTTP, WebSocket, and WebRTC.
Getting started
- Create a new app from the
mllp-clientexample. - Move into the new project folder,
my-mllp-client. - Send an HL7v2 message to a receiver listening on
127.0.0.1:2575.
npm create glion@latest my-mllp-client -- --example mllp-client
cd my-mllp-client
npm run send -- --sample adt-a01pnpm create glion my-mllp-client --example mllp-client
cd my-mllp-client
pnpm send --sample adt-a01yarn create glion my-mllp-client --example mllp-client
cd my-mllp-client
yarn send --sample adt-a01bun create glion my-mllp-client --example mllp-client
cd my-mllp-client
bun run send --sample adt-a01The CLI sends the bundled adt-a01 message and prints the receiver's AA ACK with the message control id.
Need a receiver to send to?
The Receiving HL7v2 messages guide's mllp-server example listens on 127.0.0.1:2575 out of the box. Bootstrap it in a second terminal first.
Project structure
The mllp-client example is a regular TypeScript Node project.
Two files do the work specific to Glion:
| File | Purpose |
|---|---|
src/send.ts | The default CLI — opens one connection, sends one message, prints the ACK or NAK exit. |
src/stream.ts | The streaming variant — yields every ACK frame the receiver emits (enhanced-mode receivers). |
samples/ | Bundled HL7v2 sample messages selectable by --sample <name>. |
Manual installation
- Install
@glion/mllp-clientand@glion/ack.
npm install @glion/mllp-client @glion/ackpnpm add @glion/mllp-client @glion/ackyarn add @glion/mllp-client @glion/ackbun add @glion/mllp-client @glion/ackGood to know
The client throws exception classes from @glion/ack on NAK responses, so you need both packages even if you don't import anything from @glion/ack directly.
- Create
send.tswith the runtime-specific import.
import { MllpClient } from "@glion/mllp-client/node";
const client = new MllpClient({
host: "127.0.0.1",
port: 2575,
tls: false,
});import { MllpClient } from "@glion/mllp-client/workers";
const client = new MllpClient({
host: "127.0.0.1",
port: 2575,
tls: false,
});import { MllpClient } from "@glion/mllp-client/deno";
const client = new MllpClient({
host: "127.0.0.1",
port: 2575,
tls: false,
});TLS is on by default
The client defaults to tls: true for secure-by-default connections. tls: false opts out of the secure default. Use it for loopback, a hospital intranet behind a TLS terminator, or a VPN tunnel — never on a public network.
The constructor does not open a connection. Each send() call opens its own TCP/TLS connection, sends the message, awaits a single ACK frame, and tears the connection down — modeled after a single HTTP request/response.
- Send a message.
const message = [
"MSH|^~\\&|SENDER|FACILITY|RECEIVER|FACILITY|20260510120000||ADT^A01^ADT_A01|MSG00001|P|2.5",
"EVN|A01|20260510120000",
"PID|1||PATID1234^^^FACILITY^MR||DOE^JOHN^M||19800101|M",
].join("\r");
const ack = await client.send(message);
console.log(ack.raw);
console.log(`code=${ack.code} controlId=${ack.controlId}`);- Run it with the receiver listening on port
2575.
npx tsx send.ts
MSA|AA|MSG00001
code=AA controlId=MSG00001pnpm dlx tsx send.ts
MSA|AA|MSG00001
code=AA controlId=MSG00001yarn dlx tsx send.ts
MSA|AA|MSG00001
code=AA controlId=MSG00001bun run send.ts
MSA|AA|MSG00001
code=AA controlId=MSG00001The first line is the raw acknowledgment from the wire — the MSA segment carries the success code and the original message's control id. The second line is the same fields read off the parsed ack object, so application code never has to slice HL7v2 strings.
Handling acknowledgments
The client parses the ACK response and translates NAK responses into typed JavaScript errors. Handle NAKs with try/catch and read the ACK code, textual reason, and any error details the receiver included in the ERR segment straight off the exception object.
import { AckApplicationError, AckApplicationReject, AckException } from "@glion/ack";
import { MllpClient, MllpClientError } from "@glion/mllp-client/node";
try {
const ack = await client.send(message);
// ack.code === "AA" — receiver accepted.
} catch (error) {
if (error instanceof AckApplicationError) {
// MSA-1 = AE — application-level error from the receiver.
} else if (error instanceof AckApplicationReject) {
// MSA-1 = AR — receiver rejected the message outright.
} else if (error instanceof AckException) {
// CE / CR — commit-level error or reject. Same shape.
} else if (error instanceof MllpClientError) {
// Transport-level failure. error.code is one of CONNECTION_REFUSED,
// TIMEOUT, CONNECTION_CLOSED, MALFORMED_FRAME, MALFORMED_ACK.
} else {
throw error;
}
}Each AckException subclass carries the same fields, populated from the wire ACK:
| Field | Source |
|---|---|
code | MSA-1 — the ACK code (AE/AR/CE/CR). |
message | MSA-3 — the textual reason from the receiver. |
errorCode | ERR-3 — the HL7v2 error condition code, when the receiver supplied one. |
severity | ERR-4 — the severity flag, when supplied. |
raw | The wire-format ACK string, useful for logging or persistence. |
For the full exception hierarchy and the convenience subclasses (UnsupportedMessageTypeReject, ApplicationInternalError, CommitInternalError), see the Error handling section on the receiving-side guide.
Good to know
The exception types in @glion/ack are the same ones ackMiddleware() reads on the receiver side. That simplifies error handling in full-duplex integrations where your app both sends and receives. The exceptions also work with any HL7v2-compliant MLLP server — not just Glion's — as long as the receiver follows the standard ACK conventions.
Transport Layer Security (TLS)
TLS encrypts data in transit and authenticates the receiver. The client uses TLS by default for every connection, so sensitive health information stays protected on the wire. TLS is required for any production integration over a public network, and strongly recommended even on private networks.
The client defaults to tls: true. Pass tls: false to opt out for local development or when the network is already secured — for example, a loopback connection, a hospital intranet behind a TLS terminator, or a VPN tunnel.
Don't disable TLS on a public network
TLS is a critical security layer for protecting sensitive health information in transit. Only disable it for local development or if your network is already secure.
You can configure TLS with custom certificates, keys, and trust stores. Here's an example of a client configured for mutual TLS authentication:
import { readFileSync } from "node:fs";
const client = new MllpClient({
host: "mllp.example.com",
port: 6661,
tls: {
ca: readFileSync("ca.pem"),
cert: readFileSync("client-cert.pem"),
key: readFileSync("client-key.pem"),
},
});See the @glion/mllp-client README for the full TLS option set — SNI, passphrases, and the test-only insecure: true opt-out.
Streaming intermediate ACKs
Some MLLP server implementations emit an intermediate commit ACK before the final accept or reject. This is called enhanced mode in the HL7v2 standard, and it lets the receiver respond in two steps:
- Commit step: the receiver sends a
CA(commit accept) ACK to indicate that the message was received and will be processed. The final outcome is not yet known, so the sender can free resources or update local state while the receiver works on the message. - Acknowledgment step: after processing the message, the receiver sends a final ACK (
AAfor accept,AR/AE/CRfor reject).
The Glion MLLP client supports enhanced mode via client.stream(), which returns an async iterable of ACK frames. The iterable completes after the resolving accept frame.
import { MllpClient } from "@glion/mllp-client/node";
const client = new MllpClient({
host: "127.0.0.1",
port: 2575,
tls: false,
});
const message = [
"MSH|^~\\&|SENDER|FACILITY|RECEIVER|FACILITY|20260510120000||ADT^A01^ADT_A01|MSG00001|P|2.5",
"EVN|A01|20260510120000",
"PID|1||PATID1234^^^FACILITY^MR||DOE^JOHN^M||19800101|M",
].join("\r");
try {
for await (const ack of client.stream(message)) {
console.log(`${ack.code} controlId=${ack.controlId}`);
}
} catch (error) {
// Handle exceptions as in the single-send example.
}Cloudflare Workers
The Workers adapter has the same API but a different import path:
import { MllpClient } from "@glion/mllp-client/workers";
const client = new MllpClient({ host: "mllp.example.com", port: 6661 });
const ack = await client.send(message);The Workers runtime cannot honour programmatic TLS material — passing tls.ca, tls.cert, tls.key, or related fields throws MllpClientError with code: "INVALID_INPUT" before any socket is opened. Configure TLS via Hyperdrive, Worker bindings, or terminate TLS upstream of the receiver.
See also
- Receiving HL7v2 messages — the matching receiver side, with route handlers, middleware, and ACK construction.
- The MLLP runtime model — how the receiver frames messages, dispatches routes, and constructs the ACKs this client reads.
@glion/mllp-clientREADME — full API, transport error codes, runtime adapters.@glion/ackREADME — theAckExceptionhierarchy.