TL;DR
- NestJS supports multiple transport layers: TCP, Redis, NATS, RabbitMQ, Kafka, gRPC.
- Two patterns: Request-Response (sync) and Event-Based (async/fire-and-forget).
- Use
@MessagePattern()for request-response,@EventPattern()for events. - A NestJS app can be both an HTTP server AND a microservice consumer.
Step 1: Microservice Architecture in NestJS
Microservices architecture splits a monolithic application into independent services that communicate over the network. NestJS adopted this pattern because large applications eventually need independent deployment, scaling, and team ownership of different domains. NestJS's microservice module provides transport-layer abstractions (TCP, Redis, NATS, gRPC, Kafka) so your business logic stays the same regardless of how services communicate. This architecture shines when different parts of your system have different scaling needs or when multiple teams need to deploy independently.
┌──────────────┐ HTTP ┌──────────────┐
│ Client │──────────────▶│ API Gateway │
└──────────────┘ │ (HTTP app) │
└──────┬───────┘
│ TCP/Redis/gRPC
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Users Service │ │Orders Service│ │ Email Service │
│ (TCP) │ │ (gRPC) │ │ (Redis pub) │
└──────────────┘ └──────────────┘ └──────────────┘
Step 2: Creating a Microservice (TCP Transport)
TCP transport is NestJS's simplest microservice transport — direct point-to-point communication without a message broker. It's perfect for getting started, internal services on the same network, and scenarios where you don't need pub/sub or message persistence. The microservice listens on a port, receives message patterns, and responds. NestJS uses the same decorator-based controller pattern you already know (@MessagePattern instead of @Get), making the transition from HTTP to microservice communication feel natural.
The Microservice (Server)
// users-microservice/main.ts
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: 3001,
},
},
);
await app.listen();
console.log("Users microservice running on port 3001");
}
bootstrap();
Message Handlers
// users-microservice/users.controller.ts
import { Controller } from "@nestjs/common";
import { MessagePattern, EventPattern, Payload } from "@nestjs/microservices";
@Controller()
export class UsersController {
constructor(private usersService: UsersService) {}
// Request-Response — client expects a reply
@MessagePattern({ cmd: "get_user" })
async getUser(@Payload() data: { id: string }) {
return this.usersService.findById(data.id);
}
@MessagePattern({ cmd: "create_user" })
async createUser(@Payload() data: CreateUserDto) {
return this.usersService.create(data);
}
// Event-Based — fire and forget, no reply expected
@EventPattern("user_created")
async handleUserCreated(@Payload() data: { userId: string; email: string }) {
// Send welcome email, update analytics, etc.
await this.usersService.sendWelcomeEmail(data.email);
}
}
Step 3: The Client (API Gateway)
The API gateway pattern uses a single HTTP-facing service that routes requests to internal microservices. This exists because you don't want to expose internal service topology to clients (frontend shouldn't know about 5 different service URLs), and you need a single place for cross-cutting concerns (auth, rate limiting, request logging). NestJS's ClientProxy provides a clean interface for sending messages to microservices with automatic serialization, error propagation, and timeout handling.
Register Client in Module
// api-gateway/app.module.ts
import { Module } from "@nestjs/common";
import { ClientsModule, Transport } from "@nestjs/microservices";
@Module({
imports: [
ClientsModule.register([
{
name: "USERS_SERVICE",
transport: Transport.TCP,
options: {
host: "localhost",
port: 3001,
},
},
]),
],
})
export class AppModule {}
Send Messages from Controller
// api-gateway/users.controller.ts
import { Controller, Get, Post, Body, Param, Inject } from "@nestjs/common";
import { ClientProxy } from "@nestjs/microservices";
import { firstValueFrom } from "rxjs";
@Controller("users")
export class UsersController {
constructor(@Inject("USERS_SERVICE") private client: ClientProxy) {}
@Get(":id")
async getUser(@Param("id") id: string) {
// Request-Response — send and wait for reply
return firstValueFrom(
this.client.send({ cmd: "get_user" }, { id }),
);
}
@Post()
async createUser(@Body() dto: CreateUserDto) {
const user = await firstValueFrom(
this.client.send({ cmd: "create_user" }, dto),
);
// Fire event — don't wait for response
this.client.emit("user_created", {
userId: user.id,
email: user.email,
});
return user;
}
}
Step 4: Redis Transport (Pub/Sub)
Redis transport adds pub/sub messaging between services, solving the problem of one-to-many communication (one event triggers actions in multiple services). Unlike TCP's point-to-point, Redis pub/sub lets you emit events that any number of subscribers receive independently. It's also more resilient — services can restart without breaking connections, and Redis acts as a lightweight message broker. Use this when services need loose coupling: user-created events trigger email, analytics, and onboarding services independently.
Setup
npm install @nestjs/microservices redis
Microservice with Redis
// main.ts
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.REDIS,
options: {
host: "localhost",
port: 6379,
},
},
);
Client with Redis
ClientsModule.register([
{
name: "NOTIFICATIONS_SERVICE",
transport: Transport.REDIS,
options: {
host: "localhost",
port: 6379,
},
},
]);
When to Use Redis Transport
- Multiple consumers need to receive the same event (pub/sub)
- Decoupled services that don't need direct connections
- Event-driven architectures where ordering isn't critical
Step 5: Hybrid Application (HTTP + Microservice)
Hybrid applications serve both HTTP requests (REST/GraphQL API) and microservice messages (internal communication) from a single process. This exists because not every service needs to be a pure microservice — often you want a service that has its own API but also listens for events from other services. For example, a notifications service might expose REST endpoints for CRUD but also listen for user-created events. NestJS makes this trivial by connecting multiple transports to one application.
A single app can serve HTTP requests AND listen for microservice messages:
// main.ts
import { NestFactory } from "@nestjs/core";
import { Transport, MicroserviceOptions } from "@nestjs/microservices";
import { AppModule } from "./app.module";
async function bootstrap() {
// Create HTTP server
const app = await NestFactory.create(AppModule);
// Also connect as microservice consumer
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.TCP,
options: { port: 3001 },
});
// Start both
await app.startAllMicroservices();
await app.listen(3000);
console.log("HTTP on 3000, Microservice on 3001");
}
bootstrap();
Step 6: Error Handling in Microservices
Error handling in microservices is fundamentally different from HTTP: there's no browser to show a 500 page, errors must be serialized across network boundaries, and failures in one service shouldn't cascade to crash others. NestJS provides RpcException for request-response patterns (errors propagated back to caller) and observable error handling for event patterns (errors logged but don't block the emitter). Understanding these patterns prevents the "one service dies and takes down everything" failure mode.
Request-Response Errors
import { RpcException } from "@nestjs/microservices";
@MessagePattern({ cmd: "get_user" })
async getUser(@Payload() data: { id: string }) {
const user = await this.usersService.findById(data.id);
if (!user) {
// This propagates back to the caller
throw new RpcException({
status: 404,
message: `User ${data.id} not found`,
});
}
return user;
}
Client-Side Error Handling
@Get(":id")
async getUser(@Param("id") id: string) {
try {
return await firstValueFrom(
this.client.send({ cmd: "get_user" }, { id }),
);
} catch (error) {
// RpcException is caught here
throw new NotFoundException(error.message);
}
}
Event Errors (No Reply)
@EventPattern("order_placed")
async handleOrder(@Payload() data: OrderData) {
try {
await this.processOrder(data);
} catch (error) {
// Log error — can't send back to caller
this.logger.error(`Failed to process order: ${error.message}`, data);
// Optionally: emit to dead letter queue
this.client.emit("order_failed", { ...data, error: error.message });
}
}
Step 7: Communication Patterns Comparison
Choosing the right communication pattern determines your system's reliability and coupling characteristics. Request-response (synchronous) is simple but creates temporal coupling — if the target service is down, the request fails. Events (asynchronous) decouple services but don't guarantee processing. Understanding when to use each pattern (queries need responses, notifications are fire-and-forget, critical operations need acknowledgment) is what separates working microservices from fragile distributed monoliths.
| Pattern | Method | Use Case | Guarantee |
|---|---|---|---|
| Request-Response | client.send() |
Get data, CRUD | Reply guaranteed |
| Event | client.emit() |
Notifications, logs | Fire-and-forget |
| Transport | Speed | Scalability | Use Case |
|---|---|---|---|
| TCP | Fastest | Limited | Same network, simple |
| Redis | Fast | Good | Pub/sub, caching integrated |
| NATS | Fast | Excellent | High-throughput events |
| RabbitMQ | Medium | Excellent | Reliable delivery, routing |
| Kafka | Medium | Excellent | Event sourcing, streams |
| gRPC | Fast | Good | Strongly-typed, binary protocol |
Interview Questions
-
What's the difference between
send()andemit()in NestJS microservices?send()is request-response: client waits for a reply.emit()is event-based: fire-and-forget, no reply expected.
-
How do you choose a transport layer?
- TCP for simple same-network services. Redis for pub/sub patterns. RabbitMQ for reliable delivery with complex routing. Kafka for event sourcing and high-throughput streaming.
-
What's a hybrid application in NestJS?
- An app that serves both HTTP requests (via
app.listen()) and microservice messages (viaapp.startAllMicroservices()). Same codebase, dual protocols.
- An app that serves both HTTP requests (via
-
How do you handle errors across microservices?
- Request-response: throw
RpcExceptionwhich propagates to the caller. Events: log errors, emit to dead letter queues, or use transport-level retries.
- Request-response: throw