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
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 };
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();
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);
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));
});
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
);
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();
}
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"
]
}
}
}
}
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);
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);
});
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');
});
});
Production Checklist
Before enabling feature flags in production:
- [ ] Default values are safe: Every flag defaults to existing behavior — new features are
offby default - [ ] Provider startup failure is handled: Use
setProviderAndWaitwith try/catch; fall back to defaults, don't crash - [ ] Evaluation is async-safe: Never
awaitflag evaluation in synchronous middleware - [ ] Metrics are wired: Hook into
afteranderrorlifecycle 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)