Microservices with NestJS
Nest's built-in microservice transport layer — TCP, Redis and Kafka — and one service calling another.
Beyond HTTP: Nest's microservice transport layer
Every example so far in this track has one service talking to the outside world over HTTP. The @nestjs/microservices package lets a Nest application communicate over a completely different transport instead — TCP, Redis pub/sub, Kafka, NATS, RabbitMQ, MQTT, and even gRPC — using the same familiar @Injectable()/decorator style as controllers and providers, rather than a different framework entirely. This is what people usually mean by "NestJS microservices": not a different architectural style bolted on top, but Nest's DI and decorator model extended to non-HTTP transports.
Pure microservice apps vs hybrid apps
A pure microservice app has no HTTP listener at all — it only receives messages over its configured transport:
// main.ts — a standalone TCP microservice
import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { UsersModule } from './users.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(UsersModule, {
transport: Transport.TCP,
options: { host: '0.0.0.0', port: 4001 },
});
await app.listen();
}
bootstrap();
A hybrid app keeps its normal HTTP API running and additionally listens on a second transport, via app.connectMicroservice(...) alongside the usual NestFactory.create — useful when a service needs to serve regular REST clients while also accepting internal messages from other services.
Message patterns
Inside the microservice, @MessagePattern plays the same role @Get()/@Post() play for HTTP — except instead of matching a verb and a URL path, it matches an arbitrary pattern object carried inside the message itself:
// users.controller.ts — inside the Users microservice
import { Controller } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';
import { UsersService } from './users.service';
@Controller()
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@MessagePattern({ cmd: 'find_user' })
findOne(@Payload() id: number) {
return this.usersService.findOne(id);
}
}
@MessagePattern expects a reply to be sent back to whoever sent the message — the request/response shape. Its counterpart, @EventPattern, handles a fire-and-forget notification instead, with no reply expected at all.
Calling it from another service with ClientProxy
A second service — say, an OrdersModule that's also the public-facing HTTP API — reaches the Users microservice through a ClientProxy, registered once and injected like any other provider:
// orders.module.ts
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: 'localhost', port: 4001 } },
]),
],
controllers: [OrdersController],
providers: [OrdersService],
})
export class OrdersModule {}
// orders.service.ts
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(userId: number, item: string) {
const user = await firstValueFrom(this.usersClient.send({ cmd: 'find_user' }, userId));
return { user, item };
}
}
usersClient.send(pattern, payload) sends a message matching { cmd: 'find_user' } to the Users microservice and returns an RxJS Observable that resolves once a reply comes back — firstValueFrom converts that into a plain Promise, so createOrder can simply await it like any other async call, even though under the hood a message crossed a TCP connection to an entirely separate process. .emit(pattern, payload) is the fire-and-forget counterpart to .send(), used with @EventPattern() on the receiving side when no reply is expected.
Choosing a transport
| Transport | Good for |
|---|---|
| TCP | The simplest option — direct service-to-service calls with no extra infrastructure to run |
| Redis | Lightweight pub/sub, especially in a stack already using Redis for caching |
| Kafka | High-throughput event streaming with a durable, replayable log and multiple independent consumers |
| RabbitMQ | Mature message-broker semantics — queues, routing rules, retries, dead-letter queues |
Common mistakes
- Reaching for a heavyweight transport like Kafka for a simple two-service request/response that plain TCP would handle with far less operational overhead to run and monitor.
- Using
.emit()(fire-and-forget) where the caller actually needs a reply back, or.send()(waits for a response) for something that should really just be a one-way notification — the two have genuinely different failure and latency characteristics. - Forgetting that a
@MessagePatternhandler is only reachable over its configured transport — it's completely invisible to a plain HTTP request, unlike an ordinary@Controller()route in the same application.