typescript 112 lines · 4 tabs

Per-Request Feature Flags in NestJS with a Custom Decorator and Resolution Guard

Shared by codesnips Aug 2026
4 tabs
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { FeatureFlagGuard } from './feature-flag.guard';
import { RequireFeature } from './feature-flag.decorator';

@Controller('experiments')
@UseGuards(FeatureFlagGuard)
export class ExperimentsController {
  @Post('checkout')
  @RequireFeature('new-checkout')
  checkout(@Body() body: { cartId: string }) {
    return { ok: true, cartId: body.cartId, flow: 'v2' };
  }

  @Get('search')
  @RequireFeature('beta-search')
  search() {
    return { results: [], engine: 'beta' };
  }
}
4 files · typescript Explain with highlit

This snippet shows how per-request feature flagging is layered onto NestJS route handlers using the framework's metadata + guard machinery, so a controller method can declaratively require a flag without any imperative if checks in the handler body.

In feature-flag.decorator.ts, RequireFeature is a thin wrapper over SetMetadata that attaches the flag key (and an optional redirectOnDisabled behaviour) under a private FEATURE_FLAG_KEY symbol. Using a symbol rather than a string key avoids collisions with other metadata and keeps the contract internal to this module. Storing structured options instead of a bare string lets the guard vary its response per route — some flags 404 when off, others simply hide a response field. This is the classic NestJS pattern: decorators are pure metadata, and the actual logic lives in a guard that reads it back with Reflector.

feature-flag.service.ts is where resolution actually happens. isEnabled takes the flag key plus a small FlagContext derived from the request (user id, tenant, and any explicit override header) and computes a boolean. It supports three signals in priority order: an explicit per-request override, a percentage rollout hashed deterministically off the user id, and a static default. The deterministic hash matters — the same user always lands in or out of a bucket, so the experience is stable across requests rather than flickering. The service is a normal injectable, making it trivial to swap for a LaunchDarkly-backed implementation later.

feature-flag.guard.ts ties it together. canActivate uses reflector.getAllAndOverride to read the flag metadata from both handler and controller level, letting a flag be declared once for a whole controller and overridden per method. When no metadata is present the guard returns true and stays out of the way. Otherwise it builds a FlagContext from the request, asks the service, and either allows the call or throws NotFoundException — a deliberate choice so disabled features are indistinguishable from nonexistent routes.

experiments.controller.ts demonstrates usage: @UseGuards(FeatureFlagGuard) wires the guard in, and @RequireFeature('new-checkout') gates a single endpoint. The trade-off is that flag evaluation runs on every guarded request, so the resolution logic must stay cheap and side-effect free; heavier providers should cache. This approach is worth reaching for when flags need to gate whole endpoints cleanly, keeping rollout policy out of business logic.


Related snips

Share this code

Here's the card — post it anywhere.

Per-Request Feature Flags in NestJS with a Custom Decorator and Resolution Guard — share card
Link copied