Flaggy
Get started
← Blog

Using feature flags in Angular

How to implement feature flags in Angular the idiomatic way — a service, an APP_INITIALIZER, a custom structural directive, route guards, and per-user targeting.

Angular’s dependency injection and structural directives make feature flags fit the framework cleanly — far more so than dropping raw if statements through your components. This guide shows the idiomatic way to wire feature flags into an Angular app: a single service, initialization at bootstrap, a *featureFlag directive for templates, route guards for gating whole pages, and per-user targeting.

If you’re new to the concept itself, start with what are feature flags and come back. This guide assumes you know what a flag is and want to use one in Angular.

The core service

flaggy() returns a global singleton, so wrap it in an injectable service to give the rest of your app a typed handle through Angular’s DI. Create the client once and expose a check.

// feature-flag.service.ts
import { Injectable } from '@angular/core';
import { flaggy } from '@flaggy.io/sdk-js';
import { environment } from '../environments/environment';

@Injectable({ providedIn: 'root' })
export class FeatureFlagService {
  private client = flaggy({
    apiKey: environment.flaggyApiKey,
    environment: environment.production ? 'production' : 'development',
  });

  async init(): Promise<void> {
    await this.client.initialize();
  }

  isEnabled(key: string, context?: Record<string, unknown>): boolean {
    return this.client.isEnabled(key, context);
  }
}

Put the API key in your environment.ts files so it differs per build target. The SDK’s environment option accepts "production", "staging", or "development" and defaults to "production". A client-side key is safe to expose — it only authorizes reading flag rules, not changing them.

Initialize at bootstrap with APP_INITIALIZER

Flags need to be ready before your first component renders, or you’ll get a flash of the wrong UI. APP_INITIALIZER runs a factory during bootstrap and waits for the returned promise before the app starts.

// app.config.ts (standalone)
import { ApplicationConfig, APP_INITIALIZER } from '@angular/core';
import { FeatureFlagService } from './feature-flag.service';

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_INITIALIZER,
      multi: true,
      deps: [FeatureFlagService],
      useFactory: (flags: FeatureFlagService) => () => flags.init(),
    },
  ],
};

For an older NgModule app, register the same provider in app.module.ts. The effect is identical: the SDK downloads its ruleset before any route activates, so every isEnabled call after this point is a synchronous in-memory lookup.

A structural directive for templates

The cleanest way to gate markup is a custom structural directive that works just like *ngIf. This keeps flag checks declarative and out of your component classes.

// feature-flag.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
import { FeatureFlagService } from './feature-flag.service';

@Directive({ selector: '[featureFlag]', standalone: true })
export class FeatureFlagDirective {
  constructor(
    private tpl: TemplateRef<unknown>,
    private vcr: ViewContainerRef,
    private flags: FeatureFlagService,
  ) {}

  @Input() set featureFlag(key: string) {
    const negate = key.startsWith('!');
    const flag = negate ? key.slice(1) : key;
    const show = this.flags.isEnabled(flag) !== negate;
    this.vcr.clear();
    if (show) this.vcr.createEmbeddedView(this.tpl);
  }
}

Now gate any element, and use a leading ! for the fallback branch:

<app-new-dashboard *featureFlag="'release-new-dashboard'"></app-new-dashboard>
<app-old-dashboard *featureFlag="'!release-new-dashboard'"></app-old-dashboard>

Import the standalone directive into any component that needs it (or your shared module). Because it reads from the already-initialized service, there’s no async flicker.

Gating routes with a guard

For features that are entire pages, gate the route instead of the template. A functional CanActivateFn keeps unfinished pages unreachable until the flag is on.

// feature-flag.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { FeatureFlagService } from './feature-flag.service';

export function requireFlag(key: string): CanActivateFn {
  return () => {
    const flags = inject(FeatureFlagService);
    const router = inject(Router);
    return flags.isEnabled(key) ? true : router.parseUrl('/');
  };
}
// app.routes.ts
import { Routes } from '@angular/router';
import { requireFlag } from './feature-flag.guard';

export const routes: Routes = [
  {
    path: 'new-reports',
    loadComponent: () => import('./reports/reports.component'),
    canActivate: [requireFlag('release-new-reports')],
  },
];

Combined with loadComponent, the route’s code is also lazy-loaded, so a gated-off feature isn’t even downloaded.

How flag changes reach the client

The SDK refreshes its cached rules in the background — every 60 seconds by default, backing off on failure up to 15 minutes. When you flip a flag in the dashboard, clients pick it up on their next refresh, so a change propagates within about a minute rather than instantly. isEnabled always reads the latest cached value.

There’s no push event to subscribe to. For a long-lived view that must reflect a mid-session change (a kill switch, an experiment ending) without waiting for navigation, expose the flag as a signal and refresh it on an interval:

// in FeatureFlagService — add the signal and extend the init() from earlier
import { signal } from '@angular/core';

readonly maintenanceBanner = signal(false);

async init(): Promise<void> {
  await this.client.initialize();
  const read = () => this.maintenanceBanner.set(this.isEnabled('ops-maintenance-banner'));
  read();
  setInterval(read, 60_000);
}
@if (flags.maintenanceBanner()) {
  <app-maintenance-banner />
}

For most release flags a synchronous check is fine — you read it once at render. Reserve polling for the rare flag that must visibly change mid-session.

Per-user targeting

Percentage rollouts, beta cohorts, and plan-tier gating all rely on passing a user context to the check. The targeting rule lives in the dashboard, not in your Angular code — that’s what lets a product manager change who sees a feature without a deploy.

const showEditor = this.flags.isEnabled('experiment-new-editor', {
  key: this.user.id,
  plan: this.user.plan,
  country: this.user.countryCode,
});

You define the rule once — “users where plan equals team AND country equals US” — and adjust it from the dashboard whenever you want.

Keeping Angular flags tidy

The Angular wiring above is the easy part. The harder discipline is removing flags once a feature is fully rolled out, so your templates don’t fill up with *featureFlag directives that are permanently true. A naming convention and a removal habit are what prevent that — covered in feature flag management.

The same model — service, idiomatic primitive, dashboard-side targeting — applies to React, Vue, and Svelte. See our guides on using feature flags in React and feature flags in JavaScript.

Flaggy gives you local-evaluation SDKs, segmentation, and a full audit log on a flat $99/month plan — no per-user metering.