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();
}
}
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
import { Tenant } from './tenant.entity';
@Injectable()
export class TenantGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const tenant: Tenant | undefined = request.tenant;
if (!tenant) {
throw new ForbiddenException('Tenant context not resolved');
}
if (tenant.status !== 'active') {
throw new ForbiddenException(`Tenant ${tenant.id} is ${tenant.status}`);
}
return true;
}
}
import { createParamDecorator, ExecutionContext, InternalServerErrorException } from '@nestjs/common';
import { Tenant } from './tenant.entity';
export const TenantId = createParamDecorator(
(data: keyof Tenant | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const tenant: Tenant | undefined = request.tenant;
if (!tenant) {
throw new InternalServerErrorException(
'TenantId used without TenantContextMiddleware',
);
}
return data ? tenant[data] : tenant;
},
);
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { TenantGuard } from './tenant.guard';
import { TenantId } from './tenant-id.decorator';
import { Tenant } from './tenant.entity';
import { ReportsService } from './reports.service';
@Controller('reports')
@UseGuards(TenantGuard)
export class ReportsController {
constructor(private readonly reports: ReportsService) {}
@Get('summary')
getSummary(@TenantId('id') tenantId: string, @Query('range') range = '30d') {
return this.reports.summarize(tenantId, range);
}
@Get('settings')
getSettings(@TenantId() tenant: Tenant) {
return { plan: tenant.plan, timezone: tenant.timezone };
}
}
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
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
package com.example.myapp
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
Dependency injection with Hilt
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
raw_token = SecureRandom.urlsafe_base64(32)
token_digest = Digest::SHA256.hexdigest(raw_token)
PasswordReset.create!(
user: user,
token_digest: token_digest,
Secure random token generation for sessions and recovery flows
Protocol 2
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AllowUsers deploy ops
SSH daemon hardening and key based access only
Share this code
Here's the card — post it anywhere.