HazelJS Feature Toggle Package

npm downloads

@hazeljs/feature-toggle adds feature flags to HazelJS — protect routes with @FeatureToggle() or branch in code. Flags stored in memory, seeded from options or environment variables.

Quick Reference

  • Purpose: @hazeljs/feature-toggle provides feature flags for HazelJS with decorator-based route protection and programmatic flag checks, seeded from config or environment variables.
  • When to use: Use @hazeljs/feature-toggle to ship features behind flags, guard routes until ready, or run A/B code paths.
  • Key concepts: FeatureToggleModule, @FeatureToggle('name') decorator (returns 403 when disabled), FeatureToggleService (programmatic check), environment variable seeding (FEATURE_*).
  • Dependencies: @hazeljs/core.
  • Common patterns: Register FeatureToggleModule.forRoot({ features: { newUi: true } }) → apply @FeatureToggle('newUi') on routes → or inject FeatureToggleService and call isEnabled('newUi').
  • Common mistakes: Not providing default values for flags; using feature toggles as permanent configuration (clean up old flags); not testing both enabled and disabled paths.

Purpose

You need to ship or test features behind a switch, guard routes until a feature is ready, or run A/B code paths. The @hazeljs/feature-toggle package provides:

  • Decorator-first API@FeatureToggle('name') on a controller or method; disabled flag returns 403
  • In-memory store – No external service; optional seed from initialFlags or env prefix
  • Programmatic checks – Inject FeatureToggleService and use isEnabled(), set(), get()
  • Env integration – Use an env prefix (e.g. FEATURE_) so vars like FEATURE_NEW_CHECKOUT become flags

Architecture

graph TD
  A["Module forRoot<br/>(initialFlags, envPrefix)"] --> B["FeatureToggleService<br/>(in-memory Map)"]
  B --> C["@FeatureToggle decorator"]
  B --> D["Programmatic isEnabled/set/get"]
  C --> E["Guard per flag<br/>(canActivate)"]
  E --> B

Key components

  1. FeatureToggleModule – Registers the service and optional forRoot(options) for initial flags and env prefix
  2. FeatureToggleService – Holds flags in memory; isEnabled(name), get(name), set(name, value)
  3. @FeatureToggle(name) – Method/class decorator that applies a guard; guard injects the service and returns isEnabled(name)
  4. Guard – One guard class per feature name (cached); when the flag is off, the framework returns 403

Installation

npm install @hazeljs/feature-toggle

Quick Start

Register the module

import { HazelModule } from '@hazeljs/core';
import { FeatureToggleModule } from '@hazeljs/feature-toggle';

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

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' };
  }

  @Get('legacy')
  getLegacyCheckout() {
    return { flow: 'legacy' };
  }
}

Apply the decorator on a class to require the flag for all routes:

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

Use in services (programmatic)

import { Service } from '@hazeljs/core';
import { FeatureToggleService } from '@hazeljs/feature-toggle';

@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

forRoot(options?)

  • initialFlagsRecord<string, boolean>: flags to set when the module loads.
  • envPrefixstring: environment variable prefix. Any env var like PREFIX_NAME is read and stored as a flag. The name is converted to camelCase (e.g. FEATURE_NEW_UInewUi). Values true, 1, yes (case-insensitive) are treated as true; otherwise false.

Example:

FeatureToggleModule.forRoot({
  initialFlags: { newCheckout: true, betaApi: false },
  envPrefix: 'FEATURE_',
});

With env:

FEATURE_NEW_CHECKOUT=true
FEATURE_BETA_API=0
FEATURE_NEW_UI=yes

FeatureToggleService API

  • isEnabled(name: string): boolean – Returns whether the flag is on. Unset flags are treated as disabled (false).
  • get(name: string): boolean | undefined – Raw value; undefined if the flag was never set.
  • set(name: string, value: boolean): void – Set or override a flag at runtime (in-memory only).

Decorator behavior

  • @FeatureToggle(featureName: string) – Use on a controller class or on a route method. Composes with the framework’s guard system: when the flag is disabled, the request is rejected (403) and the handler is not executed. No need to use @UseGuards manually.

Best practices

  1. Naming – Use clear, stable flag names (e.g. newCheckout, betaApi).
  2. Env – Use envPrefix so you can turn flags on/off per environment without code changes.
  3. Defaults – Unset flags are off; set initialFlags or env for features you want on by default.
  4. Scoping – Prefer method-level @FeatureToggle when only some routes need the flag; use class-level for whole controllers behind a flag.

What's next?

  • Use Config for other environment-driven settings
  • Explore Auth for route protection with roles
  • Check Cache for caching feature-flagged responses

Recipes

Recipe: Feature-Flagged Route

// File: src/beta/beta.controller.ts
import { Controller, Get } from '@hazeljs/core';
import { FeatureGuard, Feature } from '@hazeljs/feature-toggle';
import { UseGuards } from '@hazeljs/core';

@Controller('beta')
export class BetaController {
  @Get('dashboard')
  @UseGuards(FeatureGuard)
  @Feature('beta-dashboard')
  getBetaDashboard() {
    return { message: 'Welcome to the beta dashboard' };
  }
}

Recipe: Branch Logic with Feature Flags

// File: src/search/search.service.ts
import { Service } from '@hazeljs/core';
import { FeatureToggleService } from '@hazeljs/feature-toggle';

@Service()
export class SearchService {
  constructor(private readonly features: FeatureToggleService) {}

  async search(query: string) {
    if (this.features.isEnabled('ai-search')) {
      // New AI-powered search
      return this.aiSearch(query);
    }
    // Fallback to keyword search
    return this.keywordSearch(query);
  }

  private aiSearch(query: string) { return []; }
  private keywordSearch(query: string) { return []; }
}