0
# Node Development
1
2
APIs and patterns for creating custom Node-RED node types. This covers the node registration system, lifecycle management, and integration with the Node-RED runtime for building reusable node modules.
3
4
## Capabilities
5
6
### Node Registration
7
8
Core API for registering new node types with the Node-RED runtime.
9
10
```javascript { .api }
11
/**
12
* Node registration function (available in node modules)
13
* @param type - Node type identifier
14
* @param constructor - Node constructor function
15
*/
16
module.exports = function(RED) {
17
RED.nodes.registerType(type: string, constructor: NodeConstructor): void;
18
};
19
20
/**
21
* Node constructor interface
22
*/
23
interface NodeConstructor {
24
(this: NodeInstance, config: NodeConfig): void;
25
}
26
27
/**
28
* Get node instance by ID
29
* @param id - Node instance ID
30
* @returns Node instance or null
31
*/
32
RED.nodes.getNode(id: string): NodeInstance | null;
33
34
/**
35
* Create and initialize node instance
36
* @param node - Node instance object
37
* @param config - Node configuration from editor
38
*/
39
RED.nodes.createNode(node: NodeInstance, config: NodeConfig): void;
40
```
41
42
**Basic Node Example:**
43
44
```javascript
45
module.exports = function(RED) {
46
function MyCustomNode(config) {
47
// Create the node instance
48
RED.nodes.createNode(this, config);
49
50
// Store configuration
51
this.customProperty = config.customProperty;
52
53
// Set up message handler
54
this.on('input', function(msg, send, done) {
55
// Process the message
56
msg.payload = this.customProperty + ": " + msg.payload;
57
58
// Send the message
59
send(msg);
60
61
// Signal completion
62
done();
63
});
64
65
// Clean up on close
66
this.on('close', function() {
67
// Cleanup code here
68
});
69
}
70
71
// Register the node type
72
RED.nodes.registerType("my-custom-node", MyCustomNode);
73
};
74
```
75
76
### Node Instance Interface
77
78
Methods and properties available on node instances during execution.
79
80
```javascript { .api }
81
/**
82
* Node instance interface
83
*/
84
interface NodeInstance extends EventEmitter {
85
id: string;
86
type: string;
87
name?: string;
88
send(msg: NodeMessage | NodeMessage[]): void;
89
error(err: string | Error, msg?: NodeMessage): void;
90
warn(warning: string | object): void;
91
log(info: string | object): void;
92
status(status: NodeStatus): void;
93
on(event: 'input', handler: InputHandler): void;
94
on(event: 'close', handler: CloseHandler): void;
95
context(): ContextStore;
96
}
97
98
/**
99
* Input message handler
100
*/
101
interface InputHandler {
102
(msg: NodeMessage, send: SendFunction, done: DoneFunction): void;
103
}
104
105
/**
106
* Send function for output messages
107
*/
108
interface SendFunction {
109
(msg: NodeMessage | NodeMessage[]): void;
110
}
111
112
/**
113
* Done function to signal message processing completion
114
*/
115
interface DoneFunction {
116
(err?: Error): void;
117
}
118
119
/**
120
* Close handler for cleanup
121
*/
122
interface CloseHandler {
123
(removed: boolean, done: () => void): void;
124
}
125
```
126
127
**Advanced Node Example:**
128
129
```javascript
130
module.exports = function(RED) {
131
function AdvancedNode(config) {
132
RED.nodes.createNode(this, config);
133
134
const node = this;
135
let processing = false;
136
137
// Configuration
138
node.timeout = parseInt(config.timeout) || 5000;
139
node.retries = parseInt(config.retries) || 3;
140
141
// Status update
142
node.status({ fill: "green", shape: "ring", text: "ready" });
143
144
node.on('input', function(msg, send, done) {
145
// Prevent concurrent processing
146
if (processing) {
147
node.warn("Already processing, dropping message");
148
done();
149
return;
150
}
151
152
processing = true;
153
node.status({ fill: "blue", shape: "dot", text: "processing" });
154
155
// Simulate async operation with timeout
156
const timeout = setTimeout(() => {
157
processing = false;
158
node.status({ fill: "red", shape: "ring", text: "timeout" });
159
done(new Error("Processing timeout"));
160
}, node.timeout);
161
162
// Async processing
163
processMessage(msg).then(result => {
164
clearTimeout(timeout);
165
processing = false;
166
167
msg.payload = result;
168
node.status({ fill: "green", shape: "dot", text: "success" });
169
170
send(msg);
171
done();
172
}).catch(err => {
173
clearTimeout(timeout);
174
processing = false;
175
176
node.status({ fill: "red", shape: "ring", text: "error" });
177
done(err);
178
});
179
});
180
181
node.on('close', function(removed, done) {
182
// Clean up resources
183
processing = false;
184
node.status({});
185
186
if (removed) {
187
node.log("Node removed from flow");
188
}
189
190
done();
191
});
192
193
async function processMessage(msg) {
194
// Custom processing logic
195
return new Promise((resolve, reject) => {
196
setTimeout(() => {
197
resolve("Processed: " + msg.payload);
198
}, 1000);
199
});
200
}
201
}
202
203
RED.nodes.registerType("advanced-node", AdvancedNode);
204
};
205
```
206
207
### Configuration Nodes
208
209
Special nodes that provide shared configuration across multiple node instances.
210
211
```javascript { .api }
212
/**
213
* Configuration node constructor
214
*/
215
interface ConfigNodeConstructor {
216
(this: ConfigNodeInstance, config: NodeConfig): void;
217
}
218
219
/**
220
* Configuration node instance
221
*/
222
interface ConfigNodeInstance {
223
id: string;
224
type: string;
225
name?: string;
226
users: string[];
227
[key: string]: any;
228
}
229
```
230
231
**Configuration Node Example:**
232
233
```javascript
234
module.exports = function(RED) {
235
// Configuration node for API credentials
236
function ApiConfigNode(config) {
237
RED.nodes.createNode(this, config);
238
239
this.host = config.host;
240
this.port = config.port;
241
// Store credentials securely
242
this.username = this.credentials.username;
243
this.password = this.credentials.password;
244
245
// Create reusable client
246
this.client = createApiClient({
247
host: this.host,
248
port: this.port,
249
username: this.username,
250
password: this.password
251
});
252
}
253
254
// Register config node
255
RED.nodes.registerType("api-config", ApiConfigNode, {
256
credentials: {
257
username: { type: "text" },
258
password: { type: "password" }
259
}
260
});
261
262
// Node that uses the config
263
function ApiRequestNode(config) {
264
RED.nodes.createNode(this, config);
265
266
// Get config node instance
267
this.apiConfig = RED.nodes.getNode(config.apiConfig);
268
269
if (!this.apiConfig) {
270
this.error("API configuration not found");
271
return;
272
}
273
274
this.on('input', function(msg, send, done) {
275
// Use shared client from config node
276
this.apiConfig.client.request(msg.payload)
277
.then(response => {
278
msg.payload = response;
279
send(msg);
280
done();
281
})
282
.catch(err => done(err));
283
});
284
}
285
286
RED.nodes.registerType("api-request", ApiRequestNode);
287
};
288
```
289
290
### Context and Storage
291
292
Access to Node-RED's context storage system from custom nodes.
293
294
```javascript { .api }
295
/**
296
* Get node context storage
297
* @returns Context store for this node
298
*/
299
node.context(): ContextStore;
300
301
/**
302
* Get flow context storage
303
* @returns Context store for the current flow
304
*/
305
RED.util.getContext('flow', flowId): ContextStore;
306
307
/**
308
* Get global context storage
309
* @returns Global context store
310
*/
311
RED.util.getContext('global'): ContextStore;
312
```
313
314
**Context Usage in Nodes:**
315
316
```javascript
317
function StatefulNode(config) {
318
RED.nodes.createNode(this, config);
319
320
const node = this;
321
322
node.on('input', function(msg, send, done) {
323
const context = node.context();
324
325
// Get stored state
326
let counter = context.get("counter") || 0;
327
counter++;
328
329
// Update state
330
context.set("counter", counter);
331
332
// Add state to message
333
msg.counter = counter;
334
msg.payload = `Message #${counter}: ${msg.payload}`;
335
336
send(msg);
337
done();
338
});
339
}
340
```
341
342
### Message Handling Patterns
343
344
Common patterns for handling messages in custom nodes.
345
346
```javascript { .api }
347
/**
348
* Multiple output node pattern
349
*/
350
function SplitterNode(config) {
351
RED.nodes.createNode(this, config);
352
353
this.on('input', function(msg, send, done) {
354
const outputs = [];
355
356
// Prepare outputs array (one per output terminal)
357
for (let i = 0; i < config.outputs; i++) {
358
outputs[i] = null;
359
}
360
361
// Route message based on payload type
362
if (typeof msg.payload === 'string') {
363
outputs[0] = msg; // String output
364
} else if (typeof msg.payload === 'number') {
365
outputs[1] = msg; // Number output
366
} else {
367
outputs[2] = msg; // Other output
368
}
369
370
send(outputs);
371
done();
372
});
373
}
374
375
/**
376
* Batch processing node pattern
377
*/
378
function BatchNode(config) {
379
RED.nodes.createNode(this, config);
380
381
const node = this;
382
const batchSize = config.batchSize || 10;
383
let batch = [];
384
385
node.on('input', function(msg, send, done) {
386
batch.push(msg);
387
388
if (batch.length >= batchSize) {
389
// Send batch
390
const batchMsg = {
391
payload: batch.map(m => m.payload),
392
_msgid: RED.util.generateId()
393
};
394
395
send(batchMsg);
396
batch = []; // Reset batch
397
}
398
399
done();
400
});
401
402
// Timer to flush partial batches
403
const flushTimer = setInterval(() => {
404
if (batch.length > 0) {
405
const batchMsg = {
406
payload: batch.map(m => m.payload),
407
_msgid: RED.util.generateId()
408
};
409
410
node.send(batchMsg);
411
batch = [];
412
}
413
}, 30000); // Flush every 30 seconds
414
415
node.on('close', function() {
416
clearInterval(flushTimer);
417
});
418
}
419
```
420
421
### Node Module Structure
422
423
Structure and metadata for Node-RED node modules.
424
425
```javascript { .api }
426
/**
427
* Node module package.json requirements
428
*/
429
interface NodeModulePackage {
430
name: string;
431
version: string;
432
description: string;
433
"node-red": {
434
version?: string;
435
nodes: {
436
[nodeType: string]: string; // Path to node JS file
437
};
438
};
439
keywords: string[]; // Should include "node-red"
440
}
441
```
442
443
**Example package.json:**
444
445
```json
446
{
447
"name": "node-red-contrib-my-nodes",
448
"version": "1.0.0",
449
"description": "Custom nodes for Node-RED",
450
"node-red": {
451
"nodes": {
452
"my-custom-node": "nodes/my-custom-node.js",
453
"api-config": "nodes/api-config.js"
454
}
455
},
456
"keywords": [
457
"node-red",
458
"api",
459
"custom"
460
],
461
"files": [
462
"nodes/"
463
]
464
}
465
```
466
467
## Types
468
469
```javascript { .api }
470
interface NodeConfig {
471
id: string;
472
type: string;
473
name?: string;
474
[key: string]: any;
475
}
476
477
interface NodeMessage {
478
_msgid: string;
479
topic?: string;
480
payload: any;
481
[key: string]: any;
482
}
483
484
interface NodeStatus {
485
fill?: 'red' | 'green' | 'yellow' | 'blue' | 'grey';
486
shape?: 'ring' | 'dot';
487
text?: string;
488
}
489
490
interface ContextStore {
491
get(key: string | string[], store?: string, callback?: Function): any;
492
set(key: string | string[], value: any, store?: string, callback?: Function): void;
493
keys(store?: string, callback?: Function): string[];
494
}
495
```