NestJS — Enterprise Node.js

Message & Event Patterns

16 min Lesson 37 of 48

Message & Event Patterns

NestJS microservices communicate through two fundamental patterns: request-response (messages) and fire-and-forget (events). Choosing between them shapes how services stay decoupled, how errors propagate, and whether callers block waiting for a reply. Understanding both patterns — and the decorators and proxy methods that implement them — is the backbone of any NestJS microservice architecture.

@MessagePattern — Request-Response

A handler decorated with @MessagePattern participates in a request-response exchange. The caller sends a message and waits for a reply. This mirrors a traditional HTTP call: the client blocks (or awaits a Promise) until the remote handler returns or an error is thrown.

// orders.controller.ts — microservice side import { Controller } from '@nestjs/common'; import { MessagePattern, Payload } from '@nestjs/microservices'; @Controller() export class OrdersController { @MessagePattern({ cmd: 'get_order' }) getOrder(@Payload() data: { id: number }) { // returning a value sends the reply back to the caller return { id: data.id, status: 'confirmed', total: 99.99 }; } }

The pattern object ({ cmd: 'get_order' }) is the routing key. It can be a string literal or any serialisable object, as long as both sides agree on the same value.

@EventPattern — Fire-and-Forget

A handler decorated with @EventPattern receives events. The publisher does not wait for a response — it emits and moves on immediately. This is ideal for side-effects such as sending emails, updating read models, or writing audit logs, where the publisher has no interest in the outcome.

// notifications.controller.ts — microservice side import { Controller } from '@nestjs/common'; import { EventPattern, Payload } from '@nestjs/microservices'; @Controller() export class NotificationsController { @EventPattern('order.placed') handleOrderPlaced(@Payload() data: { orderId: number; email: string }) { // no return value — the publisher never waits for this console.log(`Sending confirmation email to ${data.email}`); } }
Return values from @EventPattern handlers are silently discarded. The transport layer does not route them back. If you accidentally return something important from an event handler, it disappears. Only use @MessagePattern when the caller needs a result.

ClientProxy — Sending from the Caller

On the calling side, inject a ClientProxy (produced by ClientsModule.register) and choose the matching method:

  • client.send(pattern, payload) — for @MessagePattern; returns an Observable that resolves with the reply.
  • client.emit(pattern, payload) — for @EventPattern; returns an Observable that completes when the message is dispatched (not when handled).
// api-gateway.service.ts — caller side import { Inject, Injectable } from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; import { firstValueFrom } from 'rxjs'; @Injectable() export class ApiGatewayService { constructor( @Inject('ORDERS_SERVICE') private readonly ordersClient: ClientProxy, ) {} async getOrder(id: number) { // send() — blocks until the remote handler replies return firstValueFrom( this.ordersClient.send<{ id: number; status: string }>( { cmd: 'get_order' }, { id }, ), ); } placeOrder(payload: { userId: number; items: string[] }) { // emit() — fire-and-forget; does NOT wait for a handler response this.ordersClient.emit('order.placed', payload).subscribe(); } }
Convert Observables with firstValueFrom. ClientProxy.send() returns an RxJS Observable, not a Promise. Wrap it with firstValueFrom() (from rxjs) to use it comfortably inside async/await code. Do not ignore the subscription — an unsubscribed Observable never executes.

Payload & Context Handling

Use @Payload() to extract the message data and @Ctx() to access transport-specific context (headers, acknowledgement functions, partition info, etc.):

import { MessagePattern, Payload, Ctx, KafkaContext } from '@nestjs/microservices'; @MessagePattern('inventory.check') async checkInventory( @Payload() data: { sku: string; qty: number }, @Ctx() context: KafkaContext, ) { const topic = context.getTopic(); const partition = context.getPartition(); console.log(`Message from topic ${topic}, partition ${partition}`); return { sku: data.sku, available: true }; }
Do not mix up send() and emit() with the wrong pattern decorator. Calling client.send() against a handler registered with @EventPattern will hang forever waiting for a reply that never comes. Calling client.emit() against a @MessagePattern handler means your reply is discarded — the caller gets nothing. Keep the pair consistent.

Summary

NestJS microservices offer two communication primitives: @MessagePattern + ClientProxy.send() for request-response (the caller awaits a reply), and @EventPattern + ClientProxy.emit() for fire-and-forget (no reply expected). Use @Payload() to access message data and @Ctx() for transport context. Always match the decorator on the handler with the correct proxy method on the caller — mismatches cause silent failures.