Microservices with NestJS in 2026: Architecture, gRPC and Interview Questions

A practical guide to NestJS microservices architecture with gRPC, covering service boundaries, transport layers, streaming patterns, and common interview questions for 2026.

NestJS microservices architecture with gRPC communication between distributed backend services

NestJS microservices have become the standard approach for building distributed Node.js backends in 2026. With NestJS 11's improved transporter layer, first-class gRPC support, and automatic trace ID propagation, the framework provides everything needed to split a monolith into well-bounded services without sacrificing developer experience.

gRPC vs REST for internal services

gRPC uses Protocol Buffers and HTTP/2, delivering up to 10x faster serialization and native bidirectional streaming compared to JSON over REST. For service-to-service communication inside a cluster, gRPC reduces latency and enforces strict contracts through .proto files. REST remains the better choice for public-facing APIs consumed by browsers and third parties.

NestJS Microservices Transport Layer Architecture

The @nestjs/microservices package replaces HTTP controllers with message-oriented handlers. Instead of routing by URL path, each handler responds to a message pattern — a string or object key that identifies the operation. This abstraction means the same handler logic works across TCP, Redis, NATS, Kafka, RabbitMQ, or gRPC without code changes.

NestJS 11 introduced the unwrap() method on all transporters, granting direct access to the underlying client instance. This solves a long-standing pain point: inspecting connection state, tuning keepalive intervals, or accessing broker-specific features that the NestJS abstraction doesn't expose.

Two communication patterns drive NestJS microservices:

  • Request-response (@MessagePattern): the client sends a message and waits for a reply. Suitable for synchronous queries like fetching a user profile or validating a token.
  • Event-based (@EventPattern): the client emits an event and moves on — no response expected. Ideal for audit logs, notifications, or triggering downstream workflows.
orders.controller.tstypescript
import { Controller } from '@nestjs/common';
import { MessagePattern, EventPattern, Payload } from '@nestjs/microservices';
import { OrdersService } from './orders.service';
import { CreateOrderDto } from './dto/create-order.dto';

@Controller()
export class OrdersController {
  constructor(private readonly ordersService: OrdersService) {}

  // Request-response: caller waits for the created order
  @MessagePattern('order.create')
  async createOrder(@Payload() data: CreateOrderDto) {
    return this.ordersService.create(data);
  }

  // Event-based: fire and forget, no response returned
  @EventPattern('order.shipped')
  async handleOrderShipped(@Payload() data: { orderId: string }) {
    await this.ordersService.markAsShipped(data.orderId);
  }
}

The @MessagePattern handler automatically serializes the return value and sends it back through the transport. The @EventPattern handler returns nothing — NestJS discards any return value.

Configuring gRPC as a NestJS Microservice Transport

gRPC integration in NestJS starts with a .proto file that defines the service contract. Protocol Buffers enforce type safety at the wire level — both client and server must agree on the exact shape of every message before a single byte travels the network.

proto/users.protoprotobuf
syntax = "proto3";

package users;

service UsersService {
  rpc FindOne (UserById) returns (User);
  rpc FindMany (UserFilter) returns (stream User);
}

message UserById {
  string id = 1;
}

message UserFilter {
  string role = 1;
  int32 limit = 2;
}

message User {
  string id = 1;
  string email = 2;
  string name = 3;
  string role = 4;
}

On the server side, the NestJS application bootstraps a gRPC microservice by pointing to the proto file and binding a network address:

main.tstypescript
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { join } from 'path';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.GRPC,
      options: {
        package: 'users',
        protoPath: join(__dirname, 'proto/users.proto'),
        url: '0.0.0.0:5000',
      },
    },
  );
  await app.listen();
}
bootstrap();

The controller uses @GrpcMethod instead of @MessagePattern. The decorator takes the service name and the RPC method name as arguments, mapping directly to the proto definition:

users.controller.tstypescript
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { UsersService } from './users.service';

@Controller()
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @GrpcMethod('UsersService', 'FindOne')
  async findOne(data: { id: string }) {
    return this.usersService.findById(data.id);
  }
}

NestJS handles proto loading, serialization, and HTTP/2 connection management automatically. The controller method receives plain TypeScript objects — no manual proto decoding needed.

gRPC Streaming Patterns in NestJS

Server streaming fits scenarios where a single request produces multiple results over time — think real-time price feeds, paginated database cursors, or progress updates during a long-running task. The handler returns an RxJS Observable, and NestJS streams each emitted value to the client as a separate gRPC message:

users.controller.ts — server streamingtypescript
import { Observable, from } from 'rxjs';
import { map } from 'rxjs/operators';
import { GrpcMethod } from '@nestjs/microservices';

@GrpcMethod('UsersService', 'FindMany')
findMany(data: { role: string; limit: number }): Observable<any> {
  // Stream users matching the filter one by one
  const users$ = from(this.usersService.findByRole(data.role, data.limit));
  return users$.pipe(
    map((user) => ({
      id: user.id,
      email: user.email,
      name: user.name,
      role: user.role,
    })),
  );
}

Bidirectional streaming uses @GrpcStreamCall(). Both client and server send independent streams — useful for chat systems, collaborative editing, or multiplexed telemetry ingestion. The handler receives a grpc.ServerDuplexStream instance and manages the read/write cycle manually.

Streaming error handling

A crashed Observable inside a gRPC stream terminates the entire call with a CANCELLED status. Wrap stream logic in catchError operators and emit gRPC-appropriate error metadata. Unhandled stream errors are the number-one cause of mysterious connection drops in production NestJS gRPC services.

Hybrid Applications: HTTP and gRPC on the Same Service

Many real-world NestJS services need both a public REST API and internal gRPC endpoints. The hybrid application pattern connects multiple transports to a single NestJS instance:

main.ts — hybrid applicationtypescript
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { join } from 'path';
import { AppModule } from './app.module';

async function bootstrap() {
  // HTTP server on port 3000
  const app = await NestFactory.create(AppModule);

  // gRPC microservice on port 5000
  app.connectMicroservice<MicroserviceOptions>({
    transport: Transport.GRPC,
    options: {
      package: 'users',
      protoPath: join(__dirname, 'proto/users.proto'),
      url: '0.0.0.0:5000',
    },
  });

  await app.startAllMicroservices();
  await app.listen(3000);
}
bootstrap();

This approach avoids deploying separate services when a single bounded context needs both external and internal interfaces. The same dependency injection container, guards, interceptors, and pipes apply to both transports — reducing duplication and keeping behavior consistent.

Ready to ace your Node.js / NestJS interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Service Boundaries and Domain-Driven Design with NestJS

Splitting a monolith into microservices without clear boundaries creates a distributed monolith — the worst of both architectures. Each NestJS microservice should own a single bounded context: its own database, its own domain models, and its own deployment lifecycle.

Practical guidelines for drawing service boundaries in NestJS:

  • One NestJS module per aggregate root. If Order and OrderLine always change together, they belong in the same service. If User and Order change independently, they deserve separate services.
  • Async events for cross-domain side effects. When an order is placed, emit an order.created event via Kafka or RabbitMQ. The inventory service reacts without the order service knowing it exists.
  • Shared proto packages for contracts. Store .proto files in a shared repository. Both producer and consumer generate types from the same source of truth — drift becomes a CI failure, not a production incident.

NestJS 11's microservice options from the DI container make service configuration testable. Instead of hardcoding broker URLs in main.ts, inject a ConfigService and swap connection strings per environment without rebuilding.

Reliability Patterns: Timeouts, Retries, and Circuit Breakers

Internal gRPC calls without deadlines turn a single slow dependency into a system-wide outage. Every ClientProxy.send() call should include a timeout:

orders.service.tstypescript
import { Inject, Injectable } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { firstValueFrom, timeout, retry } from 'rxjs';

@Injectable()
export class OrdersService {
  private usersService: any;

  constructor(@Inject('USERS_PACKAGE') private client: ClientGrpc) {}

  onModuleInit() {
    this.usersService = this.client.getService('UsersService');
  }

  async getOrderWithUser(orderId: string, userId: string) {
    // 3-second deadline, 2 retries with exponential backoff
    const user = await firstValueFrom(
      this.usersService.findOne({ id: userId }).pipe(
        timeout(3000),
        retry({ count: 2, delay: (err, retryCount) => {
          const jitter = Math.random() * 100;
          return new Promise(r => setTimeout(r, 1000 * retryCount + jitter));
        }}),
      ),
    );
    return { orderId, user };
  }
}

Retries should only apply to idempotent operations. Retrying a CreateOrder call risks duplicate orders. Retrying a FindOne query is safe because it has no side effects.

For circuit breaker functionality, the opossum library integrates well with NestJS. Wrap ClientGrpc calls in a circuit breaker that opens after N consecutive failures and closes after a configurable reset timeout.

NestJS 11 trace propagation

NestJS 11 automatically propagates trace IDs across Kafka, RabbitMQ, and gRPC transports. Combined with OpenTelemetry, this enables full distributed tracing without custom interceptors — a significant improvement over NestJS 10 where manual context propagation was required.

NestJS Microservices Interview Questions

Technical interviews for backend positions increasingly test microservices knowledge alongside framework-specific patterns. These questions cover the concepts most frequently asked in NestJS-focused interviews.

What is the difference between @MessagePattern and @EventPattern?

@MessagePattern implements request-response: the client sends a message and blocks until it receives a reply. @EventPattern implements fire-and-forget: the client emits an event and continues execution immediately. Under the hood, @MessagePattern returns the handler's result through the transport, while @EventPattern discards return values.

How does NestJS load and bind .proto files for gRPC?

NestJS uses the @grpc/proto-loader package to parse .proto files at startup and generate service descriptors. The GrpcOptions configuration specifies the proto path and package name. Controllers annotated with @GrpcMethod('ServiceName', 'MethodName') are matched against the parsed descriptors. If a method declared in the proto has no matching handler, NestJS throws at bootstrap.

When should gRPC replace REST between microservices?

gRPC fits internal service-to-service calls where type safety, performance, and streaming matter. Protocol Buffers produce smaller payloads and faster serialization than JSON. HTTP/2 multiplexing reduces connection overhead. REST remains preferable for public APIs, browser clients, and third-party integrations where human readability and broad tooling support outweigh raw performance.

How does the hybrid application pattern work?

NestFactory.create() boots an HTTP server. Calling app.connectMicroservice() attaches additional transports — gRPC, Kafka, Redis, or any supported transporter — to the same NestJS application instance. All transports share the same module tree, DI container, and middleware pipeline. app.startAllMicroservices() starts all connected transporters, and app.listen() starts the HTTP layer.

What reliability patterns prevent cascading failures?

Three patterns matter most: timeouts (every RPC call needs a deadline), retries with jitter (only for idempotent operations, with exponential backoff plus random jitter to avoid thundering herds), and circuit breakers (open after N failures, half-open after a cooldown, closed once health checks pass). NestJS's RxJS integration makes timeouts and retries composable through pipe operators.

For deeper practice on NestJS architecture patterns, the NestJS modules and dependency injection module covers the DI fundamentals that underpin microservice composition. The middleware and interceptors module addresses cross-cutting concerns shared across transports.

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Conclusion

  • NestJS 11's transport abstraction lets services switch between TCP, gRPC, Kafka, and NATS without rewriting handler logic — the @MessagePattern and @EventPattern decorators work identically across all transports
  • gRPC with Protocol Buffers enforces strict contracts between services through .proto files, catching integration errors at compile time rather than in production
  • Hybrid applications combine HTTP and gRPC on the same NestJS instance, eliminating the need to deploy separate services for public and internal interfaces
  • Server streaming via RxJS Observables enables real-time data feeds without WebSocket complexity — NestJS maps each Observable emission to a gRPC stream message
  • Every internal RPC call requires a deadline, retries apply only to idempotent operations, and circuit breakers prevent a single slow service from cascading into a full outage
  • Service boundaries follow domain-driven design: one bounded context per service, async events for cross-domain communication, shared proto packages for contract enforcement

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Tags

#nestjs
#microservices
#grpc
#nodejs
#typescript
#architecture

Share

Related articles