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:

Typescript
// 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:

Typescript
// 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:

Typescript
// 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 {}
Typescript
// 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 @MessagePattern handler 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.