Part 9: Microservices
Install
npm install @nestjs/microservices
# For Redis transportnpm install ioredis
# For RabbitMQ transportnpm install amqplib amqp-connection-managernpm install -D @types/amqplibWhat NestJS microservices are
@nestjs/microservices is a transport-agnostic messaging layer. Instead of HTTP, services communicate over:
- TCP: built-in, good for internal networks
- Redis: pub/sub; good for event broadcasting
- RabbitMQ: durable queues; good for reliable job processing
- Kafka, NATS, gRPC: also supported, not covered here
The same @MessagePattern and @EventPattern decorators work across transports. You swap the transport configuration without changing business logic.
A standalone microservice
A microservice starts without HTTP. It listens on a transport and processes messages:
// main.ts (microservice entry point)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.TCP, options: { host: '0.0.0.0', port: 3001, }, }, );
await app.listen(); console.log('Microservice listening on TCP :3001');}
bootstrap();@MessagePattern: request-reply
@MessagePattern marks a controller method as the handler for a specific message type. The caller sends a message and waits for a response (like an HTTP request, but over the transport).
import { Controller } from '@nestjs/common';import { MessagePattern, Payload } from '@nestjs/microservices';
@Controller()export class UsersController { constructor(private readonly usersService: UsersService) {}
@MessagePattern({ cmd: 'get_user' }) async getUser(@Payload() data: { id: number }) { return this.usersService.findOne(data.id); }
@MessagePattern({ cmd: 'create_user' }) async createUser(@Payload() data: CreateUserDto) { return this.usersService.create(data); }
@MessagePattern({ cmd: 'list_users' }) async listUsers(@Payload() data: { page: number; limit: number }) { return this.usersService.findAll(data); }}@EventPattern: fire-and-forget
@EventPattern handles one-way events. The caller emits and doesn’t wait for a response. Use this for notifications, cache invalidation, and audit logging.
import { EventPattern, Payload } from '@nestjs/microservices';
@Controller()export class EventsController { @EventPattern('user_created') async handleUserCreated(@Payload() data: { userId: number; email: string }) { await this.emailService.sendWelcome(data.email); await this.analyticsService.track('user_created', data); }
@EventPattern('order_placed') async handleOrderPlaced(@Payload() data: { orderId: string }) { await this.inventoryService.reserveItems(data.orderId); }}ClientProxy: sending messages from an HTTP service
The HTTP-facing service sends messages to microservices using ClientProxy. Register the client in a module:
import { Module } from '@nestjs/common';import { ClientsModule, Transport } from '@nestjs/microservices';import { OrdersController } from './orders.controller';import { OrdersService } from './orders.service';
@Module({ imports: [ ClientsModule.register([ { name: 'USERS_SERVICE', transport: Transport.TCP, options: { host: 'users-service', port: 3001 }, }, ]), ], controllers: [OrdersController], providers: [OrdersService],})export class OrdersModule {}Inject and use in a service:
import { Injectable, Inject } from '@nestjs/common';import { ClientProxy } from '@nestjs/microservices';import { firstValueFrom } from 'rxjs';
@Injectable()export class OrdersService { constructor( @Inject('USERS_SERVICE') private readonly usersClient: ClientProxy, ) {}
async createOrder(dto: CreateOrderDto) { // send: request-reply (returns Observable, convert to Promise) const user = await firstValueFrom( this.usersClient.send<User>({ cmd: 'get_user' }, { id: dto.userId }), );
if (!user) throw new NotFoundException('User not found');
// emit: fire-and-forget this.usersClient.emit('order_placed', { orderId: dto.id, userId: user.id });
return this.ordersRepo.create(dto); }}send() returns an Observable. Use firstValueFrom (from rxjs) to convert it to a Promise. emit() is truly fire-and-forget and returns void.
Redis transport
Redis pub/sub is good for broadcasting events to multiple subscribers:
// Microservice bootstrapconst app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, { transport: Transport.REDIS, options: { host: process.env.REDIS_HOST ?? 'localhost', port: +(process.env.REDIS_PORT ?? 6379), },});
// Client registrationClientsModule.register([ { name: 'EVENTS_SERVICE', transport: Transport.REDIS, options: { host: process.env.REDIS_HOST ?? 'localhost', port: +(process.env.REDIS_PORT ?? 6379), }, },])RabbitMQ transport
RabbitMQ provides durable queues with acknowledgment. Messages survive process restarts.
// Microservice bootstrapconst app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, { transport: Transport.RMQ, options: { urls: [process.env.RABBITMQ_URL ?? 'amqp://localhost:5672'], queue: 'orders_queue', queueOptions: { durable: true, // queue survives RabbitMQ restart }, noAck: false, // manual acknowledgment prefetchCount: 1, // process one message at a time per consumer },});
// Client registrationClientsModule.register([ { name: 'ORDERS_SERVICE', transport: Transport.RMQ, options: { urls: [process.env.RABBITMQ_URL ?? 'amqp://localhost:5672'], queue: 'orders_queue', queueOptions: { durable: true }, }, },])Hybrid app: HTTP + microservice on one process
A hybrid app listens for both HTTP and microservice messages. Useful during migration or when one service needs to be both a REST API and a message consumer:
import { NestFactory } from '@nestjs/core';import { MicroserviceOptions, Transport } from '@nestjs/microservices';import { AppModule } from './app.module';
async function bootstrap() { const app = await NestFactory.create(AppModule);
// Add a microservice listener to the HTTP app app.connectMicroservice<MicroserviceOptions>({ transport: Transport.TCP, options: { port: 3001 }, });
await app.startAllMicroservices(); await app.listen(3000);
console.log('HTTP on :3000, TCP microservice on :3001');}
bootstrap();Error handling with microservices
Errors thrown in a @MessagePattern handler propagate back to the caller as an error response. The caller’s send() Observable emits an error:
// In the microservice handler:@MessagePattern({ cmd: 'get_user' })async getUser(@Payload() data: { id: number }) { const user = await this.usersService.findOne(data.id); if (!user) throw new RpcException('User not found'); return user;}
// In the calling service:try { const user = await firstValueFrom( this.usersClient.send<User>({ cmd: 'get_user' }, { id: 99 }), );} catch (err) { // err.message === 'User not found' throw new NotFoundException(err.message);}RpcException (from @nestjs/microservices) serializes the error properly over the transport. Throwing a plain Error or HttpException works but produces a less structured response.
Dynamic client configuration
For configuration-driven client setup:
ClientsModule.registerAsync([ { name: 'USERS_SERVICE', imports: [ConfigModule], inject: [ConfigService], useFactory: (config: ConfigService): ClientProviderOptions => ({ transport: Transport.TCP, options: { host: config.get('USERS_SERVICE_HOST'), port: config.get<number>('USERS_SERVICE_PORT'), }, }), },])Handling context in message handlers
Use @Ctx() to access the raw transport context (channel, original message) when you need to manually acknowledge messages:
import { Payload, Ctx, RmqContext, MessagePattern } from '@nestjs/microservices';import { Channel, Message } from 'amqplib';
@MessagePattern('process_job')async processJob( @Payload() data: JobDto, @Ctx() context: RmqContext,) { const channel: Channel = context.getChannelRef(); const originalMsg: Message = context.getMessage();
try { await this.jobService.process(data); channel.ack(originalMsg); // acknowledge on success } catch (err) { channel.nack(originalMsg, false, false); // dead-letter on failure }}Service-to-service communication patterns
flowchart TD Client[REST Client] GW["API Gateway\n(NestJS HTTP app)"] US["Users Service\n(microservice)"] OS["Orders Service\n(microservice)"] IS["Inventory Service\n(microservice)"] Client -->|HTTP| GW GW -->|"TCP/Redis/RabbitMQ"| US GW -->|"TCP/Redis/RabbitMQ"| OS OS -->|RabbitMQ event| ISThe API Gateway handles public-facing HTTP and translates to internal message patterns. Internal services communicate via events for loose coupling.
Gotchas at this stage
firstValueFromvslastValueFrom:send()emits exactly one value and completes. Both work, butfirstValueFromis semantically clearer.ClientProxymust be connected before use: callawait client.connect()if you’re sending messages at startup (e.g., in a service’sonModuleInit).- TCP transport is not production-hardened: TCP has no built-in retries, dead-letter queues, or persistence. Use RabbitMQ or Kafka for production workloads that can’t lose messages.
- Pattern matching is by value equality:
{ cmd: 'get_user' }on the sender must exactly match@MessagePattern({ cmd: 'get_user' })on the receiver. A typo causes the message to silently go unhandled. - Scaling with Redis: multiple instances of a Redis-transport microservice each receive every message (pub/sub). For work queues where each message should be processed once, use RabbitMQ or Kafka instead.
What’s next
Part 10 covers testing and production: unit testing with Jest and createTestingModule, e2e tests with Supertest, mocking providers, generating Swagger/OpenAPI docs, Docker containerization, and health checks.