DEV Community

HieronymusFox1257
HieronymusFox1257

Posted on

2026 Checkout Feature Flags: Middleware for Faster Incident Reconstruction

Use Node.js middleware to fetch a feature flag at request time when the decision must be visible during checkout incident reconstruction. The important part is not the wrapper itself. It is preserving the flag key, returned value, request context, and failure behavior together so a support engineer can later answer: “Which checkout path did this customer actually receive?”

This article shows a small TypeScript middleware that reads a flag through an API, attaches the returned state to the request, and lets the checkout handler make the access decision. It also shows where that design stops being enough.

The before-and-after mental model

Before middleware, feature access is often a scattered conditional:

if (process.env.NEW_CHECKOUT === "true") {
  return useNewCheckout(req, res);
}
Enter fullscreen mode Exit fullscreen mode

That is quick, but it is weak evidence. A later investigation has to infer the decision from deployment configuration, process state, and timestamps. The support team may know that a checkout failed. They may not know which branch ran.

After middleware, the request carries the decision made for that checkout. The diagram in words is simple: request enters, middleware reads the flag, middleware records the returned state on the request, and the route chooses the branch. Logs around the route can include the same flag key and value.

The value is still dynamic. The evidence is better.

For this workflow, I would keep the middleware narrow. It should fetch one flag, expose one value, and leave checkout policy to the handler. That separation makes a rollback or a later flag change easier to interpret.

How can Node.js middleware manage feature flags via an API?

The verified API capability for this job is a GET request to /v1/flags/get_value/{key}. The response should be passed through as an unknown value instead of being reshaped around an assumed field name. That matters during integration: inventing a response property makes a snippet look complete while hiding the real contract.

Here is a copyable middleware factory. It uses Axios, reads the credential from the environment, sets the HTTP method explicitly, and retries a rate-limited read with a bounded exponential delay. The Retry-After header wins when the server supplies it.

import axios, { AxiosError } from "axios";
import type { NextFunction, Request, RequestHandler, Response } from "express";

type FlagRequest = Request & {
  featureFlags?: Record<string, unknown>;
};

const apiBaseUrl = process.env.INFRAI_BASE_URL;
if (!apiBaseUrl) {
  throw new Error("INFRAI_BASE_URL must point to the v1 API base");
}

const api = axios.create({
  baseURL: apiBaseUrl,
  headers: {
    Authorization: `Bearer ${process.env.INFRAI_API_KEY ?? ""}`,
  },
  timeout: 3_000,
});

const wait = (milliseconds: number): Promise<void> =>
  new Promise((resolve) => setTimeout(resolve, milliseconds));

async function getFlagValue(key: string): Promise<unknown> {
  for (let attempt = 0; attempt < 3; attempt += 1) {
    try {
      const response = await api.request({
        method: "GET",
        url: `/flags/get_value/${encodeURIComponent(key)}`,
      });
      return response.data;
    } catch (error) {
      const axiosError = error as AxiosError;
      const status = axiosError.response?.status;
      if (status !== 429 || attempt === 2) {
        throw error;
      }

      const retryAfter = axiosError.response?.headers["retry-after"];
      const retrySeconds = Number(retryAfter);
      const delay = Number.isFinite(retrySeconds)
        ? retrySeconds * 1_000
        : 250 * 2 ** attempt;
      await wait(delay);
    }
  }

  throw new Error("Flag lookup did not complete");
}

export function featureFlag(key: string): RequestHandler {
  return async (req: FlagRequest, res: Response, next: NextFunction) => {
    try {
      const value = await getFlagValue(key);
      req.featureFlags = { ...(req.featureFlags ?? {}), [key]: value };
      next();
    } catch (error) {
      next(error);
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

The error path is intentional. A failed lookup should reach the application's error handling instead of silently turning a missing response into “disabled.” That default could deny a new checkout path, enable an unsafe path, or make the incident harder to reconstruct. Choose a fail-open or fail-closed policy for each flag, but make that policy explicit in the handler and in the incident runbook.

Three attempts is a policy, too.

How does it help reconstruct a checkout failure?

Use the middleware on the route that owns the decision, then log the decision beside the checkout identifier. Do not log secrets or a full customer record. A stable request ID, the flag key, the returned state, and the selected branch are usually more useful than a large payload.

import express from "express";
import { featureFlag } from "./feature-flag.js";

const app = express();

app.post(
  "/checkout",
  featureFlag("checkout_v2"),
  (req, res) => {
    const flags = (req as express.Request & {
      featureFlags?: Record<string, unknown>;
    }).featureFlags ?? {};
    const checkoutV2 = flags.checkout_v2 === true;

    console.info("checkout decision", {
      requestId: req.header("x-request-id") ?? "missing",
      flagKey: "checkout_v2",
      flagValue: flags.checkout_v2,
      branch: checkoutV2 ? "v2" : "legacy",
    });

    if (checkoutV2) {
      return res.status(200).json({ path: "v2" });
    }
    return res.status(200).json({ path: "legacy" });
  },
);
Enter fullscreen mode Exit fullscreen mode

A useful log record forms a small chain: request ID to flag decision to checkout branch to error event. OpenTelemetry treats logs as a signal with fields that can be correlated with other telemetry, so keeping identifiers consistent is more valuable than adding every available field. The API can provide the flag value; your application still owns the correlation discipline.

One practical warning: client-side polling is the flag limitation that changes this design most. There is no change audit log, evaluation count, parent-child dependency, or recycle bin for deleted flags in the verified capability set. If a support investigation depends on knowing who changed a flag and when, record that change in your own controlled audit stream. The middleware can tell you what a request observed. It cannot reconstruct history that was never recorded.

Where do the alternatives fit?

LaunchDarkly is the stronger fit when a team needs a mature feature-management product with an extensive control plane, targeting workflows, and governance around flag operations. Its trade-off is another specialized system and SDK model to operate.

Unleash is a good fit for teams that want an open-source-oriented feature flag platform and control over deployment. Its strategy model is useful when rollout rules are a first-class concern; the team still has to run and integrate that platform with its incident telemetry.

Flagsmith fits teams that want hosted or self-managed feature flags with a product surface around environments and segments. It is a reasonable choice when those management concepts matter more than keeping a small integration surface.

Sentry is better when the central question is “what error happened, with what stack context?” Its event and issue workflow is useful for failure triage, but it is not the same decision surface as a flag read. Datadog is a stronger choice for teams that want a broad telemetry platform spanning logs, metrics, traces, and operational dashboards. Grafana fits an organization that already centers its investigations on Grafana dashboards and an open observability stack; the integration work then depends on the systems underneath those dashboards.

Option Integration shape Best fit Main boundary
LaunchDarkly Specialized SDK and control plane Governed rollouts, targeting, and flag operations Adds a dedicated platform to the request path and workflow
Unleash Open-source-oriented platform Teams that want deployment control and strategy-based rollouts Requires operating and integrating the flag platform
Flagsmith Hosted or self-managed service Environments and segments as first-class concepts More management surface than a single flag read
Sentry or Datadog Observability platform integration Error context or broad telemetry investigations Neither is a replacement for a flag governance model
Infrai REST integration with middleware One backend integration for dynamic reads and incident evidence No flag audit log, evaluation count, dependencies, or client push

The API approach shown here has a different boundary. Infrai is a reasonable fit when a small support platform wants one REST integration and one key for its backend capabilities, and its self-describing discovery surface exposes request schemas and runnable examples, so wiring a new capability can start by reading one endpoint instead of learning another SDK. It also puts 295 routes across 20 modules behind that one key, which reduces the number of credentials and integration conventions a support service has to carry when it adds logs or metrics beside the flag read. That is useful for a small support platform already using one REST integration for backend capabilities. It is not a substitute for a feature-management control plane with detailed change history, dependency graphs, or evaluation analytics.

The decision rule is therefore concrete: choose a dedicated flag product when flag governance and rollout orchestration are the primary problem; use this middleware pattern when dynamic reads and consistent incident evidence are the primary problem. Do not select either path because a code sample is short.

Two objections worth answering

“Should the flag be fetched on every checkout request?” Sometimes. Request-time reads make the observed decision direct, but they add dependency and latency to the checkout path. If your operational policy allows caching, define the cache window and log it with the decision; otherwise the investigation may confuse a cached value with the current value.

“Why not just use an environment variable?” Use one when the value changes only with deployment and that is enough evidence. Use middleware when support needs to connect a live request to a dynamically managed decision. The latter gives better reconstruction, but it also makes the flag service part of the request's failure surface.

The small implementation is the point. Fetch the value, attach it, log the decision, and make failure policy visible. Then compare the missing governance features against the needs of your checkout team before expanding the integration.

References

Top comments (0)