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' };
}
}
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. 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);
}
}
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: any variable likePREFIX_NAMEbecomes a flag. For example,envPrefix: 'FEATURE_'turnsFEATURE_NEW_UIinto the flagnewUi(camelCase). Valuestrue,1,yes(case-insensitive) are treated astrue; otherwisefalse.
So you can flip flags per environment without code changes:
FEATURE_NEW_CHECKOUT=true
FEATURE_BETA_API=0
Quick start
1. Install and register the module
npm install @hazeljs/feature-toggle
import { 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
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' };
}
}
3. Use the service where you need to branch
constructor(private readonly featureToggle: FeatureToggleService) {}
if (this.featureToggle.isEnabled('newCheckout')) { ... }
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
FeatureToggleServiceand returnsisEnabled(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 runtimeset(). - 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.
Top comments (0)