An API gateway is the single entry point for all client requests to your microservices backend. Instead of clients knowing about ten different service endpoints, they talk to one gateway that routes requests, enforces authentication, applies rate limits, and aggregates responses. In a NestJS microservices architecture, the gateway pattern is essential for maintaining clean service boundaries while presenting a unified API to front-end clients. I've implemented this pattern for projects where a React front-end needs to call multiple backend services without managing their individual endpoints.
The API gateway serves as a reverse proxy with intelligence. Its responsibilities: (1) Request routing — match incoming HTTP paths to the appropriate downstream microservice; (2) Authentication and authorization — validate JWTs or API keys before requests reach services; (3) Rate limiting — enforce per-client or per-endpoint quotas; (4) Request/response transformation — adapt payload formats between client and service contracts; (5) Circuit breaking — stop sending requests to failing services; (6) Response aggregation — combine responses from multiple services into one payload for the client.
In NestJS, the gateway can be a dedicated application that acts as an HTTP proxy. Use @nestjs/axios to forward requests to downstream services, implement Guards for authentication at the gateway level, and use Interceptors for rate limiting and response transformation. The gateway communicates with services via gRPC or HTTP — gRPC gives you typed contracts and better performance for internal traffic. For the external API surface, the gateway exposes REST (or GraphQL) regardless of what protocol the services use internally.
API Gateway Architecture (NestJS)
┌────────────────────────────────────────────────────┐
│ Clients │
│ Browser │ Mobile App │ Partner API │ CLI │
└──────────────────┬─────────────────────────────────┘
│ HTTPS (REST / GraphQL)
▼
┌────────────────────────────────────────────────────┐
│ API Gateway (NestJS) │
│ │
│ [AuthGuard] → JWT validate → inject X-User-Id │
│ [RateLimitInterceptor] → Redis token bucket │
│ [TracingInterceptor] → inject X-Request-ID │
│ [CircuitBreakerInterceptor] → fail fast │
│ │
│ Route Table: │
│ /api/orders/* → OrderService (gRPC :50051) │
│ /api/invoices/* → InvoiceService (gRPC :50052) │
│ /api/reports/* → ReportService (HTTP :3003) │
└──────────┬────────────────┬───────────────────────┘
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ Order │ │ Invoice │
│ Service │ │ Service │
│ (NestJS) │ │ (NestJS) │
└─────────────┘ └─────────────┘
BFF Extension: /api/dashboard → parallel calls to
UserService + OrderService + NotificationService → merged responseFrom my experience building the front-end for Commsult's ERP: implement request tracing at the gateway layer by injecting an X-Request-ID header into every forwarded request. Log this ID at both the gateway and each downstream service. When debugging a production issue, you can reconstruct the full request journey across all services by searching your logs for that single ID. Without this, distributed debugging is nearly impossible.
Centralizing authentication at the gateway is one of the biggest benefits of the pattern. Instead of every microservice implementing JWT validation independently, the gateway validates the token and passes the decoded claims downstream as headers (X-User-Id, X-User-Roles). Downstream services trust these headers without re-validating the JWT — they only need to verify the request came from the gateway (use mTLS or a shared internal secret for this). This separation means adding a new microservice doesn't require duplicating auth logic.
// gateway-auth.guard.ts
@Injectable()
export class GatewayAuthGuard implements CanActivate {
constructor(private jwt: JwtService) {}
canActivate(ctx: ExecutionContext): boolean {
const req = ctx.switchToHttp().getRequest();
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) throw new UnauthorizedException();
const payload = this.jwt.verify(token);
// Downstream services trust these headers — never re-validate JWT
req.headers['x-user-id'] = payload.sub;
req.headers['x-user-roles'] = payload.roles.join(',');
req.headers['x-request-id'] = randomUUID(); // inject trace ID
return true;
}
}
// dashboard.controller.ts — BFF aggregation
@Controller('dashboard')
export class DashboardController {
@Get('overview')
async getOverview(@Headers('x-user-id') userId: string) {
// Parallel calls — total latency = max(latencies), not sum
const [profile, orders, notifications] = await Promise.all([
this.userService.getProfile(userId),
this.orderService.getRecent(userId, { limit: 5 }),
this.notificationService.getUnread(userId),
]);
return { profile, orders, notifications };
}
}Rate limiting protects your services from abuse and ensures fair usage. Implement rate limiting at the gateway using a token bucket or sliding window algorithm backed by Redis — so limits are shared across multiple gateway instances. Common strategies: per-IP rate limiting for unauthenticated endpoints, per-user rate limiting for authenticated API calls, and per-endpoint burst limits for expensive operations. In NestJS, the @nestjs/throttler package handles this with Redis support via ThrottlerStorageRedisService.
Centralizing all traffic through the gateway means it must be your most reliable component. A gateway that goes down takes your entire API surface with it. Run multiple gateway instances behind a load balancer. Implement health checks on the gateway and circuit breakers on downstream service calls. The gateway should fail gracefully — return cached responses or meaningful error messages rather than hanging. Monitor gateway latency and error rates as your primary SLO: if your gateway's P99 latency spikes, every user feels it.
For teams already invested in NestJS, building a gateway in NestJS gives full control and reuses existing patterns. But purpose-built gateway products like Kong, Traefik, or AWS API Gateway handle rate limiting, auth, routing, caching, and analytics out of the box. Use a purpose-built gateway when: you're in a polyglot environment; you need enterprise features like OAuth2 flows, API key management, or analytics dashboards without building them. Build in NestJS when: your team is NestJS-native, your requirements are well-defined and unlikely to grow complex, and you want to avoid licensing costs.
The Backend-for-Frontend (BFF) pattern extends the gateway by adding response aggregation — the gateway calls multiple services in parallel and merges the results into a single response for the client. For example, a dashboard page that needs user profile, recent orders, and notification count can get all three in one API call, with the gateway making three parallel calls internally. This reduces client-side waterfall requests and simplifies front-end code. In NestJS, implement this with Promise.all() for parallel service calls and merge the results before returning.
Before going live: implement health check endpoints on all downstream services and configure the gateway to check them; set timeout limits on all proxied requests (never let a slow service block a gateway thread indefinitely); add circuit breakers with a defined fallback response; enable distributed tracing with request ID propagation; monitor gateway memory and CPU — high-traffic gateways can be CPU-intensive due to JWT verification on every request; deploy multiple gateway instances and test failover. These aren't optional — they're the difference between a gateway that improves reliability and one that becomes a fragility point.