← Back to blog
featureflags

How to Implement Feature Flags Node.js

Step-by-step tutorial to implement feature flags in Node.js with Express using @luxkern/flags: setup, evaluate, middleware, and progressive rollout.

feature-flagsnodejsexpresstutorialluxkern

How to Implement Feature Flags in Node.js



Friday, 4:58 PM. You merge a pull request that refactors the checkout flow. CI is green. You deploy to production and close your laptop. By 5:47 PM, your Slack is blowing up: the new checkout breaks for users on the annual billing plan. Your options are a) revert the entire deploy, which also rolls back three other unrelated fixes, or b) hotfix under pressure while your team is heading out. With feature flags, the fix would have been a single API call to flip a boolean. No revert. No hotfix. No redeployment. The broken code stays deployed but dormant, and you fix it Monday morning with a clear head.

Feature flags decouple deployment from release. You deploy code whenever you want, but you activate features independently, for specific users, at specific percentages, on your schedule. Netflix uses them to test UI changes on 2% of users before a full rollout. GitHub uses them to ship dark features that employees test in production weeks before customers see them. And you can set them up in your Express app in about 15 minutes.

Install and Initialize the SDK



Start with a fresh project or add to your existing Express app:

mkdir flags-demo && cd flags-demo
npm init -y
npm install express @luxkern/flags dotenv


Create a .env file with your API key (grab it from the Luxkern dashboard under Projects > Settings > API Keys):

LUXKERN_FLAGS_API_KEY=lk_flags_your_key_here
PORT=3000


Now initialize the SDK in your server entry point:

// server.js
require("dotenv").config();
const express = require("express");
const { LuxkernFlags } = require("@luxkern/flags");

const app = express(); app.use(express.json());

const flags = new LuxkernFlags({ apiKey: process.env.LUXKERN_FLAGS_API_KEY, pollingInterval: 30_000, // fetch updated rules every 30s onReady: () => console.log("Feature flags SDK ready"), onError: (err) => console.error("Flags SDK error:", err.message), });

const PORT = process.env.PORT || 3000; app.listen(PORT, () => console.log(Running on port ${PORT}));

process.on("SIGTERM", () => { flags.close(); process.exit(0); });


The SDK fetches your flag rules on startup, then polls every 30 seconds for updates. When you toggle a flag in the dashboard, every running instance picks up the change within 30 seconds. In development, you can drop the polling interval to 5 seconds for faster iteration. In production, 30-60 seconds is the sweet spot -- responsive enough for emergency kill switches, low enough overhead to be negligible.

Build the Express Middleware



The middleware attaches flag evaluation methods to every request. It extracts user context from your auth layer and provides two functions: isEnabled for boolean flags and getVariant for multi-variant experiments.

// middleware/featureFlags.js
function featureFlagsMiddleware(flagsClient) {
  return (req, res, next) => {
    // Replace with your actual auth logic
    const user = req.user || {
      id: req.headers["x-user-id"] || "anonymous",
      email: req.headers["x-user-email"] || "",
      plan: req.headers["x-user-plan"] || "free",
    };

req.flags = { isEnabled: async (flagKey) => { try { const result = await flagsClient.evaluate(flagKey, { userId: user.id, email: user.email, plan: user.plan, }); return result.enabled; } catch (err) { console.error(Flag "${flagKey}" evaluation failed:, err.message); return false; // safe default: feature off } },

getVariant: async (flagKey) => { try { const result = await flagsClient.evaluate(flagKey, { userId: user.id, email: user.email, plan: user.plan, }); return result.value; } catch (err) { console.error(Flag "${flagKey}" evaluation failed:, err.message); return "control"; // safe default: control group } }, };

next(); }; }

module.exports = { featureFlagsMiddleware };


Wire it into your app:

const { featureFlagsMiddleware } = require("./middleware/featureFlags");
app.use(featureFlagsMiddleware(flags));


The safe defaults matter. If the SDK fails to fetch rules, if the network is down, if the flag does not exist -- your app does not crash. It falls back to "feature off" and "control variant." Your application keeps serving requests. You get an error log. Nobody pages you at 2 AM because a flag evaluation timed out.

Use Flags in Your Routes



With the middleware in place, flag evaluation in your routes is a single line:

// Feature-flagged search algorithm
app.get("/api/search", async (req, res) => {
  const query = req.query.q;
  if (!query) return res.status(400).json({ error: "Missing query" });

const useNewAlgorithm = await req.flags.isEnabled("new-search-algorithm");

const startTime = Date.now(); const results = useNewAlgorithm ? await semanticSearch(query) : await keywordSearch(query); const duration = Date.now() - startTime;

res.json({ results, meta: { algorithm: useNewAlgorithm ? "semantic-v2" : "keyword-v1", duration_ms: duration, }, }); });

// Multi-variant pricing experiment app.post("/api/calculate-price", async (req, res) => { const { productId, quantity } = req.body; const variant = await req.flags.getVariant("pricing-model");

let price; switch (variant) { case "dynamic": price = await calculateDynamicPrice(productId, quantity); break; case "tiered": price = await calculateTieredPrice(productId, quantity); break; case "control": default: price = await calculateFlatPrice(productId, quantity); }

res.json({ productId, quantity, price, pricing_model: variant }); });

// Gate an entire endpoint behind a flag app.post("/api/export/bulk", async (req, res) => { const enabled = await req.flags.isEnabled("bulk-export"); if (!enabled) return res.status(404).json({ error: "Not found" });

const data = await generateBulkExport(req.body.filters); res.json({ export: data }); });


Notice the 404 response when the bulk export flag is off. The endpoint is invisible to users who are not flagged in. It does not return a 403 ("you're not allowed") which hints that the endpoint exists. It returns a 404 ("this does not exist"). This is a subtle but deliberate pattern for hiding unreleased features.

Add a React Hook for Client-Side Flags



Server-side flags control API behavior. Client-side flags control UI behavior -- showing a new onboarding flow, a redesigned dashboard, or a beta label. Here is a React hook that evaluates flags on the client:

// hooks/useFeatureFlag.js
import { useState, useEffect } from "react";

export function useFeatureFlag(flagKey, defaultValue = false) { const [enabled, setEnabled] = useState(defaultValue); const [loading, setLoading] = useState(true);

useEffect(() => { async function evaluate() { try { const res = await fetch(/api/flags/evaluate, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ flag: flagKey }), }); const data = await res.json(); setEnabled(data.enabled); } catch { setEnabled(defaultValue); } finally { setLoading(false); } }

evaluate(); }, [flagKey, defaultValue]);

return { enabled, loading }; }

// Usage in a component function Dashboard() { const { enabled: showNewLayout, loading } = useFeatureFlag("new-dashboard-layout");

if (loading) return <DashboardSkeleton />;

return showNewLayout ? <DashboardV2 /> : <DashboardV1 />; }


The corresponding API endpoint on your Express server:

app.post("/api/flags/evaluate", async (req, res) => {
  const { flag } = req.body;
  const enabled = await req.flags.isEnabled(flag);
  res.json({ flag, enabled });
});


This approach keeps your flag rules on the server. The client never sees targeting rules or percentages -- it only receives a boolean result. This is more secure than shipping flag configuration to the browser and evaluating client-side.

Set Up a Progressive Rollout



Progressive rollout is the safest way to launch a feature. You start at 5% of users, watch your error rates and metrics for a few days, then bump to 25%, then 50%, then 100%. If something breaks, you reduce the percentage instead of reverting code.

In the Luxkern dashboard, create a flag called new-onboarding-flow and configure targeting:

  • Rule 1: email ends with @yourcompany.com -- Enabled (internal dogfooding)
  • Rule 2: plan equals enterprise -- Enabled (early access for paying customers)
  • Default rule: 5% rollout (percentage-based, hashed on userId)


  • The SDK uses a deterministic hash of the userId to assign users to buckets. This guarantees two things: the same user always gets the same result (no flickering between page loads), and when you increase from 5% to 25%, the original 5% stay in -- 20% more users are added. Nobody loses access to a feature mid-rollout.

    Verify your rollout percentage with this script:

    // verify-rollout.js
    require("dotenv").config();
    const { LuxkernFlags } = require("@luxkern/flags");

    async function verify() { const flags = new LuxkernFlags({ apiKey: process.env.LUXKERN_FLAGS_API_KEY, });

    // Wait for SDK to initialize await new Promise((resolve) => setTimeout(resolve, 2000));

    const SAMPLE = 10_000; let enabled = 0;

    for (let i = 0; i < SAMPLE; i++) { const result = await flags.evaluate("new-onboarding-flow", { userId: test-user-${i}, email: user${i}@example.com, plan: "free", }); if (result.enabled) enabled++; }

    console.log(Rollout: ${((enabled / SAMPLE) * 100).toFixed(1)}% (${enabled}/${SAMPLE})); // At 5% rollout, expect: "Rollout: 5.0% (500/10000)" flags.close(); }

    verify();


    A typical rollout schedule looks like this:

  • Day 1: 5% -- watch error rates, latency, support tickets
  • Day 3: 25% -- if metrics are stable, widen the blast radius
  • Day 7: 50% -- half your users now see the feature
  • Day 10: 100% -- full rollout, monitor for 48 hours
  • Day 12: Remove the flag from code, clean up the conditional logic


  • The last step is the one most teams skip. Feature flags that live in your codebase forever become technical debt. Set a calendar reminder to remove the flag 2 weeks after hitting 100%.

    Production Hardening Patterns



    Two patterns that prevent flag evaluation from becoming a reliability risk:

    Timeout protection. Flag evaluation should never add more than a few milliseconds to your request. Wrap evaluations in a timeout:

    async function evaluateWithTimeout(flagsClient, flagKey, context, timeoutMs = 100) {
      try {
        return await Promise.race([
          flagsClient.evaluate(flagKey, context),
          new Promise((_, reject) =>
            setTimeout(() => reject(new Error("Flag timeout")), timeoutMs)
          ),
        ]);
      } catch {
        return { enabled: false, value: "control" };
      }
    }


    Structured logging. Log every flag evaluation so you can correlate feature states with errors:

    app.use((req, res, next) => {
      const evaluations = [];
      const originalIsEnabled = req.flags.isEnabled;

    req.flags.isEnabled = async (key) => { const result = await originalIsEnabled(key); evaluations.push({ flag: key, enabled: result }); return result; };

    res.on("finish", () => { if (evaluations.length > 0) { console.log(JSON.stringify({ type: "flag_evaluations", path: req.path, user_id: req.headers["x-user-id"], evaluations, status: res.statusCode, })); } });

    next(); });


    When a user reports a bug, search your logs for their user_id and you will see exactly which flags were active during their session. This turns "works on my machine" debugging into "the user had new-search-algorithm enabled, which explains the 800ms latency."

    For more background on when and why to use feature flags, read What Are Feature Flags. If you are evaluating options and considering LaunchDarkly, our comparison for indie developers in 2026 breaks down pricing and features at the scale where it actually matters for small teams.

    You deployed on Friday at 5 PM. Your feature flag is your insurance policy. Flip a boolean, not a deploy.

    Try Luxkern FeatureFlags free -- no credit card required.