Skip to main content

A Mock REST API in Node-RED and Docker

Koh Rong, Cambodia

Setting Up Node-RED

I want to create a JSON REST API in Node-RED to mock an IP camera interface. For this I am going to use the latest Node-RED Docker container from DockerHUB (Version 1.x). To persist my Node-RED flows I am going to create a nodered folder on my host system and mount it into the Node-Container like:

mkdir /opt/nodered
mkdir /opt/nodered/data

I am going to store my mock API data in JSON files inside the data folder - e.g.:

mkdir /opt/nodered/data/api
nano /opt/nodered/data/api/getuserattr.json
{
"at_user0": {
"at_username0": "admin",
"at_password0": "instar",
"at_authlevel0": "15",
"at_enable0": "on"
},
"at_user1": {
"at_username1": "user",
"at_password1": "instar",
"at_authlevel1": "3",
"at_enable1": "off"
},
"at_user2": {
"at_username1": "guest",
"at_password1": "instar",
"at_authlevel1": "1",
"at_enable1": "off"
}
}

To pull the image and run the container:

docker run -ti -d -p 1880:1880 -v /opt/nodered/data:/data --name nodered nodered/node-red

The container will now create the Node-RED configuration files inside the nodered folder on your host. We now need to edit the settings.js file to point the httpStatic folder to /data/api to make our JSON files available to the Node-RED dashboard.

nano /opt/nodered/data/settings.js
module.exports = {

...

// To password protect the node-defined HTTP endpoints (httpNodeRoot), or
// the static content (httpStatic), the following properties can be used.
// The pass field is a bcrypt hash of the password.
// See http://nodered.org/docs/security.html#generating-the-password-hash
//httpNodeAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."},
//httpStaticAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."},
httpStatic: '/data/api',

...
}

You can now test that your file is available by typing in your Node-RED IP address, port and the name of your file - e.g.:

http://192.168.2.111:1880/getuserattr.json

Node-RED Mock API

Accessing your API

Creating the API Endpoint

Node-RED Mock API

Creating a simple REST endpoint with Node-RED:

  • Use the HTTP input node to create a web hook. Then, edit the method GET and the URL /getuserattr.
  • Use the file-in node that reads a file and sends its content in the payload of the message. Then, edit the filename /data/api/getuserattr.json.
  • Use the JSON function node that converts the message to JSON.
  • Use the HTTP response node that returns the message

Node-RED Flow:

[{"id":"487ea512.dbc46c","type":"http in","z":"21db30d1.ba9c7","name":"/getuserattr/json/","url":"/getuserattr/json/","method":"get","upload":false,"swaggerDoc":"","x":99,"y":77,"wires":[["6d2398bb.c34b68"]]},{"id":"6d2398bb.c34b68","type":"file in","z":"21db30d1.ba9c7","name":"getuserattr","filename":"/data/api/getuserattr.json","format":"utf8","chunk":false,"sendError":false,"encoding":"none","x":287,"y":77,"wires":[["ac6e6909.842c98"]]},{"id":"ac6e6909.842c98","type":"json","z":"21db30d1.ba9c7","name":"","property":"payload","action":"","pretty":false,"x":429,"y":76,"wires":[["3103ee5b.f1c682"]]},{"id":"3103ee5b.f1c682","type":"http response","z":"21db30d1.ba9c7","name":"","statusCode":"","headers":{},"x":552,"y":76,"wires":[]}]

You can now access the JSON data via the URL you defined inside the HTTP-In node, e.g.:

http://192.168.2.111:1880/getuserattr/json/

Node-RED Mock API

You can use a JSON formatter plugin to beautify the JSON output

Converting your State Storage to YAML

To make my state files more maintainable / readable I want to use YAML instead of JSON:

nano /opt/nodered/data/api/getuserattr.yml
at_user0:
at_username0: admin
at_password0: instar
at_authlevel0: '15'
at_enable0: on
at_user1:
at_username1: user
at_password1: instar
at_authlevel1: '3'
at_enable1: off
at_user2:
at_username1: guest
at_password1: instar
at_authlevel1: '1'
at_enable1: off

We can access this data by swapping the JSON node with a YAML node:

Node-RED Flow:

[{"id":"f382171b.977008","type":"http in","z":"21db30d1.ba9c7","name":"/getuserattr/yaml/","url":"/getuserattr/yaml/","method":"get","upload":false,"swaggerDoc":"","x":108,"y":123,"wires":[["8077e62e.27ed98"]]},{"id":"8077e62e.27ed98","type":"file in","z":"21db30d1.ba9c7","name":"getuserattr","filename":"/data/api/getuserattr.json","format":"utf8","chunk":false,"sendError":false,"encoding":"none","x":286,"y":123,"wires":[["e258cdd1.7ac92"]]},{"id":"eb5058f9.f09328","type":"http response","z":"21db30d1.ba9c7","name":"","statusCode":"","headers":{},"x":551,"y":122,"wires":[]},{"id":"e258cdd1.7ac92","type":"yaml","z":"21db30d1.ba9c7","property":"payload","name":"","x":427,"y":122,"wires":[["eb5058f9.f09328"]]}]

Test the flow by accessing this URL: http://192.168.2.111:1880/getuserattr/yaml/

Accessing Keys

The URL shown above always returns the entire glob of information. I now want to be able to query a single key out of it. As a proof of concept I added a function node with the following code and connected a inject and debug node to it. By clicking on the inject node I will trigger my function and see the output in the Node-RED debug panel:

const state = {"at_user0":{"at_username0":"admin","at_password0":"instar","at_authlevel0":"15","at_enable0":"on"},"at_user1":{"at_username1":"user","at_password1":"instar","at_authlevel1":"3","at_enable1":"off"},"at_user3":{"at_username1":"guest","at_password1":"instar","at_authlevel1":"1","at_enable1":"off"}};
var string = JSON.stringify(state);
var output = JSON.parse(string)
msg.payload = output.at_user0;
return msg;

Node-RED Flow:

[{"id":"ddc6d5a9.369d68","type":"function","z":"21db30d1.ba9c7","name":"","func":"const state = {\"at_user0\":{\"at_username0\":\"admin\",\"at_password0\":\"instar\",\"at_authlevel0\":\"15\",\"at_enable0\":\"on\"},\"at_user1\":{\"at_username1\":\"user\",\"at_password1\":\"instar\",\"at_authlevel1\":\"3\",\"at_enable1\":\"off\"},\"at_user3\":{\"at_username1\":\"guest\",\"at_password1\":\"instar\",\"at_authlevel1\":\"1\",\"at_enable1\":\"off\"}};\nvar string = JSON.stringify(state);\nvar output = JSON.parse(string)\nmsg.payload = output.at_user0;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":250,"y":440,"wires":[["60a5053d.d911fc"]]},{"id":"59a61cb4.46ddd4","type":"inject","z":"21db30d1.ba9c7","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":90,"y":440,"wires":[["ddc6d5a9.369d68"]]},{"id":"60a5053d.d911fc","type":"debug","z":"21db30d1.ba9c7","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":420,"y":440,"wires":[]}]

By setting the msg.payload to output.at_user0 I will now ingest the entire information but only print the user data for my first user.

We can now create 1 route that returns the entire user state, as well as 3 additional routes that only return the state of one of the users: at_user0, at_user1, at_user2 by adding a function node that filters for output.at_user0, output.at_user1 or output.at_user2:

Node-RED Mock API

Node-RED Flow:

[{"id":"78862c12.e12b64","type":"http in","z":"21db30d1.ba9c7","name":"/getuserattr/","url":"/getuserattr/","method":"get","upload":false,"swaggerDoc":"","x":90,"y":312,"wires":[["daa48d2d.58606"]]},{"id":"daa48d2d.58606","type":"file in","z":"21db30d1.ba9c7","name":"getuserattr","filename":"/data/api/getuserattr.yml","format":"utf8","chunk":false,"sendError":false,"encoding":"none","x":248,"y":312,"wires":[["239bf6bc.ff85ea"]]},{"id":"3dd619a9.3d6496","type":"http response","z":"21db30d1.ba9c7","name":"","statusCode":"","headers":{},"x":500,"y":312,"wires":[]},{"id":"bc3d1a73.5b07d8","type":"http in","z":"21db30d1.ba9c7","name":"/at_user0/","url":"/at_user0/","method":"get","upload":false,"swaggerDoc":"","x":80,"y":352,"wires":[["74fda1d3.91845"]]},{"id":"74fda1d3.91845","type":"file in","z":"21db30d1.ba9c7","name":"getuserattr","filename":"/data/api/getuserattr.yml","format":"utf8","chunk":false,"sendError":false,"encoding":"none","x":230,"y":352,"wires":[["a9310d0.dff79f"]]},{"id":"6aa3299b.46a838","type":"http response","z":"21db30d1.ba9c7","name":"","statusCode":"","headers":{},"x":658,"y":352,"wires":[]},{"id":"a39ea2b0.12d6e","type":"function","z":"21db30d1.ba9c7","name":"GET at_user0","func":"const state = msg.payload;\nvar string = JSON.stringify(state);\nvar output = JSON.parse(string)\nmsg.payload = output.at_user0;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":513,"y":352,"wires":[["6aa3299b.46a838"]]},{"id":"239bf6bc.ff85ea","type":"yaml","z":"21db30d1.ba9c7","property":"payload","name":"","x":383,"y":312,"wires":[["3dd619a9.3d6496"]]},{"id":"a9310d0.dff79f","type":"yaml","z":"21db30d1.ba9c7","property":"payload","name":"","x":365,"y":352,"wires":[["a39ea2b0.12d6e"]]},{"id":"eb9d312.ed1bad","type":"comment","z":"21db30d1.ba9c7","name":"User Data Test","info":"","x":100,"y":269,"wires":[]},{"id":"97f0f0a4.8fc9f","type":"http in","z":"21db30d1.ba9c7","name":"/at_user1/","url":"/at_user1/","method":"get","upload":false,"swaggerDoc":"","x":80,"y":392,"wires":[["6e5b944b.2a65ac"]]},{"id":"6e5b944b.2a65ac","type":"file in","z":"21db30d1.ba9c7","name":"getuserattr","filename":"/data/api/getuserattr.yml","format":"utf8","chunk":false,"sendError":false,"encoding":"none","x":230,"y":392,"wires":[["b7153106.4fb8b"]]},{"id":"53d49f55.ab3a4","type":"http response","z":"21db30d1.ba9c7","name":"","statusCode":"","headers":{},"x":658,"y":392,"wires":[]},{"id":"67c2d2b5.33cc1c","type":"function","z":"21db30d1.ba9c7","name":"GET at_user1","func":"const state = msg.payload;\nvar string = JSON.stringify(state);\nvar output = JSON.parse(string)\nmsg.payload = output.at_user1;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":513,"y":392,"wires":[["53d49f55.ab3a4"]]},{"id":"b7153106.4fb8b","type":"yaml","z":"21db30d1.ba9c7","property":"payload","name":"","x":365,"y":392,"wires":[["67c2d2b5.33cc1c"]]},{"id":"6fdaf17e.ab1a2","type":"http in","z":"21db30d1.ba9c7","name":"/at_user2/","url":"/at_user2/","method":"get","upload":false,"swaggerDoc":"","x":80,"y":432,"wires":[["75bfaf5b.6a47"]]},{"id":"75bfaf5b.6a47","type":"file in","z":"21db30d1.ba9c7","name":"getuserattr","filename":"/data/api/getuserattr.yml","format":"utf8","chunk":false,"sendError":false,"encoding":"none","x":230,"y":432,"wires":[["1eb745e3.f1861a"]]},{"id":"e7eb5dfb.34328","type":"http response","z":"21db30d1.ba9c7","name":"","statusCode":"","headers":{},"x":658,"y":432,"wires":[]},{"id":"8d47135.f8c08f","type":"function","z":"21db30d1.ba9c7","name":"GET at_user2","func":"const state = msg.payload;\nvar string = JSON.stringify(state);\nvar output = JSON.parse(string)\nmsg.payload = output.at_user2;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":513,"y":432,"wires":[["e7eb5dfb.34328"]]},{"id":"1eb745e3.f1861a","type":"yaml","z":"21db30d1.ba9c7","property":"payload","name":"","x":365,"y":432,"wires":[["8d47135.f8c08f"]]}]

We now created 4 API enpoints (web hooks) that allow us to either ask for the entire user state or for the state of a specific user:

http://192.168.2.111:1880/getuserattr/
http://192.168.2.111:1880/at_user0/
http://192.168.2.111:1880/at_user1/
http://192.168.2.111:1880/at_user2/