How to Set, Toggle, and Retrieve Node.js Feature Flags via REST API (Rollback First)

Short answer: retrieve a revisioned flag before each scheduled import, set it with a reason, toggle it only with a conditional write, and fail closed when the REST response is unavailable. For a fintech import, that makes rollback safer than changing code while zero-result evidence is still being investigated.

A feature flag for a scheduled financial import should be a rollback control, not a remote light switch. Keep the decision local to the worker, record it with the import run, and fail closed when the response is missing or malformed. That gives a small team a reversible control: set a flag through REST, toggle it deliberately, retrieve it for inspection, and stop new work without changing the worker image.

The example uses a generic REST service and Node.js 20-compatible TypeScript. Endpoint names are placeholders.

What should the flag protect?

Imagine a fintech worker that starts at 02:00 UTC, fetches settlement files, validates rows, and writes an import result. The dangerous failure is a successful process that produces zero results because a source changed format, a credential points at an empty account, or the scheduler selected the wrong tenant.

The flag settlement_import_enabled controls whether a run may proceed. The worker still emits a run record when the flag is off, with feature_disabled as its reason. Silence looks like a late outage; an explicit skipped run is evidence. A useful flag document has a stable key, a boolean, a revision, and an update timestamp.

How do set, toggle, and retrieve work in Node.js?

Put the runnable example before the policy discussion. The write methods are separate from the read method so a production mutation is obvious in review.

type Flag = { key: string; enabled: boolean; revision: number; updatedAt: string }; const baseUrl = process.env.FLAG_SERVICE_URL ?? "https://flags.example.invalid"; async function request<T>(path: string, init?: RequestInit): Promise<T> { const response = await fetch(`${baseUrl}${path}`, { ...init, headers: { accept: "application/json", "content-type": "application/json" }, signal: AbortSignal.timeout(3_000) }); if (!response.ok) throw new Error(`Flag service returned ${response.status}`); return response.json() as Promise<T>; } export function retrieveFlag(key: string): Promise<Flag> { return request<Flag>(`/v1/flags/${encodeURIComponent(key)}`); } export function setFlag(key: string, enabled: boolean, reason: string) { return request<Flag>(`/v1/flags/${encodeURIComponent(key)}`, { method: "PATCH", body: JSON.stringify({ enabled, reason }) }); } export async function toggleFlag(key: string, reason: string) { const current = await retrieveFlag(key); return setFlag(key, !current.enabled, reason); } export async function startImport(): Promise<void> { const flag = await retrieveFlag("settlement_import_enabled"); if (!flag.enabled) { await recordRun({ status: "skipped", reason: "feature_disabled", flagRevision: flag.revision }); return; } await recordRun({ status: "started", reason: "feature_enabled", flagRevision: flag.revision }); await importSettlementFiles(); } type Run = { status: "skipped" | "started"; reason: string; flagRevision: number }; async function recordRun(run: Run): Promise<void> { console.log(JSON.stringify({ event: "settlement_import_run", ...run })); } async function importSettlementFiles(): Promise<void> { /* Fetch, validate, and persist files. */ } 

If retrieval times out, this sample throws. The scheduler can mark the run failed instead of quietly proceeding. A cached value is possible, but its age belongs in the run record; a stale true can be worse than a visible failure. In production, authenticate requests, validate JSON at runtime, and require an idempotency key on writes. TypeScript types do not validate bytes received over HTTP.

When should a zero-result alert trigger rollback?

Use import evidence, not flag state alone. Record runId, tenantId, flagKey, flagEnabled, flagRevision, sourceObjectCount, inputRowCount, acceptedRowCount, status, and durationMs. Keep the revision beside the business result, or an investigation cannot tell which state a run used.

For this example, alert after two consecutive scheduled runs are complete, enabled, and produce zero accepted rows while their source window is non-empty. One empty window can be legitimate. Two is a useful starting policy, not a universal truth; put the threshold in a test and a runbook. Rollback means setting settlement_import_enabled to false with a reason tied to the alert, then preserving the last enabled revision before re-enabling anything.

How should the REST contract support a safe toggle?

Retrieve should return a consistent representation. Set should return the resulting revision. Toggle is a read-then-write race, so production code should prefer a conditional set such as If-Match: revision-41 or an equivalent compare-and-swap field. If another operator changed the flag, the write should fail instead of reversing a newer decision.

The audit record needs the actor, reason, old revision, new revision, and correlation ID. The control service need not understand settlement logic; the worker must attach the flag result to its run event. OpenTelemetry documents logs as a signal that can correlate with traces and other telemetry. Here, runId, flagRevision, and reason are joinable; import skipped is not. Avoid file contents, account identifiers, tokens, and full error payloads.

There is a real trade-off here. A remote flag adds a network dependency and an operational control plane; an environment variable is simpler but needs a deployment to change. A cached decision improves availability but can keep a bad import enabled. For rollback safety, the extra request and the 3-second timeout are acceptable only when the worker records the failure and stops safely. This pattern is a poor fit for a one-shot script with no durable run record; use a reviewed deployment configuration there.

What should be tested before rollout?

Test transitions, not just HTTP status. Cover enabled, disabled, timeout, invalid response, stale revision, and service error. Missing enabled must never authorize an import.

import { strict as assert } from "node:assert"; test("disabled import records an intentional skip", () => { assert.deepEqual(decideImport({ enabled: false, revision: 42 }), { shouldRun: false, reason: "feature_disabled", flagRevision: 42 }); }); test("missing state does not authorize work", () => { assert.throws(() => decideImport(undefined), /flag state unavailable/); }); function decideImport(flag: { enabled: boolean; revision: number } | undefined) { if (!flag) throw new Error("flag state unavailable"); return flag.enabled ? { shouldRun: true, reason: "feature_enabled", flagRevision: flag.revision } : { shouldRun: false, reason: "feature_disabled", flagRevision: flag.revision }; } 

Run these tests against a fake REST server for each failure mode. Add an integration test proving that a disabled run performs no settlement writes; it catches a queued job that checked the flag only at startup.

References

The logging model follows OpenTelemetry's public logs concepts documentation. HTTP method semantics and conditional requests should be checked against the applicable HTTP specification. Before launch, name the flag owner, define its default, document the rollback command, and watch the next two runs after recovery.

A feature flag supplies the reversible control; observability explains what that control did. Keeping those responsibilities separate turns a zero-result alert into a bounded rollback.

Sources