Low-code programming platform for event-driven applications with visual flow-based editor and runtime system
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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.
Core API for registering new node types with the Node-RED runtime.
/**
* Node registration function (available in node modules)
* @param type - Node type identifier
* @param constructor - Node constructor function
*/
module.exports = function(RED) {
RED.nodes.registerType(type: string, constructor: NodeConstructor): void;
};
/**
* Node constructor interface
*/
interface NodeConstructor {
(this: NodeInstance, config: NodeConfig): void;
}
/**
* Get node instance by ID
* @param id - Node instance ID
* @returns Node instance or null
*/
RED.nodes.getNode(id: string): NodeInstance | null;
/**
* Create and initialize node instance
* @param node - Node instance object
* @param config - Node configuration from editor
*/
RED.nodes.createNode(node: NodeInstance, config: NodeConfig): void;Basic Node Example:
module.exports = function(RED) {
function MyCustomNode(config) {
// Create the node instance
RED.nodes.createNode(this, config);
// Store configuration
this.customProperty = config.customProperty;
// Set up message handler
this.on('input', function(msg, send, done) {
// Process the message
msg.payload = this.customProperty + ": " + msg.payload;
// Send the message
send(msg);
// Signal completion
done();
});
// Clean up on close
this.on('close', function() {
// Cleanup code here
});
}
// Register the node type
RED.nodes.registerType("my-custom-node", MyCustomNode);
};Methods and properties available on node instances during execution.
/**
* Node instance interface
*/
interface NodeInstance extends EventEmitter {
id: string;
type: string;
name?: string;
send(msg: NodeMessage | NodeMessage[]): void;
error(err: string | Error, msg?: NodeMessage): void;
warn(warning: string | object): void;
log(info: string | object): void;
status(status: NodeStatus): void;
on(event: 'input', handler: InputHandler): void;
on(event: 'close', handler: CloseHandler): void;
context(): ContextStore;
}
/**
* Input message handler
*/
interface InputHandler {
(msg: NodeMessage, send: SendFunction, done: DoneFunction): void;
}
/**
* Send function for output messages
*/
interface SendFunction {
(msg: NodeMessage | NodeMessage[]): void;
}
/**
* Done function to signal message processing completion
*/
interface DoneFunction {
(err?: Error): void;
}
/**
* Close handler for cleanup
*/
interface CloseHandler {
(removed: boolean, done: () => void): void;
}Advanced Node Example:
module.exports = function(RED) {
function AdvancedNode(config) {
RED.nodes.createNode(this, config);
const node = this;
let processing = false;
// Configuration
node.timeout = parseInt(config.timeout) || 5000;
node.retries = parseInt(config.retries) || 3;
// Status update
node.status({ fill: "green", shape: "ring", text: "ready" });
node.on('input', function(msg, send, done) {
// Prevent concurrent processing
if (processing) {
node.warn("Already processing, dropping message");
done();
return;
}
processing = true;
node.status({ fill: "blue", shape: "dot", text: "processing" });
// Simulate async operation with timeout
const timeout = setTimeout(() => {
processing = false;
node.status({ fill: "red", shape: "ring", text: "timeout" });
done(new Error("Processing timeout"));
}, node.timeout);
// Async processing
processMessage(msg).then(result => {
clearTimeout(timeout);
processing = false;
msg.payload = result;
node.status({ fill: "green", shape: "dot", text: "success" });
send(msg);
done();
}).catch(err => {
clearTimeout(timeout);
processing = false;
node.status({ fill: "red", shape: "ring", text: "error" });
done(err);
});
});
node.on('close', function(removed, done) {
// Clean up resources
processing = false;
node.status({});
if (removed) {
node.log("Node removed from flow");
}
done();
});
async function processMessage(msg) {
// Custom processing logic
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Processed: " + msg.payload);
}, 1000);
});
}
}
RED.nodes.registerType("advanced-node", AdvancedNode);
};Special nodes that provide shared configuration across multiple node instances.
/**
* Configuration node constructor
*/
interface ConfigNodeConstructor {
(this: ConfigNodeInstance, config: NodeConfig): void;
}
/**
* Configuration node instance
*/
interface ConfigNodeInstance {
id: string;
type: string;
name?: string;
users: string[];
[key: string]: any;
}Configuration Node Example:
module.exports = function(RED) {
// Configuration node for API credentials
function ApiConfigNode(config) {
RED.nodes.createNode(this, config);
this.host = config.host;
this.port = config.port;
// Store credentials securely
this.username = this.credentials.username;
this.password = this.credentials.password;
// Create reusable client
this.client = createApiClient({
host: this.host,
port: this.port,
username: this.username,
password: this.password
});
}
// Register config node
RED.nodes.registerType("api-config", ApiConfigNode, {
credentials: {
username: { type: "text" },
password: { type: "password" }
}
});
// Node that uses the config
function ApiRequestNode(config) {
RED.nodes.createNode(this, config);
// Get config node instance
this.apiConfig = RED.nodes.getNode(config.apiConfig);
if (!this.apiConfig) {
this.error("API configuration not found");
return;
}
this.on('input', function(msg, send, done) {
// Use shared client from config node
this.apiConfig.client.request(msg.payload)
.then(response => {
msg.payload = response;
send(msg);
done();
})
.catch(err => done(err));
});
}
RED.nodes.registerType("api-request", ApiRequestNode);
};Access to Node-RED's context storage system from custom nodes.
/**
* Get node context storage
* @returns Context store for this node
*/
node.context(): ContextStore;
/**
* Get flow context storage
* @returns Context store for the current flow
*/
RED.util.getContext('flow', flowId): ContextStore;
/**
* Get global context storage
* @returns Global context store
*/
RED.util.getContext('global'): ContextStore;Context Usage in Nodes:
function StatefulNode(config) {
RED.nodes.createNode(this, config);
const node = this;
node.on('input', function(msg, send, done) {
const context = node.context();
// Get stored state
let counter = context.get("counter") || 0;
counter++;
// Update state
context.set("counter", counter);
// Add state to message
msg.counter = counter;
msg.payload = `Message #${counter}: ${msg.payload}`;
send(msg);
done();
});
}Common patterns for handling messages in custom nodes.
/**
* Multiple output node pattern
*/
function SplitterNode(config) {
RED.nodes.createNode(this, config);
this.on('input', function(msg, send, done) {
const outputs = [];
// Prepare outputs array (one per output terminal)
for (let i = 0; i < config.outputs; i++) {
outputs[i] = null;
}
// Route message based on payload type
if (typeof msg.payload === 'string') {
outputs[0] = msg; // String output
} else if (typeof msg.payload === 'number') {
outputs[1] = msg; // Number output
} else {
outputs[2] = msg; // Other output
}
send(outputs);
done();
});
}
/**
* Batch processing node pattern
*/
function BatchNode(config) {
RED.nodes.createNode(this, config);
const node = this;
const batchSize = config.batchSize || 10;
let batch = [];
node.on('input', function(msg, send, done) {
batch.push(msg);
if (batch.length >= batchSize) {
// Send batch
const batchMsg = {
payload: batch.map(m => m.payload),
_msgid: RED.util.generateId()
};
send(batchMsg);
batch = []; // Reset batch
}
done();
});
// Timer to flush partial batches
const flushTimer = setInterval(() => {
if (batch.length > 0) {
const batchMsg = {
payload: batch.map(m => m.payload),
_msgid: RED.util.generateId()
};
node.send(batchMsg);
batch = [];
}
}, 30000); // Flush every 30 seconds
node.on('close', function() {
clearInterval(flushTimer);
});
}Structure and metadata for Node-RED node modules.
/**
* Node module package.json requirements
*/
interface NodeModulePackage {
name: string;
version: string;
description: string;
"node-red": {
version?: string;
nodes: {
[nodeType: string]: string; // Path to node JS file
};
};
keywords: string[]; // Should include "node-red"
}Example package.json:
{
"name": "node-red-contrib-my-nodes",
"version": "1.0.0",
"description": "Custom nodes for Node-RED",
"node-red": {
"nodes": {
"my-custom-node": "nodes/my-custom-node.js",
"api-config": "nodes/api-config.js"
}
},
"keywords": [
"node-red",
"api",
"custom"
],
"files": [
"nodes/"
]
}interface NodeConfig {
id: string;
type: string;
name?: string;
[key: string]: any;
}
interface NodeMessage {
_msgid: string;
topic?: string;
payload: any;
[key: string]: any;
}
interface NodeStatus {
fill?: 'red' | 'green' | 'yellow' | 'blue' | 'grey';
shape?: 'ring' | 'dot';
text?: string;
}
interface ContextStore {
get(key: string | string[], store?: string, callback?: Function): any;
set(key: string | string[], value: any, store?: string, callback?: Function): void;
keys(store?: string, callback?: Function): string[];
}