NATS Transport
NATS is a lightweight, high-performance messaging system built around the publish/subscribe model and subject-based routing. NestJS ships a first-class NATS transporter that maps message patterns to NATS subjects, supports load balancing through queue groups, and handles request-reply transparently so your @MessagePattern handlers feel exactly like any other microservice. This makes NATS an excellent choice when you want low latency, simple operations, and built-in fan-out without standing up a heavier broker.
How NestJS maps to NATS
In NestJS, every message pattern you declare becomes a NATS subject. When a client sends client.send('user.created', payload), Nest publishes a message on the user.created subject and a microservice subscribed with @MessagePattern('user.created') receives it. Patterns can be strings or objects — object patterns are serialized into a stable subject string, so { cmd: 'sum' } and 'user.created' are both valid.
NATS distinguishes two interaction styles, and Nest supports both:
| Style | NestJS API | NATS behavior |
|---|---|---|
| Request-reply | client.send() / @MessagePattern() | Uses NATS request with an auto-generated reply inbox |
| Fire-and-forget | client.emit() / @EventPattern() | Plain publish, no response expected |
Installing and configuring the transporter
First install the NATS client library used by the transporter.
npm install nats
Configure a microservice to listen over NATS in main.ts. The transporter connects to one or more NATS servers and begins subscribing to the subjects derived from your handlers.
// main.ts
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.NATS,
options: {
servers: ['nats://localhost:4222'],
queue: 'users-service',
},
},
);
await app.listen();
}
bootstrap();
Output:
[Nest] 4821 - 06/14/2026, 10:02:11 AM LOG [NestFactory] Starting Nest application...
[Nest] 4821 - 06/14/2026, 10:02:11 AM LOG [NestMicroservice] Nest microservice successfully started
Common connection options
The options object is passed through to the underlying NATS client, so any native connection setting is available alongside the Nest-specific queue.
| Option | Type | Description |
|---|---|---|
servers | string | string[] | One or more nats://host:port URLs for the cluster |
queue | string | Queue group name for load-balanced subscriptions |
name | string | Client name shown in NATS monitoring |
token / user / pass | string | Authentication credentials |
reconnect | boolean | Automatically reconnect on disconnect (default true) |
maxReconnectAttempts | number | Reconnect attempt limit; -1 for unlimited |
Subject-based routing
Handlers subscribe to subjects through the standard message and event decorators. Use @MessagePattern when the caller expects a response and @EventPattern for one-way notifications.
// users.controller.ts
import { Controller } from '@nestjs/common';
import { MessagePattern, EventPattern, Payload } from '@nestjs/microservices';
interface CreateUserDto {
email: string;
name: string;
}
@Controller()
export class UsersController {
@MessagePattern('user.find')
findUser(@Payload() id: string) {
return { id, name: 'Ada Lovelace', email: '[email protected]' };
}
@EventPattern('user.created')
handleUserCreated(@Payload() data: CreateUserDto) {
console.log(`Provisioning resources for ${data.email}`);
}
}
NATS subjects support wildcards (* for a single token, > for multiple). Because Nest derives the subject from the pattern, hierarchical names like 'order.payment.completed' let you organize traffic cleanly while keeping handlers explicit.
Queue groups for load balancing
A queue group lets multiple instances of the same service share a subscription so that each message is delivered to exactly one member of the group. This is how you scale horizontally: run three replicas of users-service with the same queue value, and NATS round-robins requests across them. Without a queue group, every subscriber receives every message — useful for broadcast events, but not for work distribution.
options: {
servers: ['nats://localhost:4222'],
queue: 'users-service', // all replicas in this group split the load
}
Set the same
queueon all replicas of a service. If you omit it, every instance will independently handle the same@MessagePattern, causing duplicate processing and racing replies.
Request-reply from a client
Inject a ClientProxy to call other services. send() returns an Observable that completes with the handler’s response; emit() fires an event and returns immediately.
// users.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { GatewayController } from './gateway.controller';
@Module({
imports: [
ClientsModule.register([
{
name: 'USERS_CLIENT',
transport: Transport.NATS,
options: { servers: ['nats://localhost:4222'] },
},
]),
],
controllers: [GatewayController],
})
export class UsersModule {}
// gateway.controller.ts
import { Controller, Get, Inject, Param } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs';
@Controller('users')
export class GatewayController {
constructor(@Inject('USERS_CLIENT') private readonly client: ClientProxy) {}
@Get(':id')
async getUser(@Param('id') id: string) {
return firstValueFrom(this.client.send('user.find', id));
}
}
Output:
$ curl http://localhost:3000/users/42
{"id":"42","name":"Ada Lovelace","email":"[email protected]"}
Behind the scenes Nest publishes the request on the user.find subject with a temporary reply inbox, the listening service responds to that inbox, and the Observable emits the decoded payload.
Best practices
- Use a consistent
queuegroup name per logical service so replicas load-balance instead of duplicating work. - Reserve
@EventPattern/emit()for fan-out notifications and@MessagePattern/send()for calls that need a reply. - Adopt a hierarchical subject naming convention (
domain.entity.action) to keep routing readable and wildcard-friendly. - Provide multiple
serversentries and leavereconnectenabled so clients survive node failures in a NATS cluster. - Always set a request timeout on the caller side (via RxJS
timeout()) so a missing responder fails fast instead of hanging. - Keep payloads small and serializable; NATS is optimized for high message throughput, not large blobs.