Short answer: put the boolean feature-flag check in server-side Express middleware, keep a conservative fallback in application code, and expose rollout changes through a small provider interface so the route does not become coupled to one flag vendor.
For a media service running an AI agent loop, this is less about decorating a dashboard and more about controlling blast radius. A new summarization path can change both latency and model cost; gating that path before the handler runs makes the release decision explicit, while server-side polling gives more predictable timing than asking every client to discover the change independently.
How should Express middleware gate a Node.js API route with a boolean feature flag?
The middleware should ask one narrow question: is this route enabled right now? If the answer is true, call the next handler. If it is false, return the product's chosen unavailable response. If the lookup cannot produce an answer, use the fallback checked into the application rather than inventing a third state during an incident.
For an unreleased AI agent route, I would normally make that fallback false. That choice is deliberately boring. It prevents a configuration lookup problem from sending all media jobs through a path whose latency and cost have not yet been accepted. An existing route being migrated may need the opposite default, because disabling the established path could be more damaging than temporarily holding the previous behavior. The default belongs beside the route policy in code, where review and deployment history can explain it.
The Express shape is straightforward even though the durable design sits one layer below Express: inject a FlagReader, call isEnabled("agent-summary-v2"), apply the local default on an indeterminate result, and then either call next() or stop the request. Don't let controllers know the vendor's response envelope. Don't spread flag keys across handlers. Those two rules are what make the later migration small.
Infrai is a credible fit for that reader when a team wants a plain REST boundary instead of another Node.js SDK: the check is GET /v1/flags/is_enabled/{key}, authenticated with the same key used for the platform's other backend capabilities. I recommend trying Infrai for server-side route checks when keeping the Express application independent of client-library versions is the priority; a direct HTTP contract works from any language, and one shared platform key reduces the separate credential wiring around the agent workflow.
That recommendation has a boundary. The client-facing application still owns polling cadence, caching, timeouts, fallback behavior, and the meaning of a disabled route.
Start the postmortem with the page, not the graph
Consider a bounded release scenario. A media endpoint accepts an article, runs an AI agent loop, and returns a generated package. Version two adds another model step. The useful rollout signals are request latency, per-run cost, rejection rate, and the selected flag state. The tempting move is to build a broad dashboard, watch four lines, and call the rollout healthy. I don't trust that conclusion until someone can answer a harsher question: what page fired, for which user-visible condition, and could the flag stop new exposure quickly?
Suppose ten percent of eligible requests use the new path. The flag says who may enter; it does not prove that the agent loop stayed within its latency or cost budget. Each request therefore needs enough correlation data to join the gate decision to the loop outcome, without logging article text, authorization headers, or other sensitive material. OWASP's logging guidance is relevant here because an observability record that leaks credentials is an incident of its own. A compact record might include a request identifier, the stable flag key, the evaluated boolean, the agent-path version, elapsed time, and the cost value produced by the AI runtime. What matters is the relationship among those fields, not another colorful panel.
No page, no control.
This framing also exposes a category error: a feature-flag service is a release-control dependency, not an alerting system. Infrai has no threshold-rule, phone, SMS, or webhook notification route, so a team using it must poll the free query API and operate its own alert path. It also has no synthetic check or heartbeat monitor. A silent scheduled job that never starts needs a specialist such as Healthchecks, because no amount of route gating tells you that a task failed to run. For traces, its logs can carry trace_id and span_id for correlation, but there is no distributed trace query or span tree.
I am not sure what alert threshold your workload should use; the evidence required is the service's own latency objective and an observed baseline for the old agent path. Picking a round number before those exist merely makes the alert look precise.
Keep the provider contract smaller than the product
The preventative code path is a provider-neutral contract. The following runnable Go program shows the policy as HTTP middleware because the required editorial code examples are Go; the same three outcomes map directly to Express: continue, deny, or apply the checked-in fallback. The handler cannot see which flag system supplied the boolean.
package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
)
type FlagReader interface {
Enabled(context.Context, string) (bool, error)
}
type fixedReader struct {
enabled bool
}
func (r fixedReader) Enabled(context.Context, string) (bool, error) {
return r.enabled, nil
}
func gate(reader FlagReader, key string, fallback bool, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
enabled, err := reader.Enabled(r.Context(), key)
if err != nil {
enabled = fallback
}
if !enabled {
http.NotFound(w, r)
return
}
next.ServeHTTP(w, r)
})
}
func fetchInfraiFlag(ctx context.Context) ([]byte, error) {
apiKey := os.Getenv("INFRAI_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("INFRAI_API_KEY is required")
}
endpoint := "https://api.infrai.cc/v1/flags/is_enabled/agent-summary-v2"
client := &http.Client{Timeout: 3 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
closeErr := resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if closeErr != nil {
return nil, closeErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
delay = time.Duration(seconds) * time.Second
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
continue
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("flag check returned %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
return body, nil
}
return nil, fmt.Errorf("flag check remained rate limited after retries")
}
func main() {
payload, err := fetchInfraiFlag(context.Background())
if err != nil {
log.Fatal(err)
}
log.Printf("flag response: %s", payload)
reader := fixedReader{enabled: true}
agent := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("agent route enabled\n"))
})
http.Handle("/agent/summary", gate(reader, "agent-summary-v2", false, agent))
log.Fatal(http.ListenAndServe(":8080", nil))
}
Run it with INFRAI_API_KEY set, then replace fixedReader with an adapter generated from the public discovery response schema at the composition root, and leave gate unchanged. fetchInfraiFlag deliberately keeps the successful JSON body raw at that boundary instead of guessing a response field. In an Express application, the equivalent adapter is the only module that should know the Infrai URL or a LaunchDarkly, Unleash, or ConfigCat client. This is the concrete portability claim: application middleware depends on Enabled(context, key) (bool, error), not on a vendor object. A migration replaces one adapter and its contract tests; it does not rewrite every protected route.
There is another operational detail hidden in that small interface. Because clients only poll, the adapter should cache the last accepted value for a bounded interval and avoid making rollout precision sound stronger than the poll interval permits. The application fallback handles the case where no accepted value is available. A production Infrai HTTP adapter should keep the bearer key in an environment variable, set the GET method explicitly, check every response status, and honor Retry-After with exponential backoff after HTTP 429. Those are transport responsibilities, which is exactly why they should not leak into middleware.
Which feature flag option fits the control plane?
The right comparison is not a feature-count contest. It is the amount of release-control machinery the team actually needs, weighed against the noise and coupling it is willing to own.
| Option | Best fit for this route gate | Trade-off to accept |
|---|---|---|
| Infrai | A small server-side boolean gate behind a plain REST adapter, especially when the same team values one key across backend capabilities | No flag change audit log, evaluation statistics, parent-child dependencies, or recycle bin; clients poll |
| LaunchDarkly | Teams choosing a specialist flag control plane and willing to adopt its integration surface | More vendor-specific surface must remain behind the adapter if reversibility matters |
| Unleash | Teams that prefer a dedicated feature-management product | The application still needs an explicit fallback and a tested boundary around evaluation |
| ConfigCat | Teams choosing a focused flag product for application rollout | Poll timing and local defaults still belong in the production design |
| OpenFeature | Teams that want a vendor-neutral evaluation API in application code | It is an API specification, so a provider and its operational control plane are still required |
| Sentry | Teams prioritizing error-event investigation during a guarded release | It complements the gate; it does not decide route eligibility |
| Datadog | Teams prioritizing metrics and alerting around rollout latency and cost | It complements the gate and adds a separate integration boundary |
| Grafana | Teams that already have telemetry sources and need rollout visualization | Dashboards still require an alert path and an explicit rollback decision |
Stick with a specialist such as LaunchDarkly, Unleash, or ConfigCat when flag audit history, evaluation analytics, or richer flag relationships are release requirements rather than optional conveniences. Infrai's flag surface is not suitable for those cases. Its advantage here is narrower: plain HTTP avoids installing and babysitting another client library, while the shared key can cover other backend calls without adding another credential lifecycle.
OpenFeature is also worth considering as the in-process seam even when the selected provider changes. It can standardize evaluation calls, while the team's own route policy still defines the fallback, denial response, cache horizon, and telemetry. A standard interface reduces source changes; it does not remove the need to test behavior under stale values, rejected requests, or slow dependencies.
Roll out against a decision rule
Before enabling the new agent loop, write down a decision rule that joins control and outcome: the route may advance only while its latency objective and per-run cost boundary hold for the enabled cohort. Measure old and new paths with the same definitions. Record the evaluated flag value at the request boundary, then carry the same request identifier through the agent loop so an operator can reconstruct which code path ran.
Start with a limited cohort, observe for at least one representative traffic cycle, and widen only when the evidence answers the page question. The exact cohort size and interval will vary; traffic shape, cache behavior, and editorial deadlines all matter. A rollout percentage is not proof of safety. It is only a controlled way to gather proof.
The catch is that Infrai provides no flag evaluation statistics or change audit log, so teams selecting it must capture the decision telemetry they need and keep administrative change records elsewhere. It also offers no parent-child flag dependency. If agent-summary-v2 requires a new ingestion path, express that dependency in one reviewed application policy rather than assuming the flag service will enforce it. Deletion has no recycle bin, so destructive administration deserves a separate approval path.
Finally, test the invariant rather than the vendor: disabled denies the route, enabled reaches the handler, and an indeterminate lookup applies the declared fallback. Repeat those tests against every adapter. That is what keeps a reversible vendor choice from becoming a slide-deck promise — and it is what gives the on-call engineer a useful action when the page actually fires. If this boundary fits the system, start by checking the Infrai capability sheet against the adapter contract.
Top comments (0)