Skip to content
NestJS ns microservices 4 min read

API Gateway Pattern

In a microservice system, clients should not talk to each individual service directly. An API gateway is a single HTTP entry point that fronts your microservices: it exposes a clean REST surface, routes each request to the right downstream service over a transport like TCP or NATS, aggregates responses from several services into one payload, and centralizes cross-cutting concerns such as authentication, rate limiting, and logging. In NestJS the gateway is just a normal HTTP application that holds one or more ClientProxy instances and delegates work through them.

Why front services with a gateway

Without a gateway, every client must know the network location of every service, speak each service’s transport, and stitch together data itself. That couples clients to your internal topology and duplicates auth logic everywhere. A gateway solves this by acting as a Backend-for-Frontend (BFF): it presents one stable, HTTP-friendly contract while the messy microservice details stay hidden behind it.

ConcernWithout gatewayWith gateway
Client couplingKnows every service hostKnows one URL
TransportClient speaks TCP/NATS/gRPCGateway translates HTTP to internal transport
AuthRepeated per serviceCentralized at the edge
AggregationClient makes N callsGateway makes N calls, returns one
Cross-cuttingScatteredOne place for logging, rate limiting

Registering client proxies

The gateway is a standard HTTP Nest app. Register a ClientProxy for each downstream service with ClientsModule. Here we wire up a users service and an orders service over TCP.

// src/app.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { GatewayController } from './gateway.controller';

@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'USERS_SERVICE',
        transport: Transport.TCP,
        options: { host: '127.0.0.1', port: 3001 },
      },
      {
        name: 'ORDERS_SERVICE',
        transport: Transport.TCP,
        options: { host: '127.0.0.1', port: 3002 },
      },
    ]),
  ],
  controllers: [GatewayController],
})
export class AppModule {}

This is a plain HTTP application, so bootstrap it the usual way with NestFactory.create:

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
  console.log('API gateway listening on http://localhost:3000');
}
bootstrap();

Routing requests to services

The controller exposes REST endpoints and forwards each one to the right service via send (request-response) or emit (fire-and-forget). Because ClientProxy returns RxJS Observables, the simplest approach is to convert them to promises with firstValueFrom.

// src/gateway.controller.ts
import { Controller, Get, Post, Body, Param, Inject } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs';

@Controller()
export class GatewayController {
  constructor(
    @Inject('USERS_SERVICE') private readonly users: ClientProxy,
    @Inject('ORDERS_SERVICE') private readonly orders: ClientProxy,
  ) {}

  @Get('users/:id')
  getUser(@Param('id') id: string) {
    return this.users.send({ cmd: 'get_user' }, { id });
  }

  @Post('orders')
  createOrder(@Body() dto: { userId: string; items: string[] }) {
    return this.orders.send({ cmd: 'create_order' }, dto);
  }
}

Nest’s HTTP layer automatically subscribes to an Observable returned from a route handler, so returning this.users.send(...) directly works without firstValueFrom. Use firstValueFrom only when you need the resolved value inside the handler, for example to aggregate.

Aggregating multiple services

The real power of a gateway is composing data. Fetch a user and their orders in parallel, then return a single combined response. Running calls concurrently with Promise.all keeps latency close to the slowest call rather than the sum.

// src/gateway.controller.ts (excerpt)
@Get('users/:id/profile')
async getProfile(@Param('id') id: string) {
  const [user, orders] = await Promise.all([
    firstValueFrom(this.users.send({ cmd: 'get_user' }, { id })),
    firstValueFrom(this.orders.send({ cmd: 'list_orders' }, { userId: id })),
  ]);

  return {
    id: user.id,
    name: user.name,
    orderCount: orders.length,
    orders,
  };
}

Output:

$ curl http://localhost:3000/users/42/profile
{
  "id": "42",
  "name": "Ada Lovelace",
  "orderCount": 2,
  "orders": [
    { "id": "o-1", "items": ["book"] },
    { "id": "o-2", "items": ["pen", "ink"] }
  ]
}

Centralizing authentication

Because every request passes through the gateway, it is the natural place to authenticate once and forward an identity downstream. A guard validates the incoming token at the edge; services can then trust the gateway.

// src/auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();
    const token = req.headers['authorization']?.replace('Bearer ', '');
    if (!token || token !== process.env.GATEWAY_TOKEN) {
      throw new UnauthorizedException('Invalid or missing token');
    }
    req.user = { id: 'verified-from-token' };
    return true;
  }
}

Apply it globally so the whole gateway surface is protected, then pass the verified user to downstream calls in the message payload:

// src/main.ts (excerpt)
import { AuthGuard } from './auth.guard';

app.useGlobalGuards(new AuthGuard());

Best Practices

  • Run downstream calls in parallel with Promise.all whenever they are independent so gateway latency stays low.
  • Authenticate and rate-limit at the gateway edge; let internal services trust requests coming from it rather than re-validating everything.
  • Keep the gateway thin: route, aggregate, and translate, but push business logic into the services themselves.
  • Shape responses for the client (BFF style) so the public contract stays stable even when internal service schemas change.
  • Set sensible timeouts on ClientProxy calls and handle partial failures gracefully so one slow service does not hang the whole response.
  • Use clear, versioned message patterns like { cmd: 'get_user' } so routing intent is explicit and easy to evolve.
Last updated June 14, 2026
Was this helpful?