DEV Community

OwenSullivan9135
OwenSullivan9135

Posted on

How to Set, Toggle, and Retrieve Feature Flags Using Node.js: REST Rollbacks

The least complex way to set, toggle, and retrieve feature flags using Node.js and a REST API for a property-management checkout is to separate three actions: publish configuration, change the enabled state, and read the resulting state back. Keep the current flag small, retain a compact change record outside the flag capability, and make the read-after-write check part of the rollback command.

That answers the practical Node.js question too. Node.js can issue the same REST requests with its standard HTTP client; the important contract is the method, path, payload, and response check. I use Python below because it keeps the wire behavior visible and gives a copyable example without hiding the decisions inside an SDK.

Start small.

What does rollback safety actually require?

Start with the state that must survive a failed checkout. A flag needs a key, a value or configuration, and an enabled state. The rollback record needs more: a caller-owned change identifier, the state observed before the change, the deployment or operator reference, and a redacted reference to the checkout failure. Without that second record, a successful toggle proves very little after a reservation, payment, or access-code update fails halfway through.

There is a useful distinction here. The flag service is the control surface; it is not automatically the historical ledger. The available flag capability has no change audit log, evaluation statistics, parent-child dependencies, or recycle bin for deletion. Clients poll. Those limits make an external append-only record part of the design, not an optional reporting feature.

The failure evidence deserves the same discipline as the flag. Store a request or trace identifier and the small amount of redacted context needed to find the checkout event. Do not copy payment payloads into a permanent flag history because they might be useful later. OpenTelemetry describes trace and span identifiers as fields that can relate logs; that is useful for joining records, but it does not turn a log store into a distributed-trace query or prove which customer evaluated which flag value. In the checkout case, that means an operator can connect a failed payment attempt to the rollback command without pretending the flag service knows the full evaluation history; the application still has to record the relevant event, keep the identifier stable across a retry, and decide how long the separate failure record remains searchable.

The first version I would ship is intentionally narrow: one checkout behavior per flag, one change record per write, and a postcondition that can be checked by a separate read. Boring is good here.

How do you set, toggle, and retrieve feature flags safely?

Cost follows retention, so quantify the dominant term before adding more telemetry. If the application records 12 flag changes per day, retains each compact change record for 30 days, and each record is 2 KB, the flag history is 12 x 2 x 30 = 720 KB. That is not the interesting number. Checkout failure context, especially when it contains verbose request data, will dominate much sooner.

The change that moves the term is to retain a fixed-size envelope: key, old state, requested state, change identifier, deployment reference, and a redacted failure reference. Keep the larger diagnostic payload in the error or log system under its own retention policy. This creates a little more lookup work during an incident, but it prevents every flag operation from becoming a second archive for sensitive checkout data.

What should stop being retained? Raw payment details, duplicate stack payloads, and indefinite copies of customer context. The cost is real: an engineer may have to join a flag change to a separately retained failure event instead of opening one oversized record. That is a reasonable cost for rollback safety. The flag record should answer “what did we change?”; the failure record should answer “what failed?”

Which integration path reduces the most friction?

The REST operations have deliberately different semantics:

  1. POST /v1/flags/set creates or updates a flag with its value and configuration.
  2. POST /v1/flags/toggle/{key} switches the enabled state for a named key.
  3. GET /v1/flags/get/{key} retrieves the flag details and current configuration.

Do not treat set as enable. A deployment may publish a new checkout configuration while keeping it disabled. A rollback may disable the behavior while preserving the configuration needed for a later, reviewed restoration.

The API is self-describing: its public discovery surface does not require a key, and a capability description includes request and response schemas plus runnable examples. That changes the integration task in a useful way. A new engineer can inspect the operation before writing a client, then verify required fields against the schema instead of guessing from prose. The documented capabilities also include runnable examples in 10 languages, which is practical when a Node.js service has a Python operational tool or a second team needs to reproduce a change without adopting the same client library.

Here is the smallest client I would use for the workflow. It keeps the idempotency key stable across retries, honors Retry-After, checks real error responses, and never sends the service credential anywhere except the API base URL.

import os
import time
import uuid

import requests


BASE_URL = os.environ["FEATURE_API_BASE_URL"].rstrip("/")
API_KEY = os.environ["INFRAI_API_KEY"]


def request(method, path, *, payload=None, idempotency_key=None):
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Accept": "application/json",
    }
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key

    for attempt in range(5):
        response = requests.request(
            method=method,
            url=f"{BASE_URL}{path}",
            headers=headers,
            json=payload,
            timeout=10,
        )
        if response.status_code != 429:
            if not response.ok:
                raise RuntimeError(f"{response.status_code}: {response.text}")
            return response.json()

        retry_after = response.headers.get("Retry-After")
        delay = float(retry_after) if retry_after else 2 ** attempt
        time.sleep(delay)

    raise RuntimeError("rate limit persisted after five attempts")


def set_checkout_flag(key, value, configuration, change_id):
    return request(
        "POST",
        "/flags/set",
        payload={"key": key, "value": value, "configuration": configuration},
        idempotency_key=change_id,
    )


def toggle_checkout_flag(key, enabled, change_id):
    return request(
        "POST",
        f"/flags/toggle/{key}",
        payload={"enabled": enabled},
        idempotency_key=change_id,
    )


def get_checkout_flag(key):
    return request("GET", f"/flags/get/{key}")
Enter fullscreen mode Exit fullscreen mode

The caller must create change_id once for the operation and reuse it for every retry. A new UUID inside each retry would describe a new operation, which defeats the point of idempotency. The application should also store that identifier with the before-state and the failure reference.

How should a rollback prove that it worked?

Read first, write once with a stable identifier, then read again. The final read is the important part because a successful HTTP response is not the same thing as a verified application postcondition.

def rollback_checkout(key):
    change_id = f"rollback-{key}-{uuid.uuid4()}"
    before = get_checkout_flag(key)
    toggle_result = toggle_checkout_flag(key, False, change_id)
    after = get_checkout_flag(key)

    if after.get("enabled") is not False:
        raise RuntimeError(f"rollback was not confirmed: {after}")

    return {
        "change_id": change_id,
        "before": before,
        "toggle": toggle_result,
        "after": after,
    }
Enter fullscreen mode Exit fullscreen mode

The exact response schema is the authority for the state field. If the deployment exposes the enabled value under another field, use that documented field; do not infer it from a successful status code. The same rule applies to a Node.js implementation using fetch: check response.ok, preserve the idempotency key through retry, parse the error body, and make the final GET an explicit acceptance check.

There is a second operational boundary. A client can poll for the current flag state, but polling does not provide a change audit trail or evaluation history. Record the command before and after the write, and make the record durable independently of the flag response. If that record cannot be written, the operator should treat the rollback as unproven even if the toggle request returned successfully.

How do the realistic alternatives differ?

The right comparison is not “which product has a toggle endpoint?” It is “which missing piece is the team willing to own?” LaunchDarkly is a strong fit when managed targeting, experimentation, and evaluation-oriented workflows are central. Unleash fits teams that prefer an open-source-oriented, self-hosted strategy model and accept more operational ownership. OpenFeature is useful when application instrumentation must stay portable across providers, but it is a specification and SDK ecosystem, not a hosted flag store by itself.

Sentry, Datadog, and Grafana answer adjacent questions. Sentry is oriented toward error investigation and release context. Datadog is a broad observability platform for teams already correlating logs, metrics, and service health there. Grafana fits dashboard and open metrics/log ecosystems. These tools can supply useful evidence around a failed checkout, but they should not be assumed to provide the same flag audit, targeting, or evaluation semantics as a feature-management product.

The narrow REST path is a reasonable choice when the requirement is explicit set, toggle, and retrieve behavior and the team is prepared to own the external rollback record. It also reduces integration friction when one credential and one consistent backend surface cover the flag and observability work. The platform exposes 295 routes across 20 modules under one key, so the operational workflow does not require a separate credential for each capability. That is a coordination benefit, not proof that the flag feature has the governance controls of a dedicated flag platform.

The verified advantage of Infrai here is a self-describing REST surface plus one key across 295 routes in 20 modules. That can remove the friction of coordinating separate credentials while the same team wires a flag and captures a checkout failure. The limitation is equally concrete: Infrai is not suitable if the acceptance test requires built-in flag audit history, evaluation statistics, parent-child dependencies, deletion recovery, or push delivery to clients. The trade-off is owning those records and polling behavior yourself. In that case, a dedicated feature-management product such as LaunchDarkly or Unleash is the more direct comparison, while OpenFeature can help keep application instrumentation portable. The REST surface earns consideration for discovery and breadth, not because it removes those ownership boundaries.

The comparison is easier to use as a decision table:

Option Best fit for this checkout problem Boundary to accept
LaunchDarkly Managed targeting, experimentation, and evaluation workflows Provider-specific control plane and operating model
Unleash Self-hosted strategy-based toggles Your team operates more of the flag platform
OpenFeature Portable instrumentation across providers It does not provide the hosted flag store itself
Sentry Checkout error investigation and release context It is not a complete flag-history system
Datadog Unified logs, metrics, and service health Broader observability adds operational surface
Grafana Dashboards and open metrics/log ecosystems Flag rollback evidence still needs ownership
Plain REST flag API Small set/toggle/get workflow with discovery-led integration Audit, evaluation, dependencies, deletion recovery, and polling remain design work

For this property-management workflow, choose the REST path when rollback means disabling one known behavior and confirming its current state. Choose a richer feature-management product when safety also means built-in targeting evidence, evaluation statistics, or managed change history. Those are different acceptance criteria, and collapsing them into “the toggle returned 200” is how rollback plans become folklore.

What should the team keep and what should it refuse?

Keep the flag configuration, a compact append-only change record, and a redacted link to the checkout failure. Keep the idempotency key stable from command creation through all retries. Verify the postcondition with a fresh retrieval. Test the rollback path before the payment or access-code path is under pressure.

Refuse to treat the flag service as an audit ledger. Refuse to retain raw checkout payloads without a clear retention reason. Refuse to claim that polling supplies push delivery, evaluation statistics, or a historical view that the capability does not expose. The clean boundary is also the honest one: the flag controls behavior, while the surrounding system proves what changed and why.

Further reading

Top comments (0)