Skip to content
NestJS ns microservices 4 min read

ClientProxy & Communication

When one Nest service needs to talk to another over a transport layer, it does so through a ClientProxy. The proxy is a thin, transport-agnostic abstraction: you tell it what to send and where, and it serializes the payload, dispatches it over TCP/Redis/NATS/Kafka/etc., and hands you back an RxJS Observable. Mastering the two methods it exposes — send() for request-response and emit() for fire-and-forget events — is the foundation of inter-service communication in NestJS.

Registering clients with ClientsModule

The idiomatic way to obtain a ClientProxy is to register it in a module with ClientsModule.register(). Each entry defines a token (the injection name), a transport, and transport-specific options. The token is what you inject into providers and controllers.

// 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: 'BILLING_SERVICE',
        transport: Transport.TCP,
        options: { host: 'localhost', port: 3001 },
      },
    ]),
  ],
  controllers: [OrdersController],
  providers: [OrdersService],
})
export class OrdersModule {}

When configuration must come from the environment, prefer registerAsync() so you can inject ConfigService instead of hardcoding hosts and ports.

ClientsModule.registerAsync([
  {
    name: 'BILLING_SERVICE',
    imports: [ConfigModule],
    inject: [ConfigService],
    useFactory: (config: ConfigService) => ({
      transport: Transport.TCP,
      options: {
        host: config.get('BILLING_HOST'),
        port: config.get<number>('BILLING_PORT'),
      },
    }),
  },
]);

Injecting and using the proxy

Inject the proxy with @Inject(<token>) and type it as ClientProxy. From there, send() and emit() are your two verbs.

// orders.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { firstValueFrom, Observable } from 'rxjs';

interface Invoice {
  id: string;
  amount: number;
  status: 'paid' | 'pending';
}

@Injectable()
export class OrdersService {
  constructor(
    @Inject('BILLING_SERVICE') private readonly billing: ClientProxy,
  ) {}

  // Request-response: expects a reply
  chargeOrder(orderId: string, amount: number): Observable<Invoice> {
    return this.billing.send<Invoice>({ cmd: 'charge' }, { orderId, amount });
  }

  // Fire-and-forget: no reply expected
  orderPlaced(orderId: string): void {
    this.billing.emit('order_placed', { orderId, at: Date.now() });
  }

  // Convert the Observable to a Promise when you prefer async/await
  async chargeAndWait(orderId: string, amount: number): Promise<Invoice> {
    return firstValueFrom(this.chargeOrder(orderId, amount));
  }
}

send() vs emit()

The choice between the two methods maps directly onto the messaging style you need. send() triggers a @MessagePattern handler and waits for a return value; emit() triggers a @EventPattern handler and returns immediately.

Aspectsend()emit()
Pattern handler@MessagePattern@EventPattern
SemanticsRequest-responseEvent (fire-and-forget)
Return valueObservable<TResult> of the replyObservable<void> (completes when published)
Back-pressure / reply channelYesNo
Use caseQuery data, RPC-style callsBroadcast that something happened

The Observable returned by send() is cold: the request is not dispatched until something subscribes. If you never subscribe (or never firstValueFrom/lastValueFrom it), the message is never sent.

Handling the Observable response

Because both methods return Observables, you get all of RxJS for free — timeouts, retries, mapping, and error recovery compose naturally.

import { catchError, map, timeout } from 'rxjs/operators';
import { of, throwError } from 'rxjs';

chargeWithGuards(orderId: string, amount: number): Observable<Invoice> {
  return this.billing.send<Invoice>({ cmd: 'charge' }, { orderId, amount }).pipe(
    timeout(5000),
    map((invoice) => ({ ...invoice, status: invoice.amount > 0 ? 'paid' : 'pending' })),
    catchError((err) => throwError(() => new Error(`Billing failed: ${err.message}`))),
  );
}

In a controller you can simply return the Observable — Nest subscribes for you and streams the resolved value to the HTTP client.

// orders.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { OrdersService } from './orders.service';

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

  @Post('charge')
  charge(@Body() body: { orderId: string; amount: number }) {
    return this.orders.chargeOrder(body.orderId, body.amount);
  }
}

Output:

POST /orders/charge  { "orderId": "ord_42", "amount": 1999 }

{ "id": "inv_8a1", "amount": 1999, "status": "paid" }

Connecting and disconnecting

ClientProxy opens the underlying connection lazily on the first message. You can warm it up explicitly with connect() — typically in onApplicationBootstrap — and release sockets in onModuleDestroy with close().

import { Injectable, OnApplicationBootstrap, OnModuleDestroy, Inject } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';

@Injectable()
export class BillingClient implements OnApplicationBootstrap, OnModuleDestroy {
  constructor(@Inject('BILLING_SERVICE') private readonly client: ClientProxy) {}

  async onApplicationBootstrap() {
    await this.client.connect();
  }

  async onModuleDestroy() {
    await this.client.close();
  }
}

The @Client decorator

As a lighter alternative to ClientsModule, the @Client() property decorator instantiates a proxy directly on a class field. It is convenient for quick setups but harder to configure asynchronously and to mock in tests, so ClientsModule is preferred for production code.

import { Controller } from '@nestjs/common';
import { Client, ClientProxy, Transport } from '@nestjs/microservices';

@Controller()
export class NotificationsController {
  @Client({ transport: Transport.TCP, options: { port: 3002 } })
  private readonly client: ClientProxy;

  notify(userId: string) {
    return this.client.emit('user_notified', { userId });
  }
}

Best Practices

  • Use ClientsModule.registerAsync() with ConfigService for environment-driven hosts, ports, and credentials instead of hardcoding them.
  • Reach for send() only when you need a reply; use emit() for state-changing notifications so callers are not blocked on a response.
  • Always subscribe to (or await firstValueFrom()) the Observable from send() — an unsubscribed cold Observable never dispatches the message.
  • Add timeout() and catchError() operators to remote calls so a slow or downed service cannot hang the caller indefinitely.
  • Prefer ClientsModule over @Client() so the proxy is a real DI token you can override and mock in unit tests.
  • Open connections eagerly in onApplicationBootstrap() and close them in onModuleDestroy() to avoid first-request latency and leaked sockets.
Last updated June 14, 2026
Was this helpful?