Skip to content
NestJS ns microservices 4 min read

Microservices Overview

A NestJS microservice is an application that communicates over a message-based protocol rather than the HTTP request/response cycle you get from the default platform. The @nestjs/microservices package gives you the same controllers, providers, modules, and dependency injection you already know, but swaps the underlying transport for something like TCP, Redis, NATS, RabbitMQ, Kafka, or gRPC. This means you write your business logic once and let Nest handle the wire format, serialization, and connection lifecycle behind a thin, consistent abstraction.

Why a separate microservices layer

The core @nestjs/platform-express (or Fastify) adapter is built around HTTP. Microservices, by contrast, are usually about asynchronous, low-overhead communication between services in a distributed system. Nest unifies both worlds: the programming model is identical, so the only thing that changes is how a message arrives and how a response leaves. That symmetry is the whole point — you should be able to move a handler from an HTTP controller to a microservice with minimal friction.

Install the package alongside the transport driver you intend to use:

npm install @nestjs/microservices

Transporters

A transporter is the strategy that encapsulates the protocol used to send and receive messages. Nest ships with several built-in transporters, and you select one when you create the microservice. Each transporter has its own connection options, but they all expose the same MessagePattern / EventPattern handler API.

TransporterTransport enumTypical use case
TCPTransport.TCPSimple, dependency-free internal RPC
RedisTransport.REDISPub/sub style messaging via Redis
NATSTransport.NATSLightweight, high-throughput messaging
RabbitMQTransport.RMQReliable queues and acknowledgements
KafkaTransport.KAFKAEvent streaming and log-based pipelines
gRPCTransport.GRPCStrongly-typed contracts via Protocol Buffers

The server role

A microservice server listens for incoming messages and dispatches them to handlers. You bootstrap it with NestFactory.createMicroservice instead of NestFactory.create. The generic type argument carries the transport-specific options so they are fully type-checked.

// 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.TCP,
      options: { host: '0.0.0.0', port: 3001 },
    },
  );

  await app.listen();
  console.log('Math microservice is listening on TCP 3001');
}
bootstrap();

Handlers live in ordinary controllers. The @MessagePattern decorator binds a method to a request/response interaction, while @EventPattern binds it to a fire-and-forget event.

// math.controller.ts
import { Controller } from '@nestjs/common';
import { MessagePattern, EventPattern, Payload } from '@nestjs/microservices';

@Controller()
export class MathController {
  @MessagePattern({ cmd: 'sum' })
  accumulate(@Payload() data: number[]): number {
    return data.reduce((acc, value) => acc + value, 0);
  }

  @EventPattern('order_created')
  handleOrderCreated(@Payload() payload: { orderId: string }) {
    console.log(`Order received: ${payload.orderId}`);
  }
}

The pattern passed to @MessagePattern can be a string, number, or object. Nest matches it by deep equality, so the client must send the exact same pattern shape to reach the handler.

The client role: ClientProxy

To call a microservice, you inject a ClientProxy. This is the abstraction that hides transport details from the caller — it exposes just two methods regardless of the underlying protocol:

  • send(pattern, data) returns an RxJS Observable for request/response messaging (matched by @MessagePattern).
  • emit(pattern, data) publishes an event with no expected reply (matched by @EventPattern).

Register the client in a module with ClientsModule, then inject it by token.

// app.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { GatewayController } from './gateway.controller';

@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'MATH_SERVICE',
        transport: Transport.TCP,
        options: { host: 'localhost', port: 3001 },
      },
    ]),
  ],
  controllers: [GatewayController],
})
export class AppModule {}
// gateway.controller.ts
import { Controller, Get, Inject } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { Observable } from 'rxjs';

@Controller('math')
export class GatewayController {
  constructor(@Inject('MATH_SERVICE') private readonly client: ClientProxy) {}

  @Get('sum')
  sum(): Observable<number> {
    return this.client.send<number, number[]>({ cmd: 'sum' }, [1, 2, 3, 4]);
  }
}

Hitting the HTTP gateway forwards the request over TCP to the math microservice and streams the reply back:

Output:

$ curl http://localhost:3000/math/sum
10

Because send() returns an Observable, Nest’s HTTP layer subscribes to it automatically when you return it from a route handler. The TCP frame, the JSON serialization, and the correlation between request and response are all managed for you.

How Nest abstracts the transport

The key idea is that ClientProxy and the @MessagePattern/@EventPattern handlers form a stable contract. Switching from TCP to Redis or NATS is mostly a configuration change in createMicroservice and ClientsModule.register — your controllers and services stay untouched. This lets you start with the zero-dependency TCP transport during development and graduate to a broker like RabbitMQ or Kafka in production without rewriting application logic.

Best practices

  • Keep message patterns in a shared constants file so the server and client never drift out of sync.
  • Use @MessagePattern for queries that need a reply and @EventPattern for fire-and-forget notifications; do not block on events.
  • Prefer object patterns ({ cmd: 'sum' }) over bare strings — they are self-documenting and easier to namespace.
  • Connect long-lived clients via the module’s onApplicationBootstrap lifecycle rather than per-request to avoid repeated handshakes.
  • Validate and type your payloads with DTOs and pipes just as you would for HTTP endpoints.
  • Choose a broker-backed transporter (RabbitMQ, Kafka, NATS) when you need durability, retries, or back-pressure; reserve TCP for simple internal RPC.
Last updated June 14, 2026
Was this helpful?