typescript 82 lines · 4 tabs

Bind Tenant Context Per Request in NestJS With a Custom @TenantId() Decorator

Shared by codesnips Jul 2026
4 tabs
import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { TenantService } from './tenant.service';

@Injectable()
export class TenantContextMiddleware implements NestMiddleware {
  constructor(private readonly tenants: TenantService) {}

  async use(req: Request, _res: Response, next: NextFunction): Promise<void> {
    const tenantId = req.header('x-tenant-id');
    if (!tenantId) {
      throw new UnauthorizedException('Missing x-tenant-id header');
    }

    const tenant = await this.tenants.findById(tenantId);
    if (!tenant) {
      throw new UnauthorizedException('Unknown tenant');
    }

    // Shared contract: everything downstream reads req.tenant
    (req as Request & { tenant: unknown }).tenant = tenant;
    next();
  }
}
4 files · typescript Explain with highlit

Multi-tenant SaaS backends need a reliable way to know which tenant is making each request, and to make that identity available everywhere without threading it through method arguments by hand. This snippet shows the idiomatic NestJS approach: resolve the tenant once at the edge of the request, stash it on the request object, then expose it through a small custom parameter decorator so controllers stay clean.

In TenantContextMiddleware, tenant resolution happens exactly once per request. The middleware reads the x-tenant-id header (a subdomain or JWT claim would work the same way), validates it against the TenantService, and attaches the resolved record to req.tenant. Rejecting unknown tenants here — with an UnauthorizedException — means downstream code can assume a valid tenant exists. Middleware is chosen over a guard for the resolution step because it runs before route matching and can populate the request for every consumer, including other guards.

TenantGuard demonstrates a second, complementary use of the same context. It reads request.tenant and enforces that the tenant is active, returning false (which NestJS turns into a 403) for suspended accounts. Because the middleware ran first, the guard never has to repeat the lookup — it only makes an authorization decision.

The centerpiece is TenantId decorator, built with createParamDecorator. The factory receives the ExecutionContext, switches to the HTTP layer via ctx.switchToHttp(), and pulls the tenant off the request. The optional data argument lets callers request a single field, so @TenantId() yields the whole object while @TenantId('id') yields just the id — a common ergonomic touch. Because a ParamDecorator runs inside the DI-aware request pipeline, it can extract anything the ExecutionContext exposes.

Finally, ReportsController ties it together: @UseGuards(TenantGuard) protects the route, and the handler simply declares @TenantId('id') tenantId: string. There is no manual header parsing, no repeated validation, and the controller reads as if the tenant were a first-class request parameter. The trade-off is that this pattern relies on request-shape conventions (req.tenant) shared between the middleware, guard, and decorator; documenting that contract matters. It also keeps services stateless rather than using request-scoped providers, which avoids the performance cost of per-request instantiation.


Related snips

Share this code

Here's the card — post it anywhere.

Bind Tenant Context Per Request in NestJS With a Custom @TenantId() Decorator — share card
Link copied