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.
| Aspect | send() | emit() |
|---|---|---|
| Pattern handler | @MessagePattern | @EventPattern |
| Semantics | Request-response | Event (fire-and-forget) |
| Return value | Observable<TResult> of the reply | Observable<void> (completes when published) |
| Back-pressure / reply channel | Yes | No |
| Use case | Query data, RPC-style calls | Broadcast that something happened |
The
Observablereturned bysend()is cold: the request is not dispatched until something subscribes. If you never subscribe (or neverfirstValueFrom/lastValueFromit), 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()withConfigServicefor environment-driven hosts, ports, and credentials instead of hardcoding them. - Reach for
send()only when you need a reply; useemit()for state-changing notifications so callers are not blocked on a response. - Always subscribe to (or
await firstValueFrom()) the Observable fromsend()— an unsubscribed cold Observable never dispatches the message. - Add
timeout()andcatchError()operators to remote calls so a slow or downed service cannot hang the caller indefinitely. - Prefer
ClientsModuleover@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 inonModuleDestroy()to avoid first-request latency and leaked sockets.