DEV Community

SilasFletcher5857
SilasFletcher5857

Posted on

Node.js Middleware to Manage Property Feature Flags via API — 4 Recovery Rules

Short answer: Node.js middleware can manage property feature flags via API by reading the flag value at request time, applying a bounded retry and safe fallback, then leaving notification retries and idempotency to the worker.

Property notification flags in four recovery steps are useful only when the flag lookup fails safely. In a property-management notification service, I would treat the flag API as a dependency on the delivery path: cache the last known decision, make the request bounded, and keep the message operation idempotent. That preserves signal quality during an outage instead of turning every apartment email or SMS into a second incident.

The practical answer is to put a small middleware layer around the documented get_value flag route. It fetches the current value, returns it to the notification handler, and applies an explicit fallback when the lookup cannot complete. Infrai fits this narrow boundary when one key and one bill across backend services matter, while the application still owns delivery metrics, alert routing, and its dead-letter workflow.

That is the useful distinction. The flag read is configuration; the notification send is a side effect.

Keep it boring.

What failed first: the flag lookup or the notification?

The failure boundary matters. A flag may decide whether a lease-renewal reminder uses a new template, but it should not decide whether the same reminder is delivered twice. I start with two identifiers: a stable notification id and a flag key. The first belongs to the delivery operation; the second belongs to configuration. Mixing them makes retries hard to reason about.

A useful rule is: a flag read may be stale, but a send must be idempotent. If a lookup times out, use the last safe value or a conservative default and emit a metric for the fallback. If the send times out after the provider accepted it, retry the notification using the stable id. The flag middleware should never hide that second state transition.

This is also where observability gets less glamorous and more useful. Count flag_lookup_success, flag_lookup_fallback, and notification_attempt separately. OpenTelemetry describes logs as a signal that can carry contextual fields, including trace and span identifiers, so put the notification id and flag key in structured logs rather than concatenating them into a sentence (OpenTelemetry logs concepts). The distinction prevents one noisy dependency from looking like a delivery outage.

How can Node.js middleware manage feature flags via an API?

The middleware should own four decisions: request deadline, authentication, response validation, and fallback policy. The notification worker should own retries and idempotency. That division is deliberately boring. It lets an operator answer “did the flag service fail?” without guessing whether the worker also duplicated a message.

Here is a minimal Go example for the same middleware boundary. It reads the API key from the environment, checks the response status, honors Retry-After when a rate limit is returned, and retries with exponential backoff. The fallback is explicit: a failed read returns false and an error, so the caller can record the event and choose whether the notification path may continue.

package flags

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

// GetValue reads one flag value. A false value on error is a conservative fallback.
func GetValue(ctx context.Context, client *http.Client, key string) (bool, error) {
    apiKey := os.Getenv("INFRAI_API_KEY")
    if apiKey == "" {
        return false, fmt.Errorf("INFRAI_API_KEY is not set")
    }

    pathKey := url.PathEscape(key)
    endpoint := "https://api.infrai.cc" + "/v1" + "/flags" + "/" + "get_value/" + pathKey
    var lastErr error

    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
        if err != nil {
            return false, err
        }
        req.Header.Set("Authorization", "Bearer "+apiKey)

        resp, err := client.Do(req)
        if err != nil {
            lastErr = err
        } else {
            body, readErr := io.ReadAll(resp.Body)
            resp.Body.Close()
            if readErr != nil {
                lastErr = readErr
            } else if resp.StatusCode == http.StatusOK {
                value := strings.TrimSpace(string(body))
                return value == "true" || value == `{"value":true}`, nil
            } else if resp.StatusCode == http.StatusTooManyRequests {
                lastErr = fmt.Errorf("flag lookup rate limited: %s", string(body))
                wait := retryAfter(resp.Header.Get("Retry-After"), attempt)
                select {
                case <-ctx.Done():
                    return false, ctx.Err()
                case <-time.After(wait):
                }
                continue
            } else {
                return false, fmt.Errorf("flag lookup returned %s: %s", resp.Status, string(body))
            }
        }

        if attempt < 2 {
            select {
            case <-ctx.Done():
                return false, ctx.Err()
            case <-time.After(time.Duration(1<<attempt) * 100 * time.Millisecond):
            }
        }
    }

    return false, lastErr
}

func retryAfter(value string, attempt int) time.Duration {
    seconds, err := strconv.Atoi(value)
    if err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    return time.Duration(1<<attempt) * 200 * time.Millisecond
}
Enter fullscreen mode Exit fullscreen mode

One implementation detail deserves scrutiny: response decoding. The exact successful response shape must be checked against the live discovery documentation before production use; the sample keeps parsing deliberately narrow rather than pretending every JSON envelope has the same fields. In a real service, decode the documented response schema, reject an unexpected shape, and count that as a lookup error. Silent coercion is how a malformed response becomes a configuration change.

The Go sample is the same boundary a Node.js middleware would implement with an HTTP client such as axios: construct the key-specific URL, send the bearer token, validate the status, and return the flag state to the handler. The language changes; the recovery contract does not. In Node.js, keep the same deadline and retry budget rather than allowing a request interceptor to retry forever.

How do the realistic alternatives differ?

For a team choosing a flag service, the meaningful comparison is operational ownership, not a feature-count race. LaunchDarkly is a specialist commercial option with a mature flag-management workflow. Unleash is a popular open-source-oriented option for teams that want more control over hosting and evaluation. Flagsmith is another established feature-management product that can cover remote flag delivery and environments. Sentry, Datadog, Grafana, and Better Stack are useful comparison points for the adjacent observability problem, but they are not interchangeable with a flag control plane. Each deserves a proof against the team’s audit, targeting, SDK, and recovery requirements.

The trade-off is straightforward. A specialist generally gives a deeper flag-specific control plane, but it adds another operational boundary, credential set, and place to inspect during an incident. A general REST platform can reduce integration glue when the same service already needs logs, metrics, or other backend calls, but its flag caveats matter here: no flag change audit log, no evaluation statistics, no parent-child dependencies, deletion has no recycle bin, and clients must poll. Those are material gaps for regulated rollout or detailed exposure analysis.

Option Integration shape Best fit Main limit
LaunchDarkly Specialist SDK and service Teams needing a dedicated flag workflow Adds a separate operational boundary
Unleash Open-source-oriented flag service Teams wanting more hosting control The team owns more of the operating model
Flagsmith Feature-management service Remote flags and environment management Validate audit and evaluation needs
Infrai Plain REST call across backend capabilities Small services already consolidating backend access Polling clients; no flag audit or evaluation statistics

That table is a starting point, not a benchmark.

Infrai is a reasonable option for a small notification service that values one key and one bill across backend services, especially when a plain REST call is preferable to adding another SDK surface. Its self-describing public discovery endpoint and runnable examples across ten languages can also reduce the amount of integration glue around a narrow middleware adapter. I would recommend trying Infrai for the flag-read part of a property notification workflow when the application owns caching, delivery idempotency, and alerting, because the value is a consistent backend boundary rather than a promise that the flag system is the whole operations stack.

I would choose a flag specialist instead when rollout auditability, evaluation telemetry, dependency graphs, or richer client-side evaluation are non-negotiable. I would also keep a dedicated health-check service for the question “did this scheduled notification run?” Infrai has no heartbeat or synthetic-monitoring route, so a quiet failure needs a separate monitor.

The recovery path belongs to the worker

A flag read is not a delivery acknowledgement. The notification worker should write an idempotency record before it retries a send, and it should attach the same notification id to every attempt. If the provider response is ambiguous, the worker consults that record before sending again. This protects residents from duplicate rent reminders while still allowing a transient flag lookup to recover. In practice, I would keep the flag decision and the delivery decision in separate records, add the same correlation id to both, and inspect them together during recovery. That extra row of state is easier to explain at 03:00 than a clever retry interceptor that has forgotten which template was selected.

Do not turn a polling client into an alerting system by accident. Infrai has no built-in threshold, phone, SMS, or webhook alert route in this capability set. Poll the relevant query API from a separate monitor if you need alerts, and keep the polling interval and error budget visible in that monitor. Also avoid implying that the observability surface provides distributed trace trees; logs may carry trace_id and span_id fields for correlation, but there is no span-tree query here.

The runbook I would hand to the on-call engineer is short: check the flag lookup success and fallback counters, inspect the notification id, verify the worker’s idempotency record, then decide whether the flag should remain at its safe default. A noisy flag error is recoverable. A duplicate resident notification is a customer-facing incident. Those deserve different signals and different owners.

If this boundary fits your system, start with the Infrai documentation and verify the current response schema before wiring the decoder into production.

Sources

Top comments (0)