Feature Flags in Node.js: Express and Fastify Guide

Originally published on rollgate.io/blog/feature-flags-nodejs.
Why Feature Flags in Node.js?
Node.js powers a huge slice of production backends — REST APIs, GraphQL gateways, background workers, BFF layers, real-time services. All of them share the same release problem: you want to ship code continuously, but you do not want every deploy to be a product change.
Feature flags in Node.js decouple deployment from release. You push code to production behind a flag and decide later who sees the new behavior, when, and under what conditions. If something breaks, you flip the flag off in the dashboard — no redeploy, no rollback PR, no pager at 3am.
This guide covers the practical side: how to wire feature flags into Express and Fastify applications, how to target specific users, how to roll out gradually, and the production gotchas that every team hits sooner or later.
Quick Start: Feature Flags in Node.js
Let us get a flag running end-to-end. Install the SDK:
npm install @rollgate/sdk-node
Then wire it up:
import { RollgateClient } from '@rollgate/sdk-node';
const rollgate = new RollgateClient({
apiKey: process.env.ROLLGATE_API_KEY!,
enableStreaming: true,
});
await rollgate.init();
if (rollgate.isEnabled('new-checkout', false)) {
console.log('New checkout flow enabled');
} else {
console.log('Legacy checkout');
}
That is the whole setup. The SDK pulls rules from the API, caches them in memory, and keeps them fresh in the background. With enableStreaming: true the client keeps a Server-Sent Events connection open and applies changes within ~50ms of a flag flip.
Evaluation is local, in-process. No network hop per flag check — the rules are already in memory, so you can evaluate thousands of flags per request without adding latency to the hot path.
The DIY Approach (and Its Limitations)
Before reaching for a dedicated platform, most teams start with environment variables:
const flags = {
newCheckout: process.env.NEW_CHECKOUT === 'true',
};
if (flags.newCheckout) {
// ...
}
This works, for exactly one week. Then you hit the limitations:
- Every flag change requires a redeploy
- No gradual rollouts — all-or-nothing
- No targeting for beta testers, enterprise plans, regions
- No kill switch — rolling back means another deploy
- No audit trail
Using Rollgate with Express
import express from 'express';
import { RollgateClient } from '@rollgate/sdk-node';
const app = express();
const rollgate = new RollgateClient({
apiKey: process.env.ROLLGATE_API_KEY!,
enableStreaming: true,
});
await rollgate.init();
app.use((req, res, next) => {
const userId = req.headers['x-user-id'] as string | undefined;
req.flags = {
isEnabled: (key: string, fallback = false) =>
rollgate.isEnabled(key, fallback, userId ? { userId } : undefined),
};
next();
});
app.get('/checkout', (req, res) => {
if (req.flags.isEnabled('new-checkout')) {
return res.json({ version: 'v2' });
}
return res.json({ version: 'v1' });
});
app.listen(3000);
The EvalContext passed as the third argument lets you evaluate a flag for a specific user without mutating client-level state.
Shut the client down cleanly on SIGTERM:
process.on('SIGTERM', async () => {
await rollgate.close();
process.exit(0);
});
Using Rollgate with Fastify
import Fastify from 'fastify';
import { RollgateClient } from '@rollgate/sdk-node';
const fastify = Fastify({ logger: true });
const rollgate = new RollgateClient({
apiKey: process.env.ROLLGATE_API_KEY!,
enableStreaming: true,
});
await rollgate.init();
fastify.decorateRequest('flags', null);
fastify.addHook('onRequest', async (request) => {
const userId = request.headers['x-user-id'] as string | undefined;
request.flags = {
isEnabled: (key: string, fallback = false) =>
rollgate.isEnabled(key, fallback, userId ? { userId } : undefined),
};
});
fastify.get('/api/experiments', async (request) => {
return {
pricing: request.flags.isEnabled('new-pricing-ui'),
search: request.flags.isEnabled('semantic-search'),
};
});
fastify.addHook('onClose', async () => {
await rollgate.close();
});
await fastify.listen({ port: 3000 });
Gradual Rollouts and User Targeting
Once flags are wired, the real value kicks in: turning a feature on for 1% of traffic, watching error rates for an hour, then bumping it to 10% the next day. Rollgate handles this with sticky, deterministic bucketing.
const showNewFlow = rollgate.isEnabled('checkout-v2', false, {
userId: user.id,
attributes: {
plan: user.plan,
region: user.region,
signupDate: user.signupDate,
},
});
Production Considerations
Caching and evaluation mode. The SDK evaluates locally by default. No network call on each isEnabled().
Resilience. Circuit breaker, retry-with-backoff, stale cache fallback. If the API becomes unreachable, your service keeps serving flag evaluations using the last known rules.
Kill switches in production. Wrap risky code paths in a flag you can flip instantly. A flag flip takes under a second; a rollback deploy takes tens of minutes.
One client per process, not per request. The SDK is thread-safe and reusable.
Best Practices
- Name flags by feature, not by team
- Always pass a sensible default
- Retire flags once a rollout hits 100%
- Log the evaluated value for high-stakes flags
- Separate experimentation flags from release flags
Read the full version with code examples and related guides on rollgate.io/blog/feature-flags-nodejs.





