Building a secure and efficient MQTT-HTTP gateway with Node.js
One of my recent projects involved creating a Node.js script that acts as a gateway or middleware, capturing messages from an MQTT broker and relaying (streaming) them on the HTTP side via GET requests. Similarly, it should also handle POST requests from the HTTP side and publish them to the corresponding broker on the respective topic.
Why use this approach?
- Security: Avoid exposing MQTT broker credentials or certificates to browsers.
- Efficiency: Browsers require WebSocket for MQTT communication, adding overhead compared to native MQTTS.
- Flexibility: Centralized middleware simplifies custom message processing, improving maintainability.
Preparing the Node.js environment.
We only need two node components viz. mqtt and express. The former is the core library used to interact with MQTT broker and latter is the micro-framework used to interact with HTTP GET/POST requests.
It’s not necessary to stick to version 4 of the mqtt.js but I found it more stable and compatible with my Node.js setup.
npm install mqtt@4 express
Optional: CORS package
You might also need the cors
package if you want to customize Cross-Origin Resource Sharing (CORS) settings and enable access to the gateway for multiple users, such as REST API clients. I needed this in my case.
npm install cors
Setting up your MQTT brokers
The brokers
object contains configurations for each MQTT broker such as the URL and options (e.g., authentication). Each broker is also assigned a lastMessage
object to store the latest MQTT message for each topic.
const mqtt = require('mqtt');
const express = require('express');
const cors = require('cors'); // Optional: Import CORS package
const fs = require('fs');
const path = require('path');
const os = require('os');
const brokers = {};
brokers['example'] = {
url: "mqtts://example.us-east-1.amazonaws.com:8883",
lastMessage: {},
devices: {},
options: {
ca: fs.readFileSync( 'certs/ca.pem'),
cert: fs.readFileSync( 'certs/cert.crt') ,
key: fs.readFileSync( 'certs/key.key') ,
protocolId: 'MQTT',
protocolVersion: 4,
keepalive: 60,
//wsOptions: {timeout: 30000, path: '/mqtt'},
clientId: 'mqttjs_'+Math.random().toString(16).substr(2,8),
rejectUnauthorized: true,
clean: true,
secureProtocol: 'TLSv1_2_method', // Explicitly use TLSv1.2
}
};
brokers["mosquitto"]= {
url: 'mqtt://test.mosquitto.org', options:{},
clientId: 'mqttjs_'+Math.random().toString(16).substr(2,8),
lastMessage: {}
};
Initializing middleware for HTTP requests
Middleware ensures proper handling of requests by validating input and enabling CORS.
// Initialize HTTP Server
const app = express();
app.use(express.json()); // Parse JSON payloads
app.use(cors({
origin: '*' // Allow requests from any origin
}));
Establishing MQTT connections
I’ve kept the subscription topic to #
wildcard i.e. subscribe to all topics, you may or may not want to do that. The “connect” event of mqttClient
handles the connection where you can subscribe to the topics. Correspondingly, the message
event handles the incoming events from each broker. The dummy switch
block here can be used to add any custom processing logic you may have. The lastMessage
variable will anyway store the message for the corresponding topic.
The error
and auth
event handlers are just dummy blocks that you can expand upon if you want to. In the end, each client object is stored to mqttClients
array object from which it can later be retrieved.
// Store MQTT client objects for each broker
const mqttClients = {};
const subTopics = '#';
// Function to connect to each broker
Object.keys(brokers).forEach(key => {
const broker =brokers[key];
console.log("initializing broker:", key, broker.url);
const mqttClient = mqtt.connect(broker.url, broker.options);
mqttClient.on('connect', () => {
console.log(`Connected to broker: ${broker.url}`);
mqttClient.subscribe(subTopics, (err) => {
if (err) {
console.error(`Subscription error for ${key}:`, err);
} else {
console.log(`Subscribed to topic ${subTopics} on ${key}`);
}
});
});
mqttClient.on('message', (topic, message) => {
const msg = message.toString();
console.log(`[${key}] Received message on topic: ${topic}`);
broker['lastMessage'][topic] = msg;
switch(key) { //custom logic
case "custom_broker":
//@todo: handle custom logic
break;
}
});
mqttClient.on('error', (err) => {
console.error(`Error with broker ${key}:`, err.message);
});
mqttClient.on('auth', (packet, cb) => {
console.log('Authenticating with certificate...');
// Check the certificate properties and perform the authentication logic here.
// Call cb() with an error if authentication fails or null if it succeeds.
cb(null);
});
mqttClients[key] = mqttClient;
});
Bridging MQTT with Express API routes
On the HTTP side, I’ve created just two routes viz. events
and publish
. The former is used to fetch messages from a broker and latter to publish to them.
app.get('/events', (req, res) => {
const { brokerKey, topic } = req.query;
// Build the SQL query dynamically based on provided parameters
if (!brokerKey || !topic) {
return res.status(400).send('Invalid request. Provide brokerkey and topic.');
}
broker = brokers[brokerKey];
switch (brokerKey) { //custom logic
case "custom_broker":
//@todo: handle custom logic
break;
default:
res.json(broker['lastMessage'][topic]);
break;
}
});
// HTTP POST: Publish messages back to MQTT
app.post('/publish', (req, res) => {
const { brokerKey, topic, message } = req.body; // Destructure input fields
// Validate the input
if (!brokerKey || !topic || !message) {
return res.status(400).send('Invalid request. Provide broker key, topic, and message.');
}
// Retrieve the MQTT client for the specified broker
const mqttClient = mqttClients[brokerKey];
if (!mqttClient) {
return res.status(400).send('Invalid broker specified.');
}
// Publish the message
mqttClient.publish(topic, message, (err) => {
if (err) {
console.error(`Publish error on ${brokerKey}:`, err);
res.status(500).send('Failed to publish message.');
} else {
res.send(`Message published successfully to ${brokerKey}.`);
}
});
});
This is also pretty simple and straightforward. As earlier, I’ve created a dummy switch block for custom handling. The default is to just pick the latest message for that particular broker and topic (broker['lastMessage'][topic]
) and return it using res.json()
.
Same holds true for publish. It accepts three parameters called brokerKey, topic and message, then correspondingly publishes the message on the broker. You may want to add custom logic here including authentication logic based on API keys, etc.
Starting the Express server
The final step is to start the Express server. Use a custom port, or fallback to a default if not provided.
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
This setup provides a reliable foundation for creating an MQTT-HTTP gateway. You can further enhance it with persistent storage, advanced authentication, or message processing workflows. Let me know how this implementation works for you—or share your own ideas for improvement—in the comments below. Happy coding!