
Feature Flags with One Decorator: Introducing @hazeljs/feature-toggle
Introducing @hazeljs/feature-toggle — decorator-first feature flags for HazelJS. Protect routes with @FeatureToggle, branch in code with FeatureToggleService, and seed from env. No external SDK.
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.
@Controller('checkout')
export class CheckoutController {
@Get('new')
@FeatureToggle('newCheckout')
getNewCheckout() {
return { flow: 'new' };
}
@Get('legacy')
getLegacyCheckout() {
return { flow: 'legacy' };
}
}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' };
}
}FeatureToggleService — branch in code
Inject FeatureToggleService and call isEnabled('flagName'), get('flagName'), or set('flagName', value). Unset flags are treated as off.
@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);
}
}Module options — initial flags and env prefix
Register the module with FeatureToggleModule.forRoot({ ... }):
- initialFlags —
Record<string, boolean>to set on startup. - envPrefix — Read from
process.env: e.g.envPrefix: 'FEATURE_'turnsFEATURE_NEW_UIinto the flagnewUi. Valuestrue,1,yes→ true.
FEATURE_NEW_CHECKOUT=true
FEATURE_BETA_API=0Quick start
1. Install and register the module
npm install @hazeljs/feature-toggleimport { HazelModule } from '@hazeljs/core';
import { FeatureToggleModule } from '@hazeljs/feature-toggle';
@HazelModule({
imports: [
FeatureToggleModule.forRoot({
initialFlags: { newCheckout: true },
envPrefix: 'FEATURE_',
}),
],
})
export class AppModule {}2. Protect routes with the decorator — add @FeatureToggle('newCheckout') on the route or controller.
3. Use the service where you need to branch — inject FeatureToggleService and call isEnabled('newCheckout').
Design choices
- In-memory by default — No database or external service. Seed from env for consistency.
- No provider lock-in — Add a custom provider later if you need one.
- Guard per flag — The decorator composes with HazelJS guards; we 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. - A/B or kill switches — Branch in services with
isEnabled()and flip flags via env or runtimeset(). - Docs — See 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, we can extend the API with a provider interface in a future release.