Trial && Error

Intrusive Thoughts & Lost Bits

View on GitHub
13 October 2025

NATS | When, Why and How

by GonzaloMB

NATS is a high‑performance messaging system for building fast, simple, and resilient services. This guide explains when and why to use NATS, how it compares to HTTP, and shows concise TypeScript/JavaScript examples for Pub/Sub, Request‑Reply, Work Queues, and JetStream persistence.


When To Use NATS

When to stick with HTTP:


Why NATS


Communication Patterns vs. HTTP

You can build HTTP‑like semantics on NATS using Request‑Reply, headers, and subject conventions, while avoiding HTTP servers where not needed.


Setup

  1. Run NATS locally (Docker):
docker run -p 4222:4222 -ti nats:2 -js
  1. Install client:
npm i nats
  1. Connect (TS/JS):
import { connect, StringCodec } from "nats";

async function main() {
  const nc = await connect({ servers: "nats://localhost:4222" });
  const sc = StringCodec();
  console.log("connected:", nc.getServer());

  // Close gracefully
  const done = nc.closed();
  nc.closed().then(() => console.log("connection closed"));
  process.on("SIGINT", async () => {
    await nc.drain();
  });
}
main();

Pub/Sub (Broadcast)

Publisher:

import { connect, StringCodec } from "nats";
const sc = StringCodec();
(async () => {
  const nc = await connect({ servers: "nats://localhost:4222" });
  nc.publish(
    "orders.created",
    sc.encode(JSON.stringify({ id: "o1", total: 42 }))
  );
  await nc.flush();
  await nc.drain();
})();

Subscriber:

import { connect, StringCodec } from "nats";
const sc = StringCodec();
(async () => {
  const nc = await connect({ servers: "nats://localhost:4222" });
  const sub = nc.subscribe("orders.created");
  for await (const m of sub) {
    console.log("event", m.subject, JSON.parse(sc.decode(m.data)));
  }
})();

Subject design tip: use dot‑separated nouns/verbs (orders.created, payments.captured).


Request‑Reply (HTTP‑like RPC)

Service (responder):

import { connect, StringCodec } from "nats";
const sc = StringCodec();
(async () => {
  const nc = await connect({ servers: "nats://localhost:4222" });
  const sub = nc.subscribe("inventory.get");
  for await (const m of sub) {
    const { sku } = JSON.parse(sc.decode(m.data));
    const qty = 7; // lookup...
    m.respond(sc.encode(JSON.stringify({ sku, qty })));
  }
})();

Client (requester):

import { connect, StringCodec } from "nats";
const sc = StringCodec();
(async () => {
  const nc = await connect({ servers: "nats://localhost:4222" });
  const msg = await nc.request(
    "inventory.get",
    sc.encode(JSON.stringify({ sku: "SKU-123" })),
    { timeout: 1000 }
  );
  console.log("reply:", JSON.parse(sc.decode(msg.data)));
})();

Note: You can attach headers (import { headers }) to carry metadata like auth, correlation IDs, or content type.


Work Queues (Competing Consumers)

Only one consumer in a queue group receives each message.

Subscriber workers:

import { connect, StringCodec } from "nats";
const sc = StringCodec();
(async () => {
  const nc = await connect({ servers: "nats://localhost:4222" });
  // queue: "workers"
  const sub = nc.subscribe("images.resize", { queue: "workers" });
  for await (const m of sub) {
    const job = JSON.parse(sc.decode(m.data));
    console.log("processing", job.id);
  }
})();

Publisher:

import { connect, StringCodec } from "nats";
const sc = StringCodec();
(async () => {
  const nc = await connect({ servers: "nats://localhost:4222" });
  for (let i = 1; i <= 10; i++)
    nc.publish("images.resize", sc.encode(JSON.stringify({ id: i })));
  await nc.drain();
})();

JetStream (Durable Streams, Replay)

Create a stream, publish, and consume with a durable.

import { connect, StringCodec, consumerOpts } from "nats";
const sc = StringCodec();
(async () => {
  const nc = await connect({ servers: "nats://localhost:4222" });
  const jsm = await nc.jetstreamManager();

  // idempotent upsert of a stream
  await jsm.streams
    .add({ name: "ORDERS", subjects: ["orders.*"] })
    .catch(() => {});

  const js = nc.jetstream();
  await js.publish("orders.created", sc.encode(JSON.stringify({ id: "o1" })));

  const opts = consumerOpts();
  opts.durable("ORDERS_WORKER");
  opts.manualAck();
  opts.ackExplicit();
  opts.deliverAll(); // or .startSequence / .startTime

  const sub = await js.subscribe("orders.*", opts);
  for await (const m of sub) {
    const ev = JSON.parse(sc.decode(m.data));
    console.log("replayed/event:", ev);
    m.ack(); // at‑least‑once
  }
})();

Also available: Key/Value store (await nc.jetstream().views.kv(...)) and Object Store for blobs.


Designing HTTP‑Like APIs on NATS

Example with headers:

import { connect, StringCodec, headers } from "nats";
const sc = StringCodec();
(async () => {
  const nc = await connect({ servers: "nats://localhost:4222" });
  const h = headers();
  h.set("authorization", "Bearer <token>");
  h.set("content-type", "application/json");
  const msg = await nc.request(
    "v1.svc.user.get",
    sc.encode(JSON.stringify({ id: "u1" })),
    { headers: h, timeout: 1000 }
  );
  console.log(JSON.parse(sc.decode(msg.data)));
})();

Operational Tips


Benefits (TL;DR)


tags: nats - microservices