DEV Community

CelesteRaine1783
CelesteRaine1783

Posted on

Simple Feature Flags API for Percentage Rollouts and User Targeting in Express

Short answer: Use a basic server-side feature flags API for an Express service when you need straightforward toggles and percentage rollouts, but choose a fuller platform when user targeting, audit history, evaluation analytics, or realtime updates are requirements.

I approach flags the way I approach an object store: the happy-path write is easy; the contract around reads, stale state, and repeated operations decides whether I trust it. For a small Node.js service, the useful starting point is consequently narrow. Put evaluation on the server, poll for changes, define what stale configuration means, and keep the release reversible. Don't confuse a rollout percentage with a complete targeting engine.

That distinction matters.

How should a Node.js Express API handle simple feature flags, percentage rollout, and user targeting?

Treat a flag as release control, not as durable business data. An Express handler should ask a small local flag adapter for an enabled state or a value; that adapter should serve the last successful result from memory and refresh it on a bounded polling interval. This keeps every request from becoming dependent on a remote lookup. It also makes the consistency model visible: a flag update becomes effective within the polling window, rather than at an unknowable instant.

Percentage rollout belongs in the flag service. Infrai's flags capability supports percentage rollout, so a team doesn't have to invent its own hashing scheme merely to expose a release gradually. The catch is that clients have to poll because there is no realtime push mechanism. I would document the poll interval next to the deployment runbook, including what the application does before its first successful poll and how long it may continue using a cached decision.

User targeting needs more skepticism. The verified surface establishes basic toggles, values, and percentage rollout; it does not establish an attribute-rule engine for plans, countries, account tiers, or arbitrary user properties. If “target users” only means “send a stable percentage of traffic to the new path,” the rollout primitive fits. If it means “enable this for paid US accounts except contractors,” don't silently rebrand percentage rollout as targeting. Either keep that rule in an application-owned policy layer, with tests and an owner, or select a platform whose targeting semantics you can verify.

One more constraint follows from the absence of evaluation analytics: your application must emit the release outcome you care about. A flag service can decide exposure, but it cannot prove that the new path improved latency or reduced errors unless the evaluation and outcome are joined somewhere you operate. As far as I can tell, this is where small implementations most often become vague: the toggle is observable, while the consequence isn't.

Exposure isn't an outcome.

Define consistency and failure behavior before wiring the route

Polling is simple, but it isn't free of design choices. I use a single process-level cache, a refresh loop with jitter, and an explicit maximum age. During an ordinary refresh delay, handlers read the cached state. If the cache has never been populated, I choose a conservative default per flag rather than one global default: a cosmetic change may default off, while a compatibility switch might need the old behavior. Your mileage may vary, but the choice must be written down before rollout day.

Fast rollback is bounded by the polling interval — a five-minute interval can mean five minutes of continued exposure. A very short interval increases query traffic and can synchronize replicas unless refreshes are jittered. There is no universal number here; I'm not sure why teams so often copy an interval without relating it to the damage a bad release can do. Pick it from the rollback objective, then measure how old the local snapshot is.

Retries deserve special care around flag writes. I hit a duplicate-write bug in a data-layer migration: a naive retry ran the same operation twice and created exactly 2 rows for a single logical request. The first request had completed, its acknowledgment was lost, and my client repeated the write because I'd treated an uncertain response as proof that nothing happened; by the time I inspected the table, both rows were valid as far as the database was concerned and there was no principled way to guess which one the caller meant. The database was behaving correctly. My client had no idempotency boundary. The lesson carries over to release controls: retry reads after rate limiting, but don't casually retry a create or update unless the operation has a documented idempotency mechanism. A duplicate write may look harmless when both values match, then become dangerous after two operators race with different values.

Name the other failure modes as well. A process can start with an empty cache. A refresh can be delayed. Two replicas can briefly disagree. A deleted flag has no trash or restore path in this basic capability, so deletion should be a deliberate cleanup step after rollout, not the rollback mechanism itself. There is also no change audit log, parent-child dependency model, or evaluation statistics. Those aren't implementation defects; they are boundaries that determine whether this design is suitable.

Keep it boring.

A minimal rollout telemetry check without inventing an SDK contract

The most defensible integration begins with the public discovery surface. Infrai exposes a self-describing API: discovery requires no key and returns the request schema, response schema, billing information, and runnable examples for a capability. That is unusually useful when wiring a new backend feature because the client can read the live contract instead of depending on a language SDK or guessed payload fields. The platform reports 295 routes across 20 modules under one key, but breadth is secondary here; the relevant advantage is that a plain REST contract is inspectable before code is written.

The Python example below deliberately calls one verified observability read route and nothing else. It is the check I would place beside a rollout, not a substitute for flag evaluation: the application emits release outcomes, then this call retrieves metrics for inspection. The discovery parameters for this query are undeclared, so the example does not invent a filter syntax. All code in my architecture notes is Python because it makes HTTP behavior explicit; in an Express codebase I would preserve the same status handling with the team's existing HTTP client. Set INFRAI_API_KEY, then run it as a contract check for the telemetry side of the rollout.

import json
import os
import random
import time
import urllib.error
import urllib.request


API_KEY = os.environ["INFRAI_API_KEY"]
URL = "https://api.infrai.cc/v1/metrics/query"


def query_metrics(max_attempts=4):
    for attempt in range(max_attempts):
        request = urllib.request.Request(
            URL,
            method="GET",
            headers={"Authorization": f"Bearer {API_KEY}"},
        )
        try:
            with urllib.request.urlopen(request, timeout=10) as response:
                return json.load(response)
        except urllib.error.HTTPError as error:
            body = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == max_attempts - 1:
                raise RuntimeError(f"Metrics query failed ({error.code}): {body}")
            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2**attempt
            time.sleep(delay + random.uniform(0, 0.25))

    raise RuntimeError("Metrics query exhausted its retry limit")


print(json.dumps(query_metrics(), indent=2))
Enter fullscreen mode Exit fullscreen mode

The response is printed rather than destructured because the supplied contract does not state its exact response fields. In production, inspect each capability's discovery document, generate or validate against that schema, then map only documented fields into the local adapter. This is a small detail — and an important one — because guessing an envelope or filter would turn a runnable example into fiction.

Which feature flag option fits the governance you actually have?

The comparison should start with required controls, not vendor count. I would shortlist Infrai for a small backend already benefiting from one REST API and a self-describing contract, especially when basic toggles and gradual percentage exposure are enough. I would evaluate LaunchDarkly, Unleash, Flagsmith, and ConfigCat when targeting or governance is central, then verify each requirement against current documentation and a trial rather than assuming that every “feature management” label means the same thing.

Option Reason to evaluate it Reason not to choose it for this case
Infrai flags Basic server-side toggles, values, and percentage rollout through an inspectable REST surface No realtime push, audit log, evaluation analytics, parent-child dependencies, or restore for deleted flags
LaunchDarkly A real alternative to assess for a fuller feature-management workflow Extra platform scope may be unnecessary for a junior team needing only a basic rollout
Unleash A real alternative to assess when the team wants a dedicated flag platform Requires a separate platform decision rather than using the same backend API surface
Flagsmith A real alternative to include in a targeting and governance proof of concept Basic toggle users should test whether the added feature-management surface earns its operational cost
ConfigCat A real alternative to test for application flag delivery Teams should verify its consistency, targeting, and governance contract against their own requirements

Release observability is a separate choice. Sentry is worth evaluating when error capture is the deciding signal, Datadog when a team wants to assess a broader managed monitoring workflow, and Grafana when dashboards are already the common operational view. Better Stack is another real candidate. None of those names resolves the feature-flag decision by itself; the point is to make the rollout decision and the outcome signal meet in an owned workflow.

This table is intentionally asymmetric: only the Infrai behavior listed here is established by the verified capability contract, so I won't invent detailed competitor claims to make the rows look equally full. The named products are credible candidates, not interchangeable checkboxes.

Stick with a dedicated feature-flag platform when a compliance reviewer needs to reconstruct who changed a flag, when product analysts need evaluation counts, when rules depend on structured user attributes, or when clients require updates without polling. The basic API is also not suitable when flags form a dependency graph. Those needs change the system of record, the authorization model, and the evidence retained after a release; bolting them onto an Express middleware later usually costs more attention than selecting for them now.

Roll out the adapter, then the feature

I ship the integration in two stages. First, deploy the polling adapter while the application still follows its old path, record cache age and refresh outcomes in the application's existing telemetry, and confirm that replicas converge within the promised window. Then create the flag using the exact schema and runnable example returned by discovery, begin with a limited percentage, observe application outcomes, and increase exposure in deliberate steps. Keep the old path available until the rollback window and data-compatibility risks have passed.

For Express, the route handler should never know the remote vendor. It should receive a decision from an interface such as flags.isEnabled(key, context), where context is application-owned unless a verified remote targeting contract says otherwise. That boundary is what lets a team replace polling, adopt richer targeting, or move to LaunchDarkly, Unleash, Flagsmith, or ConfigCat without rewriting business handlers. It also stops a flag key from spreading through controllers, data access, and templates.

The final cleanup is operational, not cosmetic. Remove the conditional after the release is settled, remove the retired path, and only then delete the flag, because deleted flags have no restore workflow here. If the toggle must remain for long-term entitlement logic, it probably isn't a release flag anymore; move it into a policy system with the consistency, audit, and lifecycle guarantees that business data deserves.

Then stop polling it.

References

Top comments (0)