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' };
}
}
import { SetMetadata } from '@nestjs/common';
export const FEATURE_FLAG_KEY = Symbol('feature-flag');
export interface FeatureFlagOptions {
key: string;
redirectOnDisabled?: boolean;
}
export const RequireFeature = (
key: string,
options: Partial<Omit<FeatureFlagOptions, 'key'>> = {},
) => SetMetadata(FEATURE_FLAG_KEY, { key, ...options } as FeatureFlagOptions);
import { Injectable } from '@nestjs/common';
import { createHash } from 'crypto';
export interface FlagContext {
userId?: string;
tenantId?: string;
override?: boolean;
}
interface FlagRule {
defaultOn: boolean;
rolloutPercent?: number;
}
@Injectable()
export class FeatureFlagService {
private readonly rules: Record<string, FlagRule> = {
'new-checkout': { defaultOn: false, rolloutPercent: 25 },
'beta-search': { defaultOn: true },
};
isEnabled(flag: string, ctx: FlagContext): boolean {
const rule = this.rules[flag];
if (!rule) return false;
if (typeof ctx.override === 'boolean') return ctx.override;
if (rule.rolloutPercent != null && ctx.userId) {
return this.bucket(flag, ctx.userId) < rule.rolloutPercent;
}
return rule.defaultOn;
}
private bucket(flag: string, userId: string): number {
const digest = createHash('sha256').update(`${flag}:${userId}`).digest();
return digest.readUInt32BE(0) % 100;
}
}
import {
CanActivate,
ExecutionContext,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { FEATURE_FLAG_KEY, FeatureFlagOptions } from './feature-flag.decorator';
import { FeatureFlagService, FlagContext } from './feature-flag.service';
@Injectable()
export class FeatureFlagGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly flags: FeatureFlagService,
) {}
canActivate(context: ExecutionContext): boolean {
const meta = this.reflector.getAllAndOverride<FeatureFlagOptions>(
FEATURE_FLAG_KEY,
[context.getHandler(), context.getClass()],
);
if (!meta) return true;
const req = context.switchToHttp().getRequest();
const ctx: FlagContext = {
userId: req.user?.id,
tenantId: req.user?.tenantId,
override: this.parseOverride(req.headers['x-feature-override']),
};
if (this.flags.isEnabled(meta.key, ctx)) return true;
throw new NotFoundException();
}
private parseOverride(header?: string): boolean | undefined {
if (header === 'on') return true;
if (header === 'off') return false;
return undefined;
}
}
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
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
class AddSettingsToAccounts < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_column :accounts, :settings, :jsonb, null: false, default: {}
Postgres JSONB Partial Index for Feature Flags
<%# private stream: turbo signs the serialized record name %>
<%= turbo_stream_from current_user %>
<section class="notifications">
<h1>Notifications</h1>
Turbo Streams + authorization: signed per-user stream name
class FeatureFlag < ApplicationRecord
validates :key, presence: true, uniqueness: true
validates :percentage, inclusion: { in: 0..100 }
after_commit :expire_cache
Safer Feature Flagging: Cache + DB Fallback
class Document < ApplicationRecord
belongs_to :owner, class_name: "User"
has_many :visibilities, class_name: "DocumentVisibility", dependent: :delete_all
scope :public_documents, -> { where(is_public: true) }
Polymorphic “Visible To” Scope with Arel
Share this code
Here's the card — post it anywhere.