Feature flags in Nest.js

Feature flags in Nest.js
Photo by Dillon Shook / Unsplash

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