DEV Community

1xApi
1xApi

Posted on • Originally published at 1xapi.com

How to Implement Feature Flags in Node.js APIs with OpenFeature (2026 Guide)

Feature flags — also called feature toggles — let you ship code to production while keeping features hidden behind a runtime switch. No risky big-bang releases. No hotfix scrambles at 2 AM. Just a clean, controlled rollout where you decide when users see what you built.

In 2026, the industry has converged on OpenFeature as the open, vendor-agnostic standard for feature flag management — backed by the CNCF and used by teams at every scale. In this guide, you'll implement production-grade feature flags in a Node.js API using the OpenFeature SDK v1.20.2, with real patterns for canary releases, A/B testing, and emergency kill switches.

Why Feature Flags Matter for API Developers

Most tutorials treat feature flags as a frontend problem — show a new button to 10% of users. But feature flags are even more powerful in backend APIs:

  • Canary deployments: Roll out a new algorithm or database query to 5% of traffic, measure error rates, then promote or roll back — all without redeployment
  • Kill switches: If your new payment processor integration starts throwing 500s at midnight, flip a flag from your phone — no kubectl required
  • Database migrations: Shadow-write to a new schema, read from old until you're confident, flip the read flag
  • Tenant-specific features: Enable beta features for specific API key holders
  • Experiment groups: A/B test two different rate limiting algorithms and compare latency

Without flags, every one of these requires a deployment. With flags, it's a config change.

OpenFeature: The CNCF Standard (2026)

OpenFeature launched as a CNCF sandbox project in 2022 and has matured significantly. As of early 2026:

  • Spec v0.8.0 — Covers evaluation context, hooks, events, tracking for A/B tests, transaction context propagation, and multi-provider support
  • Node.js SDK v1.20.2 — Full spec compliance, async evaluation, TypeScript-first
  • CNCF Incubating — Production-ready, adopted by Flagsmith, LaunchDarkly, Unleash, Flipt, and others

The key benefit: switching flag providers (from LaunchDarkly to Flagsmith, or to your own in-house system) requires zero application code changes. Only the provider configuration changes.

Project Setup

Install the OpenFeature Node.js SDK and a provider. For this guide, we'll use a self-hosted approach with an in-memory provider for local dev and the open-source Flagd for production:

npm install @openfeature/server-sdk @openfeature/core
# For production with Flagd
npm install @openfeature/flagd-provider
Enter fullscreen mode Exit fullscreen mode

Supported providers in 2026: LaunchDarkly, Flagsmith, Unleash, Flagd (self-hosted), Split, CloudBees, and more — all spec-compliant.

Step 1: Configure OpenFeature in Your Express App

Create src/flags.ts to centralize your flag setup:

import { OpenFeature, OpenFeatureAPI } from '@openfeature/server-sdk';
import { FlagdProvider } from '@openfeature/flagd-provider';

export async function initFeatureFlags(): Promise<void> {
  const provider = new FlagdProvider({
    host: process.env.FLAGD_HOST || 'localhost',
    port: Number(process.env.FLAGD_PORT) || 8013,
    tls: process.env.NODE_ENV === 'production',
  });

  // Wait for the provider to connect before handling traffic
  await OpenFeature.setProviderAndWait(provider);

  console.log('[flags] OpenFeature provider ready:', provider.metadata.name);
}

export { OpenFeature };
Enter fullscreen mode Exit fullscreen mode

Wire it up in src/app.ts:

import express from 'express';
import { initFeatureFlags } from './flags';

const app = express();

async function start() {
  await initFeatureFlags(); // Flags ready before first request

  app.listen(3000, () => {
    console.log('API running on port 3000');
  });
}

start();
Enter fullscreen mode Exit fullscreen mode

The setProviderAndWait() call is critical — it blocks startup until the provider has fetched its initial flag state, preventing a race condition where requests arrive before flags are loaded.

Step 2: Evaluate Flags with Evaluation Context

The real power of OpenFeature is evaluation context — passing user/request attributes so the provider can make targeting decisions:

import { OpenFeature } from '@openfeature/server-sdk';
import { Request, Response, NextFunction } from 'express';

// Middleware: attach OpenFeature client and evaluation context to each request
export function featureFlagMiddleware(
  req: Request,
  res: Response,
  next: NextFunction
) {
  const client = OpenFeature.getClient();

  // Evaluation context — passed to the provider for targeting rules
  const context = {
    targetingKey: req.headers['x-api-key'] as string || 'anonymous',
    userId: (req as any).user?.id,
    plan: (req as any).user?.plan || 'free',
    region: req.headers['cf-ipcountry'] as string || 'unknown',
    apiVersion: req.headers['accept-version'] as string,
  };

  (req as any).flags = { client, context };
  next();
}

app.use(featureFlagMiddleware);
Enter fullscreen mode Exit fullscreen mode

Now use flags inside route handlers:

app.get('/api/search', async (req, res) => {
  const { client, context } = (req as any).flags;

  // Boolean flag: use new search algorithm?
  const useV2Search = await client.getBooleanValue(
    'search-v2-algorithm',
    false,  // default if flag eval fails
    context
  );

  // String flag: which ranking model to use?
  const rankingModel = await client.getStringValue(
    'search-ranking-model',
    'bm25',  // default
    context
  );

  if (useV2Search) {
    return res.json(await searchV2(req.query.q, rankingModel));
  }
  return res.json(await searchV1(req.query.q));
});
Enter fullscreen mode Exit fullscreen mode

Step 3: Flag Types — Boolean, String, Number, JSON

OpenFeature supports four flag types. Use the right one:

const client = OpenFeature.getClient();
const ctx = { targetingKey: 'user-123', plan: 'pro' };

// Boolean — feature on/off
const newDashboard = await client.getBooleanValue('new-dashboard', false, ctx);

// String — variant selection, config values
const paymentProvider = await client.getStringValue('payment-provider', 'stripe', ctx);

// Number — thresholds, limits
const rateLimit = await client.getNumberValue('api-rate-limit', 100, ctx);

// Object — complex config (JSON)
const experimentConfig = await client.getObjectValue(
  'experiment-config',
  { variant: 'control', weight: 0 },
  ctx
);
Enter fullscreen mode Exit fullscreen mode

JSON flags are especially useful for complex A/B test configurations — you can store the full experiment definition in the flag value instead of hardcoding it.

Step 4: Production Kill Switch Pattern

The most important flag pattern for API developers: the kill switch. When a new feature causes problems in production, you flip a flag and instantly revert — no deployment:

// src/middleware/killSwitch.ts
import { OpenFeature } from '@openfeature/server-sdk';

const KILL_SWITCHES: Record<string, string> = {
  'kill-v2-payments': '/api/payments',
  'kill-new-search': '/api/search',
  'kill-ai-recommendations': '/api/recommendations',
};

export async function killSwitchMiddleware(
  req: Request,
  res: Response,
  next: NextFunction
) {
  const client = OpenFeature.getClient();

  for (const [flagKey, path] of Object.entries(KILL_SWITCHES)) {
    if (req.path.startsWith(path)) {
      const killed = await client.getBooleanValue(flagKey, false);
      if (killed) {
        return res.status(503).json({
          error: 'SERVICE_TEMPORARILY_UNAVAILABLE',
          message: 'This feature is temporarily disabled. Please try again later.',
          retryAfter: 60,
        });
      }
    }
  }

  next();
}
Enter fullscreen mode Exit fullscreen mode

This middleware checks kill switches before every request to guarded routes. When you flip kill-v2-payments to true in your flag dashboard, all /api/payments requests immediately get a clean 503 — rather than errors cascading.

Step 5: Canary Releases with Percentage-Based Rollouts

Configure your flag provider to use percentage-based targeting. Here's a Flagd flag definition (flags.flagd.json):

{
  "flags": {
    "search-v2-algorithm": {
      "state": "ENABLED",
      "variants": {
        "on": true,
        "off": false
      },
      "defaultVariant": "off",
      "targeting": {
        "fractional": [
          ["on", 10],
          ["off", 90]
        ]
      }
    },
    "payment-provider": {
      "state": "ENABLED",
      "variants": {
        "stripe": "stripe",
        "adyen": "adyen"
      },
      "defaultVariant": "stripe",
      "targeting": {
        "if": [
          { "===": [{"var": "plan"}, "enterprise"] },
          "adyen",
          "stripe"
        ]
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The first flag routes 10% of traffic to the new search algorithm using fractional targeting — consistent per targetingKey, so the same user always gets the same variant. The second flag routes all enterprise plan users to Adyen as the payment provider.

Step 6: Hooks — Logging, Metrics, and Error Handling

OpenFeature hooks fire at specific points in the flag evaluation lifecycle. Use them to add observability without touching your flag evaluation code:

import { Hook, HookContext, EvaluationDetails } from '@openfeature/server-sdk';

// Metrics hook — track flag evaluation latency and values
const metricsHook: Hook = {
  after(hookContext: HookContext, details: EvaluationDetails<unknown>) {
    metrics.histogram('feature_flag.evaluation_ms', hookContext.executionTime ?? 0, {
      flag: hookContext.flagKey,
      variant: details.variant ?? 'unknown',
    });
    metrics.increment('feature_flag.evaluations_total', {
      flag: hookContext.flagKey,
      value: String(details.value),
    });
  },

  error(hookContext: HookContext, error: unknown) {
    logger.error('Feature flag evaluation failed', {
      flag: hookContext.flagKey,
      defaultValue: hookContext.defaultValue,
      error,
    });
    // Flag evaluation already returns default value on error — this just logs it
  },
};

// Register globally — applies to all evaluations
OpenFeature.addHooks(metricsHook);
Enter fullscreen mode Exit fullscreen mode

Step 7: Events — React to Flag Changes

OpenFeature's eventing system lets you react when the provider receives updated flag configurations:

import { OpenFeature, ProviderEvents } from '@openfeature/server-sdk';

OpenFeature.addHandler(ProviderEvents.Ready, () => {
  logger.info('[flags] Provider is ready');
});

OpenFeature.addHandler(ProviderEvents.ConfigurationChanged, (event) => {
  logger.info('[flags] Flag configuration updated', {
    flagsChanged: event?.flagsChanged ?? [],
  });

  // Clear any flag-dependent caches
  if (event?.flagsChanged?.includes('search-v2-algorithm')) {
    searchCache.clear();
  }
});

OpenFeature.addHandler(ProviderEvents.Error, (event) => {
  logger.error('[flags] Provider error — falling back to defaults', event);
  alerting.send('feature-flags-provider-error', event);
});
Enter fullscreen mode Exit fullscreen mode

Step 8: Testing Feature Flags

Use the OpenFeature In-Memory provider for unit tests — zero infrastructure required:

import { OpenFeature, InMemoryProvider } from '@openfeature/server-sdk';

describe('Search API', () => {
  beforeEach(async () => {
    await OpenFeature.setProviderAndWait(
      new InMemoryProvider({
        'search-v2-algorithm': {
          defaultVariant: 'on',
          variants: { on: true, off: false },
        },
        'search-ranking-model': {
          defaultVariant: 'semantic',
          variants: { bm25: 'bm25', semantic: 'semantic' },
        },
      })
    );
  });

  it('uses V2 search algorithm when flag is on', async () => {
    const res = await request(app).get('/api/search?q=nodejs');
    expect(res.body.algorithm).toBe('v2');
  });

  it('falls back to V1 when flag evaluation throws', async () => {
    await OpenFeature.setProviderAndWait(new ErroringProvider());
    const res = await request(app).get('/api/search?q=nodejs');
    expect(res.status).toBe(200); // Default value used, no crash
    expect(res.body.algorithm).toBe('v1');
  });
});
Enter fullscreen mode Exit fullscreen mode

Production Checklist

Before enabling feature flags in production:

  • [ ] Default values are safe: Every flag defaults to existing behavior — new features are off by default
  • [ ] Provider startup failure is handled: Use setProviderAndWait with try/catch; fall back to defaults, don't crash
  • [ ] Evaluation is async-safe: Never await flag evaluation in synchronous middleware
  • [ ] Metrics are wired: Hook into after and error lifecycle events
  • [ ] Events are monitored: Alert on ProviderEvents.Error
  • [ ] Flags are cleaned up: A flag at 100% for 30+ days is dead code — remove it
  • [ ] Documentation: Every flag has a description, owner, and cleanup date

Summary

OpenFeature v1.20.2 gives Node.js APIs a production-grade, vendor-neutral feature flagging system. The key patterns:

Pattern Use Case
getBooleanValue Feature on/off, kill switches
Evaluation context User targeting, tenant flags, A/B groups
Fractional targeting Canary rollouts (10%, 50%, 100%)
Hooks Metrics, logging, error tracking
Events Cache invalidation, alerting on provider changes
Transaction context Consistent evaluation across one request
InMemoryProvider Unit testing without infrastructure

The vendor-agnostic design means you can start with a simple JSON file on disk (via Flagd) and graduate to LaunchDarkly or Flagsmith later — without changing a single line of application code.


Building APIs that power apps and need reliable feature rollouts? Check out 1xAPI on RapidAPI for production-ready API building blocks.

Top comments (0)