DEV Community

Cover image for Feature Flags with One Decorator: Introducing @hazeljs/feature-toggle
Muhammad Arslan
Muhammad Arslan

Posted on

Feature Flags with One Decorator: Introducing @hazeljs/feature-toggle

We’re shipping @hazeljs/feature-toggle — a small, decorator-first package for feature flags in HazelJS. Protect routes when a flag is off, branch in code with FeatureToggleService, and optionally seed flags from environment variables. No external SDK; just core and one decorator.


Why feature flags?

You need to ship code behind a switch: roll out a new checkout flow, hide a beta API until it’s ready, or run two code paths for A/B testing. Doing this with if (process.env.FEATURE_X) and manual checks in every route gets messy. You want:

  • Route-level control — “This endpoint is only available when the flag is on.”
  • Programmatic checks — “In this service, call the new flow if the flag is on.”
  • One place to configure — Env vars or a simple in-memory store, without a separate SaaS on day one.

@hazeljs/feature-toggle gives you exactly that: a single decorator for routes and an injectable service for everything else.


What’s in the box

@FeatureToggle('name') — one decorator, no boilerplate

Put @FeatureToggle('newCheckout') on a controller or a route method. When the flag is disabled, the request is rejected with 403 and your handler never runs. When it’s enabled, the request proceeds as usual. No need to wire @UseGuards yourself; the package composes with HazelJS guards under the hood.

@Controller('checkout')
export class CheckoutController {
  @Get('new')
  @FeatureToggle('newCheckout')
  getNewCheckout() {
    return { flow: 'new' };
  }

  @Get('legacy')
  getLegacyCheckout() {
    return { flow: 'legacy' };
  }
}
Enter fullscreen mode Exit fullscreen mode

Use it on the class to require the flag for every route on that controller:

@Controller('beta')
@FeatureToggle('betaApi')
export class BetaController {
  @Get()
  index() {
    return { message: 'Beta API' };
  }
}
Enter fullscreen mode Exit fullscreen mode

FeatureToggleService — branch in code

Inject FeatureToggleService and call isEnabled('flagName'), get('flagName'), or set('flagName', value). Unset flags are treated as off. Use this in services, pipelines, or any non-route code.

@Service()
export class OrderService {
  constructor(private readonly featureToggle: FeatureToggleService) {}

  createOrder(data: OrderData) {
    if (this.featureToggle.isEnabled('newCheckout')) {
      return this.createWithNewFlow(data);
    }
    return this.createWithLegacyFlow(data);
  }
}
Enter fullscreen mode Exit fullscreen mode

Module options — initial flags and env prefix

Register the module with FeatureToggleModule.forRoot({ ... }):

  • initialFlagsRecord<string, boolean> to set on startup.
  • envPrefix — Read from process.env: any variable like PREFIX_NAME becomes a flag. For example, envPrefix: 'FEATURE_' turns FEATURE_NEW_UI into the flag newUi (camelCase). Values true, 1, yes (case-insensitive) are treated as true; otherwise false.

So you can flip flags per environment without code changes:

FEATURE_NEW_CHECKOUT=true
FEATURE_BETA_API=0
Enter fullscreen mode Exit fullscreen mode

Quick start

1. Install and register the module

npm install @hazeljs/feature-toggle
Enter fullscreen mode Exit fullscreen mode
import { HazelModule } from '@hazeljs/core';
import { FeatureToggleModule } from '@hazeljs/feature-toggle';

@HazelModule({
  imports: [
    FeatureToggleModule.forRoot({
      initialFlags: { newCheckout: true },
      envPrefix: 'FEATURE_',
    }),
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

2. Protect routes with the decorator

import { Controller, Get } from '@hazeljs/core';
import { FeatureToggle } from '@hazeljs/feature-toggle';

@Controller('checkout')
export class CheckoutController {
  @Get('new')
  @FeatureToggle('newCheckout')
  getNewCheckout() {
    return { flow: 'new' };
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Use the service where you need to branch

constructor(private readonly featureToggle: FeatureToggleService) {}
if (this.featureToggle.isEnabled('newCheckout')) { ... }
Enter fullscreen mode Exit fullscreen mode

Design choices

  • In-memory by default — No database or external service. You can seed from env so flags are consistent across restarts and environments.
  • No provider lock-in — The package doesn’t depend on LaunchDarkly or similar. You can add a custom provider later if you need one.
  • Guard per flag — The decorator creates a small guard class per feature name (cached). The guard injects FeatureToggleService and returns isEnabled(name). If you’re used to HazelJS guards, it’s the same pipeline; we just hide the boilerplate behind @FeatureToggle.

When to use it

  • Rollouts — Expose a new API or flow only when the flag is on.
  • Beta endpoints — Put @FeatureToggle('betaApi') on a controller and enable it only in staging or for internal users.
  • A/B or kill switches — Branch in services with isEnabled() and flip flags via env or runtime set().
  • Docs and landing — Link to the Feature Toggle package docs and the package README for full API and examples.

What’s next

We’re keeping the first version minimal: in-memory store, optional env seeding, and the decorator. If you need persistence, percentage rollouts, or targeting (e.g. by user or tenant), we can extend the API with a provider interface in a future release. For now, you get a simple, dependency-light way to gate routes and branch in code — with one decorator and one service.

Try it and tell us what you’d add: GitHub, Discord, or npm.

Top comments (0)