Back to Articles
50 min read

Building Scalable Microservices with NestJS: Architecture, Transporters, and Patterns

Monoliths have their place, but scale often demands distribution. This comprehensive guide dissects the NestJS Microservices module, providing deep dives into every major transport layer—from the low-latency of TCP to the event streaming power of Kafka. We explore the trade-offs between Request-Response and Event-based patterns and how to implement Hybrid Applications that bridge the gap.

Microservices

Microservices Architecture Overview

NestJS microservices enable building distributed systems where each service handles a specific domain, communicating via lightweight protocols like TCP, Redis, or message brokers, allowing independent scaling, deployment, and technology choices per service.

┌─────────────────────────────────────────────────────────────────┐
│                    NestJS Microservices Architecture            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌──────────┐         ┌─────────────────────────────────────┐  │
│   │  Client  │────────►│         API Gateway                 │  │
│   │ (HTTP)   │         │       (Hybrid App)                  │  │
│   └──────────┘         └──────────────┬──────────────────────┘  │
│                                       │                         │
│            ┌──────────────────────────┼────────────────────┐    │
│            ▼                          ▼                    ▼    │
│   ┌─────────────────┐    ┌─────────────────┐    ┌──────────────┐│
│   │  User Service   │    │  Order Service  │    │Product Service│
│   │     (TCP)       │    │    (Redis)      │    │   (gRPC)     ││
│   └────────┬────────┘    └────────┬────────┘    └───────┬──────┘│
│            │                      │                     │       │
│            ▼                      ▼                     ▼       │
│   ┌─────────────────┐    ┌─────────────────┐    ┌──────────────┐│
│   │   PostgreSQL    │    │    MongoDB      │    │    Redis     ││
│   └─────────────────┘    └─────────────────┘    └──────────────┘│
└─────────────────────────────────────────────────────────────────┘

TCP Transport

TCP transport is NestJS's default and simplest microservice transport layer, using raw TCP sockets for fast, low-latency communication between services on the same network, ideal for internal service-to-service calls without the overhead of HTTP.

// ============ MICROSERVICE (Server) ============ // main.ts async function bootstrap() { const app = await NestFactory.createMicroservice<MicroserviceOptions>( AppModule, { transport: Transport.TCP, options: { host: '0.0.0.0', port: 3001, }, }, ); await app.listen(); } // user.controller.ts @Controller() export class UserController { @MessagePattern({ cmd: 'get_user' }) getUser(@Payload() data: { id: string }) { return { id: data.id, name: 'John Doe' }; } } // ============ CLIENT (Gateway) ============ @Module({ imports: [ ClientsModule.register([{ name: 'USER_SERVICE', transport: Transport.TCP, options: { host: 'user-service', port: 3001 }, }]), ], }) export class GatewayModule {} @Controller('users') export class GatewayController { constructor(@Inject('USER_SERVICE') private client: ClientProxy) {} @Get(':id') getUser(@Param('id') id: string) { return this.client.send({ cmd: 'get_user' }, { id }); } }

Redis Transport

Redis transport uses Redis pub/sub for service communication, enabling message broadcasting, simple service discovery, and persistence of messages during brief outages—great for teams already using Redis and needing lightweight distributed messaging.

// Microservice setup const app = await NestFactory.createMicroservice<MicroserviceOptions>( AppModule, { transport: Transport.REDIS, options: { host: 'localhost', port: 6379, retryAttempts: 5, retryDelay: 1000, }, }, ); // Client registration ClientsModule.register([{ name: 'CACHE_SERVICE', transport: Transport.REDIS, options: { host: 'redis', port: 6379, }, }]); // Handler @MessagePattern('notifications') handleNotification(@Payload() data: any, @Ctx() context: RedisContext) { console.log(`Channel: ${context.getChannel()}`); return this.notificationService.process(data); } // Communication flow: // ┌────────────┐ ┌─────────┐ ┌─────────────┐ // │ Service A │─────►│ Redis │◄─────│ Service B │ // │ (publish) │ │ PUB/SUB │ │ (subscribe) │ // └────────────┘ └─────────┘ └─────────────┘

NATS Transport

NATS is a lightweight, high-performance messaging system that NestJS supports natively, offering request-reply and pub-sub patterns with automatic load balancing across service instances—excellent for cloud-native applications requiring minimal latency.

// Microservice configuration const app = await NestFactory.createMicroservice<MicroserviceOptions>( AppModule, { transport: Transport.NATS, options: { servers: ['nats://localhost:4222'], queue: 'orders-queue', // Enable load balancing }, }, ); // Client setup ClientsModule.register([{ name: 'ORDER_SERVICE', transport: Transport.NATS, options: { servers: ['nats://nats-server:4222'], }, }]); // Service handler with queue groups (load balancing) @MessagePattern('order.create') async createOrder(@Payload() data: CreateOrderDto) { return this.orderService.create(data); } // NATS queue group load balancing: // ┌──────────┐ // │ Producer │──── order.create ────┐ // └──────────┘ │ // ┌──────▼──────┐ // │ NATS │ // │ Server │ // └──────┬──────┘ // ┌──────────────┼──────────────┐ // ▼ ▼ ▼ // [Instance 1] [Instance 2] [Instance 3] // (receives) (idle) (idle) // └── Only ONE instance receives each message ──┘

RabbitMQ Transport

RabbitMQ transport leverages AMQP protocol features like persistent queues, dead-letter exchanges, and complex routing, making it ideal for enterprise systems requiring guaranteed delivery, message acknowledgment, and sophisticated routing scenarios.

// Microservice with RabbitMQ const app = await NestFactory.createMicroservice<MicroserviceOptions>( AppModule, { transport: Transport.RMQ, options: { urls: ['amqp://user:pass@rabbitmq:5672'], queue: 'orders_queue', queueOptions: { durable: true, // Survives broker restart }, noAck: false, // Manual acknowledgment prefetchCount: 10, }, }, ); // Handler with acknowledgment @MessagePattern('process_order') async processOrder( @Payload() data: OrderDto, @Ctx() context: RmqContext, ) { const channel = context.getChannelRef(); const originalMsg = context.getMessage(); try { const result = await this.orderService.process(data); channel.ack(originalMsg); // Acknowledge success return result; } catch (error) { channel.nack(originalMsg, false, false); // Reject, don't requeue throw error; } } // RabbitMQ topology: // Producer ──► Exchange ──► Queue ──► Consumer // │ // └──► Dead Letter Queue (failed messages)

Kafka Transport

Kafka transport enables high-throughput, distributed event streaming with partitioned topics and consumer groups, perfect for event sourcing, real-time analytics, and systems requiring ordered message processing with replay capabilities.

// Kafka microservice const app = await NestFactory.createMicroservice<MicroserviceOptions>( AppModule, { transport: Transport.KAFKA, options: { client: { clientId: 'order-service', brokers: ['kafka:9092'], }, consumer: { groupId: 'order-consumer-group', }, }, }, ); // Kafka handler @MessagePattern('orders.created') async handleOrderCreated( @Payload() data: OrderCreatedEvent, @Ctx() context: KafkaContext, ) { const { offset, partition, topic } = context.getMessage(); console.log(`Topic: ${topic}, Partition: ${partition}, Offset: ${offset}`); return this.analyticsService.trackOrder(data); } // Client sending to Kafka @Inject('KAFKA_SERVICE') private kafka: ClientKafka; async onModuleInit() { this.kafka.subscribeToResponseOf('orders.created'); await this.kafka.connect(); } // Kafka partitioning: // ┌────────────────────────────────────────────────┐ // │ Topic: orders │ // ├────────────┬────────────┬────────────┬─────────┤ // │Partition 0 │Partition 1 │Partition 2 │Part. 3 │ // │[msg1,msg4] │[msg2,msg5] │[msg3,msg6] │[msg7] │ // └─────┬──────┴─────┬──────┴─────┬──────┴────┬────┘ // │ │ │ │ // Consumer 1 Consumer 2 Consumer 3 Consumer 3

gRPC Transport

gRPC transport uses Protocol Buffers for strongly-typed, efficient binary serialization with automatic code generation, bidirectional streaming, and excellent tooling—ideal for performance-critical internal services with strict API contracts.

// Proto file: hero.proto syntax = "proto3"; package hero; service HeroService { rpc FindOne (HeroById) returns (Hero); rpc FindMany (stream HeroById) returns (stream Hero); } message HeroById { int32 id = 1; } message Hero { int32 id = 1; string name = 2; } // Microservice setup const app = await NestFactory.createMicroservice<MicroserviceOptions>( AppModule, { transport: Transport.GRPC, options: { package: 'hero', protoPath: join(__dirname, 'hero.proto'), url: '0.0.0.0:5000', }, }, ); // Controller @Controller() export class HeroController { @GrpcMethod('HeroService', 'FindOne') findOne(data: HeroById): Hero { return { id: data.id, name: 'John' }; } @GrpcStreamMethod('HeroService', 'FindMany') findMany(data$: Observable<HeroById>): Observable<Hero> { return data$.pipe( map(({ id }) => ({ id, name: `Hero ${id}` })), ); } }

MQTT Transport

MQTT transport is a lightweight pub-sub protocol designed for IoT and constrained networks, supporting QoS levels and retained messages—perfect for IoT applications, mobile backends, and scenarios with unreliable network connections.

// MQTT microservice const app = await NestFactory.createMicroservice<MicroserviceOptions>( AppModule, { transport: Transport.MQTT, options: { url: 'mqtt://localhost:1883', username: 'user', password: 'password', }, }, ); // IoT sensor data handler @Controller() export class SensorController { @MessagePattern('sensors/+/temperature') // + is wildcard handleTemperature( @Payload() data: { value: number }, @Ctx() context: MqttContext, ) { const topic = context.getTopic(); // e.g., 'sensors/room1/temperature' console.log(`${topic}: ${data.value}°C`); } @MessagePattern('sensors/#') // # matches multiple levels handleAllSensors(@Payload() data: any) { return this.sensorService.log(data); } } // MQTT QoS Levels: // ┌─────┬─────────────────────────────────────────┐ // │ QoS │ Description │ // ├─────┼─────────────────────────────────────────┤ // │ 0 │ At most once (fire and forget) │ // │ 1 │ At least once (may duplicate) │ // │ 2 │ Exactly once (guaranteed) │ // └─────┴─────────────────────────────────────────┘

Custom Transporters

NestJS allows creating custom transporters by extending Server and ClientProxy classes, enabling integration with any messaging system like Amazon SQS, Google Pub/Sub, or proprietary protocols while maintaining the same decorator-based programming model.

// Custom transporter for AWS SQS export class SqsServer extends Server implements CustomTransportStrategy { private sqs: SQS; private queueUrl: string; constructor(options: SqsOptions) { super(); this.sqs = new SQS(options); this.queueUrl = options.queueUrl; } async listen(callback: () => void) { this.pollMessages(); callback(); } private async pollMessages() { while (true) { const { Messages } = await this.sqs.receiveMessage({ QueueUrl: this.queueUrl, WaitTimeSeconds: 20, }).promise(); for (const message of Messages || []) { const pattern = JSON.parse(message.Body).pattern; const handler = this.getHandlerByPattern(pattern); if (handler) { await handler(JSON.parse(message.Body).data); await this.sqs.deleteMessage({ QueueUrl: this.queueUrl, ReceiptHandle: message.ReceiptHandle, }).promise(); } } } } close() { // Cleanup } } // Usage const app = await NestFactory.createMicroservice(AppModule, { strategy: new SqsServer({ queueUrl: 'https://sqs...' }), });

Message Patterns

Message patterns are identifiers (strings or objects) that match incoming messages to their handlers, supporting both request-response (@MessagePattern) and event-based (@EventPattern) communication styles for flexible service interaction design.

@Controller() export class PaymentController { // String pattern - simple matching @MessagePattern('process_payment') processPayment(@Payload() data: PaymentDto) { return this.paymentService.process(data); } // Object pattern - more expressive @MessagePattern({ cmd: 'validate', version: 'v2' }) validatePaymentV2(@Payload() data: PaymentDto) { return this.paymentService.validateV2(data); } // Event pattern - fire and forget @EventPattern('payment_completed') handlePaymentCompleted(@Payload() data: PaymentEvent) { // No response needed this.analyticsService.track(data); } } // Client usage // Request-response (waits for reply) this.client.send({ cmd: 'validate', version: 'v2' }, payload); // Event (fire and forget) this.client.emit('payment_completed', event); // Pattern matching: // ┌──────────────────────────────────────────┐ // │ Incoming: { cmd: 'validate', version: 'v2' } // │ │ // │ Match: { cmd: 'validate', version: 'v2' } ✓ // │ No match: { cmd: 'validate' } ✗ // │ No match: { cmd: 'validate', version: 'v1' } ✗ // └──────────────────────────────────────────┘

Event-based Communication

Event-based communication uses @EventPattern and client.emit() for fire-and-forget messaging where the sender doesn't wait for or expect a response, perfect for audit logs, notifications, and triggering side effects across services.

// Publisher service @Injectable() export class OrderService { constructor( @Inject('NOTIFICATION_SERVICE') private notificationClient: ClientProxy, @Inject('ANALYTICS_SERVICE') private analyticsClient: ClientProxy, ) {} async createOrder(dto: CreateOrderDto) { const order = await this.orderRepo.save(dto); // Emit events - don't wait for response this.notificationClient.emit('order_created', { orderId: order.id, userId: order.userId, }); this.analyticsClient.emit('track_event', { event: 'purchase', properties: { orderId: order.id, amount: order.total }, }); return order; } } // Subscriber service @Controller() export class NotificationController { @EventPattern('order_created') async handleOrderCreated(@Payload() data: OrderCreatedEvent) { await this.emailService.sendOrderConfirmation(data.userId, data.orderId); // No return value needed } } // Event flow: // Order Service Message Broker // │ │ // │── emit('order_created') ──────►│ // │ (doesn't wait) │ // │ │──► Notification Service // │ │──► Analytics Service // │ │──► Inventory Service

Hybrid Applications

Hybrid applications combine HTTP and microservice transports in a single NestJS application, allowing you to expose REST APIs while simultaneously listening for messages from other services—common for API gateways and BFF (Backend for Frontend) patterns.

async function bootstrap() { // Create HTTP application const app = await NestFactory.create(AppModule); // Connect microservice transports app.connectMicroservice<MicroserviceOptions>({ transport: Transport.TCP, options: { port: 3001 }, }); app.connectMicroservice<MicroserviceOptions>({ transport: Transport.REDIS, options: { host: 'redis', port: 6379 }, }); app.connectMicroservice<MicroserviceOptions>({ transport: Transport.RMQ, options: { urls: ['amqp://rabbitmq:5672'], queue: 'events_queue', }, }); // Start all transports await app.startAllMicroservices(); // Start HTTP server await app.listen(3000); console.log('HTTP on :3000, TCP on :3001, Redis & RMQ connected'); } // Hybrid app architecture: // ┌─────────────────────────────────────────────┐ // │ Hybrid Application │ // ├─────────────────────────────────────────────┤ // │ ┌─────────┐ ┌─────┐ ┌───────┐ ┌─────┐ │ // │ │ HTTP │ │ TCP │ │ Redis │ │ RMQ │ │ // │ │ :3000 │ │:3001│ │ pub/sub│ │queue│ │ // │ └────┬────┘ └──┬──┘ └───┬───┘ └──┬──┘ │ // │ └──────────┴────────┴─────────┘ │ // │ │ │ // │ Shared Services │ // │ & Business Logic │ // └─────────────────────────────────────────────┘

Service Discovery

Service discovery enables microservices to dynamically find and communicate with each other without hardcoded addresses, typically implemented using tools like Consul, etcd, or Kubernetes DNS, with NestJS integrating through custom providers or configuration modules.

// Using Consul for service discovery @Module({ imports: [ ConsulModule.forRoot({ host: 'consul', port: 8500, }), ], }) export class AppModule {} @Injectable() export class ServiceRegistry { constructor(private consul: ConsulService) {} async registerService() { await this.consul.agent.service.register({ name: 'order-service', id: `order-${process.env.HOSTNAME}`, address: process.env.POD_IP, port: 3000, check: { http: `http://${process.env.POD_IP}:3000/health`, interval: '10s', }, }); } async discoverService(name: string): Promise<string> { const services = await this.consul.catalog.service.nodes(name); // Simple round-robin selection const service = services[Math.floor(Math.random() * services.length)]; return `http://${service.ServiceAddress}:${service.ServicePort}`; } } // Service discovery flow: // ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ // │ Service A │ │ Consul/ │ │ Service B │ // │ │ │ etcd │ │ │ // └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ // │ │ │ // │── register ────────►│◄── register ────────│ // │ │ │ // │── discover B ──────►│ │ // │◄── B's address ─────│ │ // │ │ │ // │─────────── direct communication ─────────►│