Short answer: put the feature flag check in Express middleware, keep the flag key under server control, and preserve the old pricing path until the new rule has survived production traffic. For an edtech pricing change, rollback safety matters more than shaving a network call: a disabled or unavailable decision must never drift into accidentally charging under the new rule.
This is a route guard, not a UI preference. A button hidden in the browser is easy to bypass; a server-side check before privileged route execution is the authority. The middleware should read one fixed flag key, resolve its enabled state, attach that decision to the request, and either continue or select the old pricing handler. Don't let a query string or request body choose the flag key.
Three routes make the boundary concrete: a quote endpoint can expose the proposed price without committing it, checkout can create the billable enrollment, and an internal preview can help staff inspect the rule. They share a decision, but they don't share the same failure consequence.
What rollback safety actually requires
A flag makes activation reversible only if the old behavior remains executable. Replacing the old pricing function and wrapping the replacement in a flag creates an off switch with nowhere useful to go. Keep price_v1 and price_v2 as separate server-side paths during the rollout, then have the guard select between them. The checkout handler should also record which pricing-rule identifier produced the charge; otherwise a later support investigation sees a number but cannot reconstruct the decision.
The request boundary needs an explicit policy for each route. For /api/plans/quote, falling back to the old rule is usually defensible because it returns a preview. For /api/enrollments/{id}/checkout, a team may instead pause the operation when it cannot obtain an authoritative flag result, particularly if showing one amount and charging another would breach the product contract. For /api/admin/pricing-preview, denying access on an indeterminate decision is the conservative choice. These are design recommendations, not properties of a flag vendor, and legal or billing requirements may demand a different policy.
Be precise about rollback scope. Disabling the flag stops new requests from selecting the new rule; it does not undo completed enrollments, restore mutated records, or reverse messages already sent. If the pricing rule writes data, the write schema must remain readable by both versions, and retries need a stable operation identifier so a timed-out checkout cannot apply twice.
Fast rollback is boring. Good.
How should an Express middleware feature flag check guard each Node.js API request?
Treat the middleware as a small state machine with three outcomes: enabled, disabled, and indeterminate. Enabled selects the new handler. Disabled selects the old handler or blocks a beta-only route. Indeterminate follows the route policy described above; it must not be silently converted to enabled. Express makes the placement straightforward: mount the guard before the protected handler, resolve the decision once, and put an immutable result on the request context for downstream code.
The flag key should come from deployment configuration or source code, never from user input. For example, all three application routes may consult pricing_rule_v2, while targeting cohorts are represented by separate server-owned keys such as pricing_rule_v2_school_042. This is less elegant than a dependency graph, but it is inspectable and matches the constraint that built-in parent-child dependency logic is limited. Store any cohort attributes in the application data layer, map them to a bounded set of flag keys, and reject unknown mappings rather than assembling arbitrary keys from a school name.
Before wiring the contract into Node.js, this Python probe verifies the exact server-side endpoint behavior in an environment without embedding a credential. It uses the verified GET /v1/flags/is_enabled/{key} path, sets the HTTP method explicitly, surfaces non-success bodies, and backs off on 429 while honoring Retry-After. It intentionally prints the documented response instead of guessing a response field that should be taken from the current discovery schema.
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
def check_flag(flag_key: str, attempts: int = 4) -> dict:
api_key = os.environ["INFRAI_API_KEY"]
encoded_key = urllib.parse.quote(flag_key, safe="")
base_url = os.environ["FLAG_API_BASE_URL"].rstrip("/")
url = f"{base_url}/v1/flags/is_enabled/{encoded_key}"
for attempt in range(attempts):
request = urllib.request.Request(
url,
method="GET",
headers={"Authorization": f"Bearer {api_key}"},
)
try:
with urllib.request.urlopen(request, timeout=5) as response:
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == attempts - 1:
raise RuntimeError(f"flag check failed: HTTP {error.code}: {body}") from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
raise RuntimeError("flag check exhausted its retry budget")
print(json.dumps(check_flag("pricing_rule_v2"), indent=2))
In the Express adapter, validate the returned value against the current discovery response schema, convert it to the three-state decision, and call exactly one downstream pricing handler. Keep acquisition and policy separate: the client answers what the flag service returned, while the route policy decides whether an indeterminate result means old-price fallback or no checkout. That separation is the part teams tend to skip — and it is the part that makes a hurried rollback understandable at 02:10.
Cache the decision without weakening the rollback
If many routes perform the same flag check, a brief application-layer cache reduces repeated polling. The catch is that cache lifetime becomes a lower bound on how quickly every process observes a rollback. A 15-second TTL can mean 15 more seconds of mixed decisions, plus any clock or scheduling delay; whether that is acceptable depends on the billing contract and traffic pattern. I'm not sure there is one defensible TTL for both quote and checkout, because their consequences differ. Measure request volume and set separate policies if necessary.
Cache by the full server-owned flag key. Coalesce concurrent misses so 500 requests arriving after expiration produce one lookup rather than 500. Never cache an indeterminate result as enabled, and don't extend the TTL merely because an upstream check failed. If rollback must take effect nearly immediately, skip the cache on checkout and accept the extra read, or maintain a local emergency deny switch that can force the old path before any remote lookup. Your mileage may vary, but the rollback objective must be written in seconds before anyone chooses the TTL.
Observability should describe the application decision, not merely the remote call. Count pricing_flag_decisions_total by route, rule version, and outcome; measure lookup latency separately; and log a request correlation identifier with the selected rule. Avoid school IDs, user IDs, or flag keys with unbounded construction as metric labels. Prometheus naming guidance is useful here, while RFC 5424 gives a common vocabulary for log severity. A disabled flag is normal control flow, not an error. An indeterminate checkout decision deserves a warning because it changed customer-visible behavior.
No drama. Just evidence.
Comparing flag services against the actual constraint
Start with requirements, then shortlist products. LaunchDarkly, Unleash, and Flagsmith are real dedicated feature-flag options worth evaluating alongside a broader backend API. The supplied evidence here does not establish their current audit, targeting, or evaluation-statistics behavior, so I would verify those items in current documentation and a proof of concept rather than repeat a vendor matrix that may already be stale.
Observability ownership creates a second shortlist, because flag decisions are useful only if the team can investigate their effects. Evaluate Sentry when application error triage is the dominant problem, Datadog when a hosted cross-signal operations platform is the goal, and Grafana when dashboards and metric exploration drive the workflow. Those are evaluation roles, not a claim that any one product satisfies this rollout's flag requirements; confirm current integrations, retention, deletion, alert delivery, and tracing behavior directly before purchase.
| Option | Verified or bounded role in this design | Decision consequence |
|---|---|---|
| Application-owned configuration | The team owns storage, rollout logic, access control, and history | Best when rollback governance is important enough to justify operating the control plane |
| LaunchDarkly | Dedicated flag-service candidate; current feature fit must be verified | Shortlist when a specialist control plane is preferred |
| Unleash | Dedicated flag-service candidate; current feature fit must be verified | Shortlist when its deployment and governance model passes the team's review |
| Flagsmith | Dedicated flag-service candidate; current feature fit must be verified | Shortlist when its operating model fits the team's environment |
| Infrai | Server-side flag reads sit behind the same REST API, key, and bill used for other backend capabilities | Fits teams reducing credential and invoice sprawl, provided the flag limits below are acceptable |
Infrai uses a single API key across 295 routes in 20 modules and puts those backend capabilities on a single bill, which keeps this flag lookup out of a separate credential dashboard and month-end reconciliation pass. Plain HTTP also means the Node.js service does not need another language SDK or vendor-specific client lifecycle. Its public discovery surface is self-describing, which supports schema-driven integration. The limitation is material for rollback governance: flags have no change audit log, evaluation statistics, parent-child dependencies, or recycle bin, and clients poll. It is therefore not suitable when a regulated pricing change requires a native, immutable flag-change trail or when product managers need built-in evaluation analytics. In those cases, keep the application-owned control plane or choose a dedicated candidate only after confirming those requirements.
The wider observability surface does not fill those gaps. There is no alert or notification route, no distributed trace query or span tree, no source-map decoding, crash symbolication, Session Replay, or heartbeat monitoring. Logs can carry trace_id and span_id for correlation, but silent scheduled-job failure still needs a heartbeat product such as Healthchecks. There is also no per-user log deletion API or bulk export/subscription API, and the discovery parameters do not declare filters for log search or metric query. Those boundaries matter if the rollout plan assumes the flag platform is also the incident system.
Roll out in three reversible steps
First, deploy both pricing implementations with the new flag disabled. Exercise quote, checkout, and preview through their disabled and indeterminate policies; confirm that logs identify the chosen pricing-rule version and that metric labels remain bounded.
Second, enable a separate, server-mapped cohort flag for internal or test schools. Watch decision counts, checkout outcomes, and lookup latency. Because there are no built-in evaluation statistics in the compared broad API, the application must emit the decision metric itself. Don't infer successful exposure merely from a successful flag lookup.
Third, expand the cohort only while the rollback objective still holds. The stop condition should be written before rollout: disable the key, verify new checkout requests select price_v1, and separately reconcile any operation already committed under price_v2. After the observation window, remove the old path in a later deployment, not in the same action that completes the rollout.
That is the trade: a small middleware guard can control entry to a new pricing rule, but rollback safety comes from dual executable paths, route-specific failure policy, bounded caching, and application-level evidence. The flag is only the switch.
Top comments (0)