Skip to content

Feature flags

What are feature flags?

A great explanation can be found in this article. Really worth a read!

What are providers?

A provider is the external service, library, or system responsible for managing and evaluating the state of all feature flags in an application.

You provide evaluation context to this provider to determine the state of a feature flag. This evaluation can, for example, contain the uuid or countryName of the user.

We make use of service called FeatBit, since it's open source and includes A/B Testing, Canary Launches & analytics.

Using feature flags

Adding a new feature flag

  1. Define the feature flag in the FeatureFlag enum.
typescript
export enum FeatureFlag {
  SOME_FEATURE = "some-feature",
  ANOTHER_FEATURE = "another-feature",
}
  1. Define the type of the feature flag in the FeatureFlagTypes interface.
typescript
export interface FeatureFlagTypes {
  [FeatureFlag.SOME_FEATURE]: boolean;
  [FeatureFlag.ANOTHER_FEATURE]: string;
}
  1. Define the default value in the defaultFeatureFlagValues map.
typescript
const defaultFeatureFlagValues: { [K in FeatureFlag]: FeatureFlagTypes[K] } = {
  [FeatureFlag.SOME_FEATURE]: false,
  [FeatureFlag.ANOTHER_FEATURE]: "a",
};
  1. Create the feature flag in FeatBit dashboard. Create Feature Flag in FeatBit

Evaluating

Restricting endpoints

When you want to restrict an endpoint based on a feature flag, you can use the @FeatureFlags() decorator.

WARNING

You can only use boolean feature flags with this decorator.

typescript
import { FeatureFlags } from "#src/modules/feature-flags/feature-flag.decorator.js";
import { FeatureFlag } from "#src/modules/feature-flags/feature-flag.enum.js";

export class SomeFeatureController {
  @FeatureFlags(FeatureFlag.SOME_FEATURE)
  async execute(input: string): Promise<void> {
    // Your logic here
  }
}

Programmatically

If you want to evaluate feature flags programmatically.

typescript
import { Injectable } from "@nestjs/common";
import { EvaluateFeatureFlagUseCase } from "#src/modules/feature-flag/use-cases/evaluate-feature-flag/evaluate-feature-flag.use-case.js";

@Injectable()
export class SomeFeatureUseCase {
  constructor(
    private readonly evaluateFeatureFlag: EvaluateFeatureFlagUseCase
  ) {}

  public async execute() {
    const isFeatureEnabled = await this.evaluateFeatureFlag.execute(
      FeatureFlag.SOME_FEATURE
    );

    // Your logic here
  }
}

Provider factory

In order to differentiate between multiple providers (e.g. email clients) based on the feature flag value, you can create a factory that will return the correct provider instance.

WARNING

Make sure you set the scope to Scope.REQUEST so that the provider will be instantiated each request, as the evaluation context is request specific.

typescript
@Module({
  providers: [
    {
      provide: MailClient,
      useFactory: mailClientFactory,
      inject: [ConfigService, EvaluateFeatureFlagUseCase],
      scope: Scope.REQUEST,
    },
    MailService,
  ],
  exports: [MailService],
})
export class MailModule {}
typescript
import { ConfigService } from "@nestjs/config";
import { MailClient } from "#src/modules/mail/clients/mail.client.js";
import { EvaluateFeatureFlagUseCase } from "#src/modules/feature-flag/use-cases/evaluate-feature-flag/evaluate-feature-flag.use-case.js";

export async function mailClientFactory(
  configService: ConfigService,
  evaluateFeatureFlag: EvaluateFeatureFlagUseCase
): Promise<MailClient> {
  const flag = await evaluateFeatureFlag.getStringValue(
    FeatureFlag.MAIL_PROVIDER
  );

  switch (flag) {
    case MailProvider.SCALEWAY:
      return new ScalewayMailClient(configService);
    case MailProvider.SEND_GRID:
      return new SendGridMailClient(configService);
    default:
      exhaustiveCheck(flag);
  }
}

Testing

When creating the testing module, we set the defaultProvider to use an InMemoryProvider (featureFlagsTestProvider), which allows us to mock the feature flag evaluations in our tests.

To mock a feature flag evaluation, you can do the following:

typescript
mock.method(InMemoryProvider.prototype, "resolveBooleanEvaluation", async () =>
  Promise.resolve(false)
);