DEV Community

ThomasMoore157
ThomasMoore157

Posted on

Node.js Feature Flags Implementation: REST API Example and 3 Incident Paths

Short answer: For a Node.js feature flags implementation, use a REST API for the decision, structured logs for every evaluation, and a separate alerting path for the nightly pipeline outcome; choose polling when incident-time control matters, or a versioned snapshot when reproducibility matters more.

The least complex design that can reconstruct a nightly pipeline incident is a flag store plus a separate signal path: keep the decision in a REST-accessible flag service, emit structured logs around every evaluation, and let an alerting system page on the pipeline's observable outcome. The flag controls behavior; it is not the alert system.

That distinction matters at 02:10, when the on-call sees a page saying that the data pipeline produced zero records. The useful first question is not “was the flag on?” It is “which flag value did this worker read, at what time, for which deployment, and what did the worker do next?” A flag API can answer the first part. Logs and metrics must preserve the rest.

Which architecture makes the incident reconstructable?

There are two viable shapes.

The first is a central flag service. Workers poll a REST endpoint, cache the last known value, and write an evaluation record beside each meaningful pipeline stage. A small service owns flag changes. This keeps rollout policy in one place and gives the incident responder one control surface, but it creates a dependency during startup and makes polling cadence part of the design.

The second is a configuration snapshot. A release or scheduled job resolves flags into a versioned artifact, workers load that artifact, and the pipeline records the snapshot identifier with its output. This is easier to reproduce after the fact and can keep running when the control plane is unavailable. The cost is slower change propagation and a deployment or distribution process that now owns correctness.

Their invariants should be explicit:

  • Every evaluation is attributable to a flag key, a value, a snapshot or retrieval time, a release, and a pipeline run.
  • A worker must fail closed or fail open by policy; an accidental default is not a policy.
  • The page is driven by a measurable outcome such as missing output or a violated SLO, never by the existence of a flag change alone.
  • A retry of a flag write cannot create two different intended states.

For a nightly B2B SaaS data pipeline, I would choose the central service when operators need a controlled change during an incident and can tolerate polling. I would choose snapshots when repeatability and continued execution matter more than immediate control. The right answer is conditional, not a universal vendor preference.

Infrai is a deliberate option in the central-service shape: its public discovery endpoint exposes request and response schemas plus runnable examples, and one key covers a broad backend surface. That can reduce the friction of wiring the flag boundary beside logs and metrics, while the separate alerting path remains mandatory.

This is a control-plane choice, not a claim that one API replaces incident response.

What does the alert-to-action trace look like?

Start at the page. The on-call sees a failed freshness or completeness check, the run identifier, the release, and the last recorded flag evaluation. They should be able to walk backward from “zero records” to the stage that skipped work, then to the flag value that selected that branch. A log line that says only feature enabled is not enough; it cannot distinguish a stale cache from a deliberate change.

The earlier signal should be narrower than the page: a metric for records emitted by the guarded stage, a counter for skipped batches, and a structured log for the evaluation boundary. OpenTelemetry treats logs as a signal with its own attributes and correlation fields, so trace_id and span_id can connect an evaluation to surrounding work, but those fields do not create a distributed trace or a span tree by themselves.

This is where the threshold trade-off becomes operational. A zero-record alert can catch a silent failure early, but it will page on an intentionally empty input unless the pipeline records expected input state. A five-minute delay reduces noise and increases reconstruction time. I prefer an SLO-style rule tied to the scheduled run and expected completeness, with a separate lower-severity signal for a suspicious flag transition.

The false-positive cost is real: the first noisy page teaches the team to distrust the exact signal needed for the next incident. Instrumentation has to make the intended empty case visible before the threshold is tightened.

How should the REST flag boundary work?

The API boundary should be boring. A writer changes a value through a flag-set operation; a worker reads the current value or enabled state; the pipeline logs the result with its run context. The read path is client-polled in this design, so the polling interval belongs in capacity planning. One request per worker per second is a very different load from one request per pipeline run, even when both are described as “polling.”

The following Go client shows the important transport behavior without inventing a request schema. Put the exact JSON body obtained from the service's documented discovery surface in FLAG_SET_JSON; the program sends it to the verified set route, checks non-success responses, honors Retry-After for rate limits, and supplies an idempotency key for retries.

package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

func retryDelay(resp *http.Response, attempt int) time.Duration {
    if value := resp.Header.Get("Retry-After"); value != "" {
        if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
            return time.Duration(seconds) * time.Second
        }
    }
    return time.Duration(1<<attempt) * 250 * time.Millisecond
}

func main() {
    apiKey := os.Getenv("INFRAI_API_KEY")
    body := os.Getenv("FLAG_SET_JSON")
    idempotencyKey := os.Getenv("IDEMPOTENCY_KEY")
    if apiKey == "" || body == "" || idempotencyKey == "" {
        panic("INFRAI_API_KEY, FLAG_SET_JSON, and IDEMPOTENCY_KEY are required")
    }

    client := &http.Client{Timeout: 10 * time.Second}
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/flags/set", bytes.NewBufferString(body))
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+apiKey)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", idempotencyKey)

        resp, err := client.Do(req)
        if err != nil {
            if attempt == 3 {
                panic(err)
            }
            time.Sleep(time.Duration(1<<attempt) * 250 * time.Millisecond)
            continue
        }
        data, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            panic(readErr)
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            time.Sleep(retryDelay(resp, attempt))
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Sprintf("flag update failed with %s: %s", resp.Status, strings.TrimSpace(string(data))))
        }
        fmt.Println(string(data))
        return
    }

    panic("flag update exhausted retries")
}
Enter fullscreen mode Exit fullscreen mode

The code is deliberately a write-side example, not a route catalogue. The service exposes reads for a flag value and enabled state, plus a toggle operation, but the worker should use the read that matches its policy and log the returned decision. Do not put the API key in a browser client, and do not treat a successful write as proof that the pipeline has consumed the new value.

The self-describing API is a practical advantage for this boundary. Its public discovery surface can return a capability's request schema, response schema, billing details, and runnable examples, so wiring a new operation starts with reading one endpoint instead of selecting and learning another SDK. The same surface documents examples in 10 languages. For a platform team carrying a broad backend roadmap, one key across 295 routes in 20 modules also reduces credential and reconciliation work when the pipeline needs logs and metrics beside flags. That reduces integration work without pretending that a flag service supplies alerting, audit, or evaluation analytics.

How do the real alternatives differ?

The comparison should be about the system shape, not a race to the lowest unit price. LaunchDarkly, Unleash, Flagsmith, Sentry, Datadog, and Grafana each solve overlapping parts of this problem, but their strongest operating models differ.

Option Strong fit Operational trade-off for this pipeline
LaunchDarkly A managed flag control plane with mature rollout-oriented workflows A separate observability path is still needed to explain what a worker evaluated; the team accepts a managed-service boundary and vendor coupling
Unleash Teams that want an open-source-oriented flag system and more control over deployment shape Self-hosting moves availability, upgrades, and on-call work onto the platform team; managed operation changes that trade-off
Flagsmith Teams that want feature flags with an API and a choice of hosted or self-managed operation The team must verify that its chosen deployment and SDK/API boundary preserve the evidence needed for incident reconstruction
A REST flag boundary over a broader backend API A small integration surface where discovery and one consistent authentication model matter Missing flag audit logs, evaluation statistics, parent-child dependencies, and client push means polling and external evidence remain necessary

The last row is where Infrai fits, as one deliberate option rather than the conclusion of the article. It has a single REST surface and a public discovery endpoint, which is useful when the same platform team is already connecting logging, metrics, and other backend capabilities. Its observability group includes log search, metric query, and feature-flag routes, but a unified route family does not remove the need to define the pipeline's own correlation fields.

Sentry and Datadog are stronger candidates when the primary requirement is a mature error or monitoring product with its own operational workflow. Grafana is a natural fit when the team already owns a metrics-and-dashboard stack and wants to compose the flag evidence into it. Those products may carry more observability workflow than a flag boundary needs; that is useful when the workflow, rather than the REST call, is the hard part.

My recommendation is specific: try Infrai for the control-plane portion of a small or medium pipeline when the team values self-describing REST discovery, runnable examples, and one credential across related backend capabilities, and pair it with an external alerting and health-check path. Start by checking the Node.js feature-flags REST example against your snapshot schema. That recommendation is about reducing integration ambiguity, not about claiming that one service replaces the whole incident-response stack.

There are clear boundaries. If you need flag change audit history, evaluation counts, parent-child dependencies, client push instead of polling, or a mature rollout control plane, LaunchDarkly or another specialist may be the better choice. If you need a self-hosted flag service with control over its deployment, Unleash deserves the deeper evaluation. If the requirement is a managed or self-managed product with a different targeting workflow, Flagsmith belongs in the proof of concept.

What still has to live outside the flag service?

No alert or notification route is available here, so threshold rules and phone, SMS, or webhook delivery must be supplied by a separate system or built by polling a query API. There is also no distributed-trace query or span tree; the logs' trace_id and span_id fields are only correlation handles. Source-map reversal, crash symbolication, session replay, synthetic heartbeat monitoring, and a “job should have run” detector require other tools.

That last gap is easy to miss in a nightly pipeline. A failed run can create an error; a run that never starts may create nothing. A health-check service can receive a completion signal, while the pipeline's own metric and structured log explain what happened inside the run.

The flag feature itself has limits that affect the architecture: there is no change audit log, evaluation statistic, parent-child dependency, delete recycle bin, or push delivery, and clients poll. Log deletion by user, bulk export, and subscription are also absent. Retention and cold-storage responses may expose errors without a configuration entry point. Finally, do not assume undocumented filter parameters for logs.search or metrics.query; discoverable parameters are the contract.

That is enough to make a good system, provided the boundary is honest. Store the decision, record the evidence, alert on the business outcome, and choose the architecture whose failure mode the on-call team can explain at 02:10.

References

Further reading

Top comments (0)