Feature flags in Nest.js
It's been a while since my last post, I've decided to switch the language to English to have a wider audience. Having said that, let's move into Feature Flags!
Let's imagine we have a system with the following capabilities
- Login & Signup
- Dashboard with metrics
- Marketplace
- List products
- Buy product
- Rate an order
Now suppose that the Marketplace feature was just launched but we don't want users accessing it yet because we forgot to implement the "Rate an order" functionality. It'd be nice if we could have a boolean flag for each feature where we can tell the system if it's enabled or not. That's a feature flag, but how can we implement it on Nest.js? Let's see about that
Implementation
The way I use to implement this in every project is fairly simple:
- A decorator that tells to which feature flag the controller belongs to
- A guard (or middleware) that ensures that the given route has it's feature flag enabled
import { SetMetadata } from '@nestjs/common'
export const FEATURE_FLAG = 'FEATURE_FLAG'
export const FeatureFlag = (name: string) => SetMetadata(FEATURE_FLAG, name)
feature-flag.decorator.ts
Next we need a guard that checks the metadata added by the previous @FeatureFlag()
decorator
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException
} from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { FEATURE_FLAG } from './feature-flag.decorator'
class FeatureDisabledException extends UnauthorizedException {
constructor() {
super('Feature is not enabled')
}
}
@Injectable()
export class FeatureFlagGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
public canActivate(context: ExecutionContext): boolean {
const featureName = this.reflector.get<string>(FEATURE_FLAG, context.getClass())
// if not specified, we allow by default
if (!featureName) return true
const canActivate = this.isFeatureEnabled(featureName) ?? false
if (!canActivate) {
throw new FeatureDisabledException()
}
return canActivate
}
private isFeatureEnabled(name: string): boolean {
// Impl to retrieve current enabled feature flags
}
}
feature-flag.guard.ts
I'm gonna leave the isFeatureEnabled
implementation up to you, you can store the list of enabled/disabled feature flags on a json file and put that on a S3 Bucket, or your could store it on a document in dynamoDB. For instance, my current project is a multi-tenant system, so each tenant has its own feature flags stored on a json
column in a postgres database, which looks something like this:
{
"SIGN_UP": true,
"SIGN_IN": true,
"MARKETPLACE": false
}
feature-flag.json
We need to tell nestjs, that we want to use this guard for every request, for that we are going to globally register it on the main.ts
import { NestFactory, Reflector } from '@nestjs/core'
import { AppModule } from './app.module'
import { FeatureFlagGuard } from './modules/security/authorization/feature-flag.guard'
async function bootstrap(): Promise<any> {
const app = await NestFactory.create(AppModule, {
logger: ['debug']
})
const reflector = app.get(Reflector)
app.useGlobalGuards(new FeatureFlagGuard(reflector))
await app.listen(5000)
}
bootstrap()
main.ts
Note that we are providing the instance of the FeatureFlagGuard
hence we need to pass its dependencies manually. The reflector, which is the object that we use to get the metadata set by the decorators, can be obtained from the app
object, it's automatically populated by the framework.
Usage
Now let's update our MarketplaceController
so that it's linked to a specific Feature Flag
import { Controller, Get, Post } from '@nestjs/common'
import { FeatureFlag } from '../security/authorization/feature-flag.decorator'
@Controller('user/marketplace')
@FeatureFlag('MARKETPLACE')
export class MarketplaceController {
constructor(private readonly service: Service) {}
// routes
@Post('buy')
public async buy(): Promise<any> {}
@Get('items')
public async listItems(): Promise<any> {}
}
marketplace.controller.ts
And we are done! Let's go through the whole flow
- A request
GET /user/marketplace/items
arrives - The guard
FeatureFlagGuard
checks if the controller has a featureFlag set. If not, it allows the request - If the controller has a feature flag linked, it will check if it's enabled, if it is, it allows the request, otherwise it throws
FeatureDisabledException
- If allowed, it reaches the controller
MarketplaceController
Conclusion
As you can see it's pretty simple to implement, and the benefit it provides are huge; Someone found a bug or an exploit on production? No worries, just disable that feature and fix it while preventing it's usage. Personally I use this on all of my projects. Thanks for reading! :D