The moves that turn a flow into something you can reuse, test and hand off.
Design patterns — start here
The moves that turn a flow into something you can reuse, test and hand off. Find the seams, pick the lightest level of reuse, keep the UI and logic apart, and hold state in context — then scale only when one instance proves it must.
Name the structure
Most spaghetti is three or four components never named; draw a box around each.
Name the stages hiding inside one instance
Before
one flow, no seams
nothing to reuse or hand off
draw the boxes
After
Ingest
Normalize
Enrich
Publish
A giant flow is bad because it has no seams. Most spaghetti is three or four well-defined components that were never named.
Use it when
You cannot reuse, test, or hand off a piece of a blob, and any edit means reading the whole tab. Naming the structure is the fix, not tidier wires.
How to apply it
Look for a repeated cluster, a logical stage (ingest, normalize, enrich, publish), a bounded responsibility, or a reuse magnet. Draw a box around each and extract it.
Good for
Splitting one tab into ingestion, processing, storage, and presentation.
Pick the lowest rung
link in/out → link call → subflow → packaged node; each rung costs more than the one below.
Pick the lowest rung that solves it
Cost rises with each rung
1 · link in / out
tidiness only
2 · link call
shared, returning service
3 · subflow
per-instance or reuse
4 · palette node
real code, distribution
When you find structure there are levels of extraction: link in and out, then link call, then subflow, then a packaged node. Pick the lowest one that solves the problem.
Use it when
Each rung costs more than the one below it. Reaching straight for a subflow or a custom node when a link call would do adds weight you do not need.
How to apply it
Link in and out for tidiness and tab-to-tab routing. Link call for a shared returning service. Subflow when it needs per-instance config or cross-instance reuse. Palette node for real code or distribution.
Watch out
Link nodes are organization, not modularity. Do not mistake tidiness for a reusable unit.
UI ↔ logic like client ↔ server
The backend sends a display-ready view-model; the frontend emits intent (action + payload).
Frontend and backend, two instances, one contract
Frontend · renders + emits
Node-RED
renders + emits
intent (what the user wants) ↕ view-model (display-ready state)
Backend · holds the truth
Node-RED
holds the truth
reads · writes
Store
SQL database
records
Treat the boundary between the Dashboard and your flow logic like a client and server API. The frontend renders state and emits intent; the backend holds the truth.
Use it when
The UI and the logic change at different rates. When you build payloads inside templates or cram logic next to a widget, every change touches both.
How to apply it
The backend sends a finished, display-ready view-model. The frontend emits a consistent intent message, an action plus a payload. Templates bind and emit; they never fetch, transform, or decide.
Good for
Redesigning the whole dashboard without touching a single business-logic node.
Context is shared memory
Hold the object in one place at the narrowest scope; messages are verbs, context is state.
Messages are verbs, context is nouns
Event
event
a reading arrived
node
recompute
write
Context store
context store
assets.<id>, oee.line1
read the one key it needs
Consumer
another node
reads one key
Context is shared memory with a defined scope. It holds a logical object in one place instead of threading it through wires. Messages are verbs; context is nouns.
Use it when
If you pass the same fat object through fifteen nodes just to move it, that is the job context exists to do. Wire gymnastics to avoid storing a value is the real anti-pattern.
How to apply it
Store the object once under a namespaced key at the narrowest scope that works (node, then flow, then global). Each node reads the one key it needs, and a persistent store holds anything that must survive a restart.
Watch out
One writer per key, serialize concurrent updates, and keep enough on the wire to stay debuggable.
Static config vs runtime config
Env vars set at deploy (read-only to the flow); persisted context for anything a user edits.
Env var, set at deploy
BROKER_HOST
baked, read-only
to change it
edit env, then redeploy
Persisted context, changed by a user
UI edits
a form or button
context store
live config
flow at run
reads current
Two different kinds of configuration. Static config changes per environment and is set at deploy: broker host, DB connection. Runtime config changes while running, by a user, with no redeploy.
Use it when
Env vars are resolved at deploy time and are read-only to the running flow. The moment a user needs to change what the flow operates on, an env var forces a developer and a redeploy.
How to apply it
Keep static config in env vars or a config node. Put user-editable config in persisted context (or a config file), edited through the UI via an intent message, and read by the flow at execution time.
Watch out
If a value would ever be changed through a button or form, it is not an env var.
Deploy flows as JSON
Populate link arrays with node ids and l:true; the editor's quiet fixes won't happen over the API.
Emit JSON, deploy via the Admin API
You emit it
flow JSON
you emit it
POST /flows
Admin API
Admin API
POST /flows
deploys to
Runtimes
Node-RED
runtime
Node-RED
edge device
links populated by node id (not name), l:true, x = LEFT + width/2, respond once on msg.error
Emitting flows as JSON and deploying via the Admin API. The editor quietly fixes mistakes that a programmatic deploy does not, so the failure classes are different.
Use it when
API-deployed flows look right but hang, double-respond, loop, or read as spaghetti when the things the editor papers over are left undone.
How to apply it
Populate link arrays with target node ids (name matching is editor-only) and set l:true. Skip success formatters when msg.error is set so you respond once. Never let a tab-wide catch re-dispatch a response-layer error. Compute x as LEFT + width/2.
Watch out
Empty link arrays are the top reason an API-deployed flow does nothing.