How to use feature flags in NestJS
NestJS has a strong opinion about architecture: modules, dependency injection, decorators. Your feature flag setup should follow the same patterns instead of fighting them.
This guide shows how to integrate Flagify into a NestJS application using @flagify/nestjs — a module that gives you injectable services, route guards, and parameter decorators out of the box.
Install
npm install @flagify/nestjs @flagify/node
Register the module
Add FlagifyModule to your root module. The simplest setup uses forRoot:
import { Module } from '@nestjs/common';
import { FlagifyModule } from '@flagify/nestjs';
@Module({
imports: [
FlagifyModule.forRoot({
projectKey: 'your-project-key',
publicKey: 'pk_dev_abc123_xxxxxxxx',
secretKey: 'sk_dev_abc123_xxxxxxxx',
options: {
realtime: true,
},
}),
],
})
export class AppModule {}
The module registers globally by default — every controller and service can inject FlagifyService without re-importing the module.
Using ConfigService
In production, pull keys from environment variables:
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { FlagifyModule } from '@flagify/nestjs';
@Module({
imports: [
ConfigModule.forRoot(),
FlagifyModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
projectKey: config.getOrThrow('FLAGIFY_PROJECT_KEY'),
publicKey: config.getOrThrow('FLAGIFY_PUBLIC_KEY'),
secretKey: config.get('FLAGIFY_SECRET_KEY'),
options: { realtime: true },
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
Evaluate flags in a service
Inject FlagifyService and call it like any other dependency:
import { Controller, Get } from '@nestjs/common';
import { FlagifyService } from '@flagify/nestjs';
@Controller('dashboard')
export class DashboardController {
constructor(private readonly flagify: FlagifyService) {}
@Get()
getDashboard() {
const showNewUI = this.flagify.isEnabled('new-dashboard');
const maxItems = this.flagify.getValue('dashboard-max-items', 10);
return { showNewUI, maxItems };
}
}
The three methods you will use most:
| Method | Returns | Use case |
|---|---|---|
isEnabled(key) | boolean | On/off toggles |
getValue(key, fallback) | T | Typed config values |
getVariant(key, fallback) | string | A/B test variants |
Always provide a fallback. If the flag does not exist or the client is still initializing, the fallback is returned.
Gate routes with guards
Instead of writing if statements in every handler, use @RequireFlag to gate entire routes:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { RequireFlag, FeatureFlagGuard } from '@flagify/nestjs';
@Controller('beta')
@UseGuards(FeatureFlagGuard)
export class BetaController {
@RequireFlag('beta-access')
@Get()
getBetaFeature() {
return { message: 'Welcome to the beta!' };
}
}
If the flag is disabled, the guard returns a 403 Forbidden. No conditional logic in the handler.
You can also match specific values:
@RequireFlag({ key: 'checkout-experiment', value: 'variant-a' })
@Get('checkout-v2')
getCheckoutV2() {
return { message: 'Variant A checkout' };
}
Inject flags as parameters
For simple cases, skip the service entirely and inject flag values directly into handler parameters:
import { Controller, Get } from '@nestjs/common';
import { FeatureFlag } from '@flagify/nestjs';
@Controller('products')
export class ProductsController {
@Get()
getProducts(
@FeatureFlag('show-recommendations') showRecs: boolean,
@FeatureFlag({ key: 'max-results', fallback: 20 }) maxResults: number,
) {
return { showRecs, maxResults };
}
}
This keeps handlers clean when all you need is a flag value.
User targeting
For percentage rollouts or attribute-based targeting, you need user context. Configure an identify callback when registering the module:
FlagifyModule.forRoot({
projectKey: 'your-project-key',
publicKey: 'pk_dev_abc123_xxxxxxxx',
secretKey: 'sk_dev_abc123_xxxxxxxx',
identify: (ctx) => {
const request = ctx.switchToHttp().getRequest();
return {
id: request.user?.id ?? 'anonymous',
email: request.user?.email,
role: request.user?.role,
};
},
});
Now the FeatureFlagGuard automatically extracts the user from each request and evaluates flags with targeting rules. No extra code in your controllers.
For manual evaluation with user context, use the evaluate method:
@Get()
async getFeatures(@Req() req: Request) {
const result = await this.flagify.evaluate('premium-features', {
id: req['user'].id,
email: req['user'].email,
});
return { enabled: result.value, reason: result.reason };
}
Testing
FlagifyTestingModule lets you test controllers without network calls:
import { Test } from '@nestjs/testing';
import { FlagifyTestingModule } from '@flagify/nestjs/testing';
import { DashboardController } from './dashboard.controller';
describe('DashboardController', () => {
it('returns new UI when flag is enabled', async () => {
const module = await Test.createTestingModule({
imports: [
FlagifyTestingModule.withFlags({
'new-dashboard': true,
'dashboard-max-items': 25,
}),
],
controllers: [DashboardController],
}).compile();
const controller = module.get(DashboardController);
const result = controller.getDashboard();
expect(result.showNewUI).toBe(true);
expect(result.maxItems).toBe(25);
});
});
You can also update flags mid-test:
const service = module.get(FlagifyService) as any;
service.setFlag('new-dashboard', false);
Real-time updates
With realtime: true, the SDK connects via SSE and flag changes are reflected immediately — no restart needed. The connection uses exponential backoff (1s to 30s) for reconnection.
Without realtime, flags are fetched once at startup and cached. Use staleTimeMs to control how long cached values are considered fresh.
Summary
| Feature | How |
|---|---|
| Evaluate flags | Inject FlagifyService |
| Gate routes | @RequireFlag + FeatureFlagGuard |
| Inject values | @FeatureFlag parameter decorator |
| User targeting | identify callback or evaluate() method |
| Testing | FlagifyTestingModule.withFlags() |
The NestJS SDK follows the framework’s conventions. Flags are injectable, testable, and declarative. No workarounds, no global singletons.
Read the full NestJS SDK reference for all options.