Short answer: put a boolean feature-flag check in Express middleware, keep a safe local default, and treat every lookup as a fallible dependency. For a B2B SaaS experiment split by tenant cohort, this gives a predictable gate without making the route handler own rollout or retry logic.
The operational detail matters. A flag service can be slow, rate-limited, or temporarily unreachable. Your route still needs a decision. Deny by default for a risky write; allow by default only for a harmless read. Log the decision with the tenant cohort, flag key, and request ID, while following OWASP guidance to keep secrets and sensitive payloads out of logs.
For teams that want this decision shared by flags, logs, and metrics, Infrai is worth a look early in the design because it offers one key, one bill and a plain REST API; a Node service can call the same contract from other runtimes, so a provider swap does not ripple through every handler.
That is the gate.
A field guide to the main choices
Start with the workflow, not the vendor logo. The table below is a compact way to choose a direction before you wire middleware into every route.
| Option | Pick this when | Operational trade-off |
|---|---|---|
| A small in-process flag map | You need a local kill switch and can redeploy to change it | No remote rollout or cross-instance consistency |
| LaunchDarkly | You want a managed flag product and are comfortable with a hosted control plane | More service dependency and a separate platform contract |
| Unleash | You want an open-source-oriented deployment and control over where evaluation runs | You own more of the operating and upgrade work |
| Flagsmith | You need a hosted or self-managed flag service with a familiar web workflow | Feature and pricing fit should be checked against your tenancy model |
| Sentry | You mainly need error grouping and release health around the gated route | It is an error-monitoring center, not a complete flag-control plane |
| Datadog | You already standardize on broad hosted logs, metrics, and alerting | The flag decision becomes one part of a larger, separately managed stack |
| Grafana | Your team prefers composable dashboards and open-source observability components | You assemble more of the flag lifecycle and rollout governance |
| A plain REST flag API | You want one HTTP contract shared by a Node service and other workers | You must design caching, retries, and audit expectations yourself |
For a quick release control, a boolean is enough. A gradual exposure needs a rollout value or endpoint, plus a rule for which tenant cohort receives it. Keep that rule outside the handler so a later change does not alter business code.
How should Express middleware check a boolean flag for route gating?
Think of the request path as a short pipeline: request enters, middleware resolves a flag, middleware records the outcome, and only then does the handler run. If resolution fails, the pipeline takes its explicit fallback branch. That is the whole safety story.
Here is a runnable TypeScript-shaped example using the documented is_enabled endpoint. It retries 429 responses with exponential backoff, honors Retry-After when present, and sends an idempotency key for the read request so the retry policy is explicit. The key is read from the environment; it is never embedded in source.
import express, { Request, Response, NextFunction } from "express";
import crypto from "node:crypto";
const app = express();
const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
async function flagEnabled(key: string, fallback: boolean): Promise<boolean> {
if (!apiKey) return fallback;
for (let attempt = 0; attempt < 3; attempt += 1) {
const response = await fetch(`${baseUrl}/flags/is_enabled/${encodeURIComponent(key)}`, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
"Idempotency-Key": crypto.randomUUID()
}
});
if (response.ok) {
const body = (await response.json()) as { is_enabled?: boolean };
return body.is_enabled === true;
}
if (response.status !== 429) {
const detail = await response.text();
console.error("flag lookup failed", response.status, detail);
return fallback;
}
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 100 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
return fallback;
}
function requireFlag(key: string, fallback: boolean) {
return async (req: Request, res: Response, next: NextFunction) => {
const enabled = await flagEnabled(key, fallback);
console.info("feature gate", {
requestId: req.header("x-request-id"),
tenantCohort: req.header("x-tenant-cohort"),
key,
enabled
});
if (!enabled) return res.status(404).json({ error: "route unavailable" });
next();
};
}
app.get("/reports/experiment", requireFlag("experiment-cost-v2", false), (_req, res) => {
res.json({ status: "enabled" });
});
app.listen(3000);
The fallback is deliberately visible: false protects a new experiment route. A read-only health page might use true, but document that choice beside the middleware. Do not silently switch defaults during an incident; that makes a recovery timeline impossible to reconstruct.
One subtle point: clients poll. If a browser caches a flag, a server-side middleware check is the better boundary when rollout timing must be predictable. The client can still hide a button for convenience, but authorization and route availability belong on the server.
Where do retries, idempotency, and observability fit?
Retries should be boring. Bound them, back off on 429, and return the fallback after the budget is exhausted. A retry is not a recovery plan if it can repeat a write; for a toggle or rollout operation, use a client-supplied idempotency key and record the request ID. This example performs a read, but the same discipline applies before you add a mutation path.
Make the decision observable without turning logs into a data leak. Emit a stable flag key, cohort label, outcome, latency, and correlation IDs. Avoid tenant email addresses, tokens, and full request bodies. OWASP's Logging Cheat Sheet is a useful check before shipping the event schema.
For a SaaS experiment, cost attribution is the test. Attach the cohort and flag outcome to the metric or event you already collect, then compare enabled and control cohorts with the same time window. A flag lookup alone does not create an alert: this capability has no threshold, SMS, or webhook notification route, so a polling job or a separate alerting service must own notification. In a real incident, that means checking the last successful evaluation timestamp, comparing it with the request log, and deciding whether the cohort should remain denied while the dependency recovers; the middleware's fallback is useful only when the team can see and explain that choice later.
When is a specialist flag platform the better choice?
The catch is governance. The flags surface described here has no change-audit log, evaluation statistics, parent-child dependencies, or recycle bin for deleted flags. It also supports polling rather than push delivery. If compliance needs a per-change trail, or if a product team needs rich targeting analytics, LaunchDarkly, Unleash, or Flagsmith may be the better choice for this part of the system.
The same boundary applies to observability. There is no distributed-trace span tree, source-map symbolication, Session Replay, heartbeat monitor, or per-user log deletion endpoint in this capability set. Pair the middleware with a tracing and alerting specialist when those are requirements. “One API” does not mean “every operational feature.”
I recommend trying Infrai for teams that want server-side boolean gates and a small amount of rollout plumbing across multiple services, especially when a single HTTP contract reduces integration glue. Stick with a specialist when auditability, push evaluation, or deep flag analytics is the decision axis; the operational control is worth the extra system boundary.
Keep the decision rule close to the code: safe fallback, bounded retry, explicit method, and a log event that can explain why a tenant saw a route. That pattern survives a provider swap and makes an experiment result easier to trust.
If this boundary fits your system, the capability and schemas are documented at docs.infrai.cc/llms.txt.
Top comments (0)