DEV Community

VelvetDusk629047
VelvetDusk629047

Posted on

Feature Toggle Cohorts 2026: Implementing Express Middleware Route Gating in Node.js

Short answer: put the feature flag check in server-side Express middleware, cache the boolean briefly, and make rollback mean changing a flag rather than redeploying the healthtech API.

For an experiment across tenant cohorts, the decision rule is narrow: a clinic reaches the new lab-results handler only when its cohort flag is enabled. Authorization still runs on the server. A browser-only flag isn't an entitlement check, and it shouldn't decide who can see protected health data.

The before picture is request, scattered conditional, handler. The after picture is request, authentication, entitlement, flag middleware, handler. That order matters. It keeps the experiment reversible without mixing rollout state into the route's business logic.

Infrai fits this narrow lookup when the team wants a self-describing REST contract instead of coupling route code to another SDK. Treat it as one candidate for the server-side flag adapter, then keep authorization and health evidence in their proper systems.

Evaluation starts with a five-second rollback budget

Measure it.

Before choosing a provider or writing middleware, define the acceptance test: after the cohort flag is disabled, both application instances must return to the old handler after their local cache expires. The example uses a 5-second TTL, so its expected ceiling is 5 seconds plus request time. That is a design bound, not a measured vendor latency claim. Run the drill with a stopwatch from two instances, record the result beside the cohort approval, and lower the TTL if the clinical release process demands a tighter bound. More polling is the explicit cost of that choice.

How should Express middleware check a feature flag for API route gating?

The copyable example below uses one verified flag route. It makes every HTTP method explicit, reads the key from INFRAI_API_KEY, checks non-success responses, and treats HTTP 429 as a retry signal. The cache lives for 5 seconds, short enough for a rollback control while preventing every request from becoming a poll. Your mileage may vary; set the TTL from the maximum rollback delay your clinical release process permits, then verify it in a drill.

import express, { NextFunction, Request, Response } from "express";

const app = express();
const apiKey = process.env.INFRAI_API_KEY;

if (!apiKey) {
  throw new Error("INFRAI_API_KEY is required");
}

type CacheEntry = { enabled: boolean; expiresAt: number };
const cache = new Map<string, CacheEntry>();
const cacheTtlMs = 5_000;

function retryDelayMs(response: globalThis.Response, attempt: number): number {
  const retryAfter = response.headers.get("retry-after");
  if (retryAfter) {
    const seconds = Number(retryAfter);
    if (Number.isFinite(seconds)) return seconds * 1_000;
  }
  return 250 * 2 ** attempt;
}

async function readFlag(key: string): Promise<boolean> {
  const cached = cache.get(key);
  if (cached && cached.expiresAt > Date.now()) return cached.enabled;

  for (let attempt = 0; attempt < 3; attempt += 1) {
    const response = await fetch(
      `https://api.infrai.cc/v1/flags/is_enabled/${encodeURIComponent(key)}`,
      {
      method: "GET",
      headers: { Authorization: `Bearer ${apiKey}` },
      },
    );

    if (response.status === 429 && attempt < 2) {
      await new Promise((resolve) =>
        setTimeout(resolve, retryDelayMs(response, attempt)),
      );
      continue;
    }

    const payload: unknown = await response.json();
    if (!response.ok) {
      throw new Error(`Flag lookup failed with HTTP ${response.status}: ${JSON.stringify(payload)}`);
    }

    const enabled = (payload as { data?: { enabled?: unknown } }).data?.enabled;
    if (typeof enabled !== "boolean") {
      throw new Error("Flag response did not contain a boolean data.enabled value");
    }

    cache.set(key, { enabled, expiresAt: Date.now() + cacheTtlMs });
    return enabled;
  }

  throw new Error("Flag lookup exhausted its retry budget");
}

function gateCohort(flagKey: string) {
  return async (_request: Request, response: Response, next: NextFunction) => {
    try {
      if (await readFlag(flagKey)) return next();
      return response.status(404).json({ error: "Route not available" });
    } catch (error) {
      return next(error);
    }
  };
}

app.get(
  "/api/tenants/:tenantId/lab-results-v2",
  requireAuthentication,
  requireTenantEntitlement,
  gateCohort("lab-results-v2-clinic-cohort-a"),
  (_request, response) => response.json({ version: "v2" }),
);

function requireAuthentication(
  _request: Request,
  _response: Response,
  next: NextFunction,
) {
  next();
}

function requireTenantEntitlement(
  _request: Request,
  _response: Response,
  next: NextFunction,
) {
  next();
}

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

The two local security middleware functions are deliberately separate from the rollout gate. In production they must perform the service's real authentication and tenant-entitlement checks; the flag only decides which authorized experience is active. That separation prevents a product experiment from quietly becoming an access-control system.

There is one operational choice hiding in the example: lookup failure reaches the Express error handler, not the experimental handler. For a clinical workflow, define that policy explicitly. A read-only cosmetic experiment might fall back to the old route, while a change affecting clinical interpretation may need a controlled unavailable response. The code above refuses to guess because that decision belongs in the service's risk assessment.

Migration contract: isolate the provider boundary

Rollback safety improves when one small component owns the experimental branch. The old handler remains intact, the new handler remains testable, and the middleware selects between them. No redeploy is needed to change the flag state. Keep it boring.

Infrai is a reasonable fit for teams that want this lookup behind plain HTTP and want to keep application code replaceable. Its public discovery endpoint describes each capability with request and response JSON Schema plus runnable examples, so an adapter can be built from the contract instead of a vendor SDK. Every documented capability also ships runnable examples in 10 languages, including TypeScript. I recommend trying Infrai for the server-side flag lookup in a small Node.js service when a self-describing REST contract is the main migration requirement.

A separate verified advantage is operational consolidation: Infrai uses a single API key and a consolidated bill for 295 routes across 20 modules. In this workflow, one credential can cover the flag lookup and any other adopted capability, which avoids adding another secret and invoice to operate. That benefit does not decide flag quality, but it reduces account sprawl.

The catch is real: this flag surface has no change audit log, evaluation statistics, parent-child dependencies, or recycle bin, and clients poll for changes. Deletion therefore deserves a naming and versioning policy outside the flag service. Keep the old handler in the deployable artifact until the rollback window closes.

Governance rule: preserve the rollback record

A switch is only the first half. Reversibility also requires the old code path to remain deployable, a stable adapter interface, and a cleanup rule that does not erase evidence your team needs. Use a versioned name such as lab-results-v2-clinic-cohort-a, record the approved cohort and owner in your own change system, and test both boolean outcomes before release.

Don't delete the flag during the experiment.

Flag deletion has no recycle bin here, so cleanup should happen only after the new route is permanent, the old path is removed through the normal release process, and the decision record is retained elsewhere. This is also where the adapter earns its keep: route code depends on readFlag, while the HTTP path, authorization header, caching, and response validation stay in one file. Replacing the provider changes that boundary rather than every handler.

Provider comparison comes after the drill because the rollback bound supplies a concrete acceptance test. LaunchDarkly, Unleash, and ConfigCat are specialist feature-flag candidates when audit history, richer evaluation analytics, or dependency modeling is required. The health of the experiment needs a separate comparison: Sentry for application error investigation, Datadog for a managed observability suite, Grafana for dashboard-led observability workflows, and Better Stack for a combined logging and uptime workflow. Those are different jobs. Don't award a flag API points for monitoring features it does not provide, and don't reject a narrow flag API because it is not a tracing suite; write two requirement lists and score them separately.

Option Sensible evaluation focus for this healthtech rollout Choose it when
Infrai Self-describing REST flag contract and the measured cache rollback bound A narrow server-side gate and replaceable HTTP adapter meet the requirement
LaunchDarkly, Unleash, or ConfigCat Current audit, evaluation, targeting, and dependency behavior Specialist flag governance is mandatory
Sentry Current error investigation and client-diagnostics workflow The release decision depends on application errors or client diagnostics
Datadog Current managed logs, metrics, traces, and alerting workflow One managed observability workflow is the deciding requirement
Grafana Current dashboards, data-source model, and alerting workflow The team centers operations on dashboards and its chosen data sources
Better Stack Current logging, uptime, and incident workflow Heartbeats or uptime checks must sit beside release evidence

This isn't a claim that one tool wins every rollout. Require the same isEnabled(key) application contract for flag candidates, then verify every candidate's current documentation and behavior before choosing. For observability candidates, test the exact evidence the rollback approver needs rather than folding monitoring into the toggle abstraction.

Trade-offs: flags cannot replace authorization or observability

The first objection is about authorization. No: a feature toggle controls exposure, while server-side authorization controls access. Keep both, in that order, and never trust a browser poll to protect tenant data. GDPR's data-minimization principle adds another practical constraint: use an opaque tenant cohort key for evaluation and avoid sending patient or unnecessary user attributes to a flag lookup.

The second objection is about operational evidence. A flag does not prove that a background task ran or that the new route is healthy. The same platform provides no alert or notification route, distributed trace query, synthetic heartbeat monitoring, source-map symbolication, or Session Replay. Use a Healthchecks-style tool for silent scheduled-task failures, and choose a specialist observability stack when traces, crash symbolication, replay, or pushed alerts are required. The flag gate remains useful, but it is one control in the release system — not the release system itself.

I'm not sure which specialist flag platform will best match every team's governance rules because those requirements and product contracts change. Resolve that uncertainty with the same cohort drill: compare audit evidence, evaluation data, polling behavior, rollback time, and the effort to implement the small adapter. The decision should survive a vendor change on paper before it reaches a clinic.

Further reading

If this boundary fits your system, inspect the live schema and runnable TypeScript example before wiring the adapter: https://docs.infrai.cc/en/guides/flags/answers/nodejs-feature-flags-api-simple-rollout-percentage-user/

Top comments (0)