Node-RED Internal JSON API: Flow Design, Trust Boundaries, and Failure Modes
A practical architecture note for exposing read-only and write JSON endpoints in Node-RED using HTTP In, Function, and HTTP Response nodes, with trust boundaries and failure modes.
30 Oct 2026, 15:49 UTC

You need a small internal JSON API—perhaps one endpoint that returns configuration and another that accepts state updates—and Node-RED is already running in your environment. The practical takeaway is that Node-RED can serve this role for low-traffic, internal-only endpoints, but only if you treat the editor and runtime as separate trust boundaries, validate every payload inside Function nodes, and do not rely on flow context for durable state.
Requirements
- Read-only GET endpoint that returns JSON.
- Write POST endpoint that accepts JSON, validates it, stores state in flow context, and returns 200 or 400.
- State survives between requests while the runtime is running.
- Service runs behind a reverse proxy or on a private network; no external internet access required.
- The Node-RED editor does not need to be reachable at runtime.
Smallest suitable design
Use one flow with two HTTP In nodes, each paired with a Function node and an HTTP Response node. The HTTP Response node is mandatory; without it, an incoming request waits until it times out.
GET endpoint
Wire HTTP In (GET) to a Function node, then to HTTP Response. In the Function node:
const stored = flow.get("config") || { mode: "off", updated: null };
return {
payload: stored,
statusCode: 200,
headers: { "content-type": "application/json" }
};
POST endpoint
Wire HTTP In (POST) to a Function node, then to HTTP Response. In the Function node:
let input;
try {
input = JSON.parse(msg.payload);
} catch (err) {
return {
payload: { error: "invalid_json" },
statusCode: 400,
headers: { "content-type": "application/json" }
};
}
if (!input || typeof input.value !== "string") {
return {
payload: { error: "missing_value" },
statusCode: 400,
headers: { "content-type": "application/json" }
};
}
flow.set("state", { value: input.value, updated: new Date().toISOString() });
return {
payload: { ok: true },
statusCode: 200,
headers: { "content-type": "application/json" }
};
Flow context is the built-in key-value store scoped to the current flow. It lives in memory by default, so values disappear when Node-RED restarts unless context storage is configured in settings.js.
Trust and data boundaries
- HTTP In nodes accept requests from any caller on the network unless Node-RED settings or a reverse proxy enforce authentication and TLS.
- The editor (httpAdmin) and the API runtime (httpNode) share the same Node-RED process by default. Exposing the editor port without authentication is a boundary risk even if your API endpoints are simple.
- If only runtime APIs are needed, disable the editor or bind it to localhost in settings.js, and place the API behind a reverse proxy or firewall that restricts callers to the intended network segment.
- Treat all input from HTTP In as untrusted. Validate structure and types before writing to flow context or using values in logic.
Operational checks
- Open the Node-RED sidebar and confirm the flow deploys without errors.
- Add a Catch node wired to the Function node error output to observe uncaught exceptions in the runtime.
- From a client on the same network segment, run:
curl -i http://:1880/state \
-X POST -H "Content-Type: application/json" \
-d '{"value":"example"}'
Where to run: any machine that can reach the Node-RED runtime port. Required permissions: network access to the configured port; no special OS privileges unless the port is below 1024. Expected check: HTTP/1.1 200 OK and a JSON body such as {"ok":true}. Risk: without authentication, any process on that segment can write state.
Repeat with a GET request to confirm the read endpoint returns the last stored value.
Rollback
Deploying a flow changes runtime state. Before deploying, export the current flow JSON from the Node-RED editor menu. If the new flow breaks the endpoint, re-import the previous export and deploy again, or use the editor's undo option if available in your session.
Failure modes
- Malformed JSON in a POST body throws an exception unless the Function node wraps parsing in try/catch, which can crash the message path and leave the caller waiting.
- Multiple Function nodes writing to the same flow context key without sequencing can produce race conditions because Node-RED processes messages on a single event loop but asynchronous work can interleave.
- Node-RED restart clears in-memory flow context, so clients may observe missing state after a reboot or crash.
- If a Function node returns without calling HTTP Response through a wired node, the connection remains open until the client or proxy times out.
Conditions that change the design
| Trigger | Design change |
|---|---|
| State must survive restarts | Configure contextStorage in settings.js to use file or database storage instead of memory-only context. |
| Endpoints must be reachable from the public internet | Add a reverse proxy with TLS and authentication, or move the API to a dedicated service rather than exposing Node-RED directly. |
| High concurrency or strict schema validation | Replace Function nodes with a dedicated Node.js/Express service or similar framework. |
| Multiple independent writers need strict ordering | Introduce a queue or external store with transactions instead of relying on flow context. |
Limitations and practical verification
Limitations: flow context is not durable by default, Node-RED has no built-in request schema validation beyond what you write in JavaScript, and the editor surface expands the attack surface if left exposed.
Practical verification: deploy the two-endpoint flow in a test instance, send curl requests for both GET and POST, confirm status codes and JSON bodies match expectations, then restart Node-RED and repeat the GET request. If the stored value is gone, context storage is memory-only and must be configured for durability. Finally, attempt to reach the endpoint from outside the intended network segment; if it responds, the reverse proxy or firewall boundary is not enforced.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.