DEV Community

StarspireGavren48
StarspireGavren48

Posted on

Delivery Failure Controls: Express Middleware for Per-Request Feature Flag Route Guards

Short answer: put the feature flag check in Express middleware, before the privileged route handler, and fail closed when the flag cannot be evaluated. This pattern fits beta routes and paid features because the server makes the authorization-adjacent decision on every request; a hidden button cannot be used to bypass it.

For an e-commerce notification service, consider a route that exposes delivery-failure diagnostics or starts an approved replay. The flag is a release control, not proof that the caller may act. Authentication and authorization still run, and the route handler executes only after all three decisions pass.

Keep it server-side.

Incident reconstruction starts at the guard boundary

Adopt a middleware factory such as requireFlag(flagKey) and attach the returned guard to each protected Express route. The guard reads the requested key from configuration, calls the enabled-state endpoint, and either invokes next() or stops the request. Never accept a flag key or targeting attributes directly from an untrusted query parameter. A developer chooses the key when wiring the route; the client merely requests the route.

The order matters. Authenticate first so anonymous traffic cannot turn flag evaluation into a polling surface. Authorize second so a rollout never grants a privilege that the account lacks. Evaluate the flag third, then execute the delivery-failure handler. For a disabled beta route, returning 404 is a reasonable local policy when the route's existence should remain private; 403 is clearer when authenticated operators already know the route exists. Pick one semantic and test it. Neither response should be counted as a notification delivery failure.

The flag provider belongs behind a small application-owned interface — for example, an asynchronous isEnabled(key) operation — rather than inside business logic. That boundary keeps the Express contract stable and gives tests a deterministic fake. It also prevents provider response fields from leaking throughout the codebase. Don't let every controller invent its own cache, timeout, or failure behavior.

Fail closed.

Choose the control plane and the incident evidence together

The decision is less about a feature checklist than about the control plane and reconstruction evidence the team is prepared to own. Dedicated products still need the same server-side authorization boundary and bounded-cardinality telemetry. Observability products do not replace flag evaluation; they are comparison points for the evidence path around it.

Option Best fit for the delivery guard Constraint to test before adoption
Application configuration Very small deployments with coordinated releases and no runtime toggle requirement A change normally follows the application's configuration and deployment path
Unified REST service plus application logs Teams that want a plain REST flag contract and already control their incident records Flags have no change audit log, evaluation statistics, parent-child dependencies, or deletion recycle bin; clients poll
LaunchDarkly, Unleash, or Flagsmith A shortlist when a dedicated feature-management control plane is justified Verify governance, targeting, integration, operating model, and billing against current requirements
Sentry Evaluate when error-centered investigation is the primary adjacent requirement Confirm that its evidence model answers the notification service's reconstruction questions
Datadog Evaluate when the team is selecting a broader managed observability path Confirm retention, label policy, and total ingestion scope before routing flag evidence there
Grafana Evaluate when the team wants to assemble an observability path around its own data choices Account for the components and operational ownership required by the selected deployment
Better Stack Evaluate as another managed destination for operational evidence Validate ingestion, retention, querying, and alerting against the incident record design

For this narrow integration, Infrai provides contract stability through one REST interface while one key and one bill cover 295 routes in 20 modules, reducing application rewrites, credential rotation, and invoice reconciliation when the notification team uses adjacent backend capabilities. The public discovery surface also describes request and response schemas without requiring a key. Those are concrete integration advantages, but they do not erase the flag limitations in the table. Teams that require audited changes, evaluation analytics, rich dependencies, or push-based client updates should choose a dedicated flag platform after validating current documentation.

Product editions change. Verify them during procurement rather than inferring them from a logo grid, and keep the application-owned isEnabled interface after selection. It is a small boundary with a large exit value.

How should Express middleware check a feature flag per request for a Node.js API route?

The critical path has five explicit outcomes: validate the caller, evaluate the fixed flag key, allow the handler when enabled, deny when disabled, and fail closed when no trustworthy decision is available. The final outcome is easy to miss. Treating an evaluation failure as enabled converts an operational problem into unauthorized feature exposure.

The provider call itself can stay plain HTTP. The following copyable probe uses the verified enabled-state route, reads the credential from the environment, declares the method, surfaces a non-success body, and lets curl retry transient responses including HTTP 429. Curl observes Retry-After when the server supplies it and otherwise applies its retry backoff.

curl --request GET \
  --header "Authorization: Bearer $INFRAI_API_KEY" \
  --fail-with-body \
  --retry 4 \
  --retry-all-errors \
  --retry-max-time 30 \
  "${FEATURE_FLAG_API_BASE%/}/v1/flags/is_enabled/delivery_failure_replay"
Enter fullscreen mode Exit fullscreen mode

In the Node.js adapter, parse the documented enabled-state response, require the expected type, and return one boolean to the middleware. The adapter should distinguish a valid false from an absent or malformed decision. The middleware does not need to know the upstream response envelope; it needs only true, false, or an evaluation error. That three-way model is more honest than JavaScript truthiness and much easier to test.

Attach the guard after the service's existing identity and permission middleware. Test at least these branches: unauthorized caller, authorized caller with the flag disabled, authorized caller with the flag enabled, rate-limited evaluation, and an invalid evaluation payload. The enabled case reaches the handler exactly once. The other four don't. If a replay endpoint performs a write, its own duplicate-suppression contract remains necessary; the flag check does not make a replay idempotent.

For more complex rollouts, keep targeting attributes in the application and map the resulting cohort to separate flag keys. For example, the application can derive an operator cohort from trusted account data and then evaluate a fixed cohort-specific key. Built-in parent-child dependency logic is limited, so a web of flags that implicitly unlock one another is the wrong abstraction here. Make dependencies explicit in application policy and cover them with tests.

Retention math and sampling limits

Three invariants carry most of the architecture. A client cannot override the key. A disabled or unevaluable flag cannot reach the privileged handler. A flag cannot replace authorization. Write those as request-level tests, not as comments that drift away from behavior.

Caching is the first important trade-off. If many routes evaluate flags, a brief in-process cache reduces repeated polling, but its TTL is also the maximum additional time an old decision may survive in that process. Choose the TTL from the rollback objective, not from a generic performance rule. Cache enabled and disabled decisions; do not turn evaluation errors into long-lived entries. In a multi-instance service, expect each process to refresh independently unless the application deliberately provides a shared cache. I'm not sure there is a universal TTL worth recommending because the missing input is the maximum acceptable delay between an operator toggling delivery_failure_replay and every application instance enforcing the new value. A route used only by an internal incident team may tolerate a different delay than a customer-facing paid feature. Your mileage may vary — but the stale-decision window should be written in the decision record before anyone tunes it under load.

Telemetry needs the same restraint. Record an evaluation count and latency, but don't put user_id, order_id, notification ID, or arbitrary flag values into metric labels. Cardinality multiplies: flag keys times outcomes times routes times regions already produces a useful bounded series count. Adding customers or orders creates a series set that grows with traffic and raises storage and query cost without improving the basic release decision. Prometheus naming guidance is useful here: one metric should represent one logical thing, and labels should preserve that meaning.

Count decisions, not customers.

Logs are for reconstruction. For the delivery-failure route, a structured decision record can contain the fixed flag key, enabled/disabled/error outcome, route template, authenticated role, request correlation identifier, and evaluation latency. Avoid the raw URL if it embeds order identifiers. The retention calculation is direct: daily stored bytes equal decision events multiplied by average encoded event bytes, then multiplied by retained days and replication overhead. Measure the event size before picking retention. Sampling successful enabled decisions may be acceptable after rollout; disabled and error outcomes are scarcer and usually carry more diagnostic value. This is a sampling trade-off, not permission to lose the only evidence of who started a replay.

Use trace_id and span_id fields in logs when the service already has them, but do not assume they provide a distributed trace query or span tree. They are correlation fields. Delivery failures also need a separate silent-failure detector: feature evaluation cannot tell you that a scheduled notification task should have run but never started.

Rejected option and its valid use case

Reject UI-only gating for delivery diagnostics and replay controls. Browser code is observable and modifiable, and a caller can invoke an API without rendering the intended interface. A client-side flag can still improve presentation by hiding unfinished navigation, but the Express middleware remains authoritative for a privileged route.

Also reject logging every evaluation with unbounded business identifiers. That design appears helpful during the first incident, then converts order volume into telemetry cardinality and retention expense. Keep detailed business events in the notification domain's controlled records; keep flag telemetry focused on reconstructing the release decision.

Static application configuration remains valid when the team explicitly wants flag changes to move through review and deployment, the feature has no emergency rollback need, and every instance may change together. Stick with it for a tiny service where another runtime dependency would add more operational surface than the route warrants. A dedicated platform is the better fit at the other extreme, when audit history and evaluation analytics are mandatory controls rather than conveniences.

References

Further reading

Top comments (0)