Multi-Protocol Echo Server with NestJS: WebSocket, Socket.IO, and MQTT in a Single App
Testing real-time integrations usually means spinning up multiple services. I built socket-server — a NestJS server that exposes native WebSocket, Socket.IO, and an embedded MQTT broker from a single process.

Every time I work on a real-time feature — a WebSocket feed, a Socket.IO room, or an MQTT device integration — I run into the same friction: setting up the server side to test against.
Spinning up a standalone MQTT broker, a separate Socket.IO server, and a plain WebSocket endpoint just to test a client is tedious. You end up with multiple processes, multiple ports, and a docker-compose.yml that feels like overkill for what should be a quick feedback loop.
So I built socket-server: a single NestJS application that speaks all three protocols at once and echoes everything back.
What It Does
One npm run dev gives you:
| Protocol | Endpoint |
|---|---|
| HTTP | http://localhost:4500/ |
| Native WebSocket | ws://localhost:4500 |
| Socket.IO | http://localhost:4502 |
| MQTT over TCP | mqtt://localhost:1883 |
| MQTT over WebSocket | ws://localhost:4500/mqtt |
Every protocol follows the same contract: connect, send a message, get it echoed back with a structured JSON payload and a timestamp. No broker to install, no separate config files, no extra containers.
The Interesting Part: One Port for Two Protocols
The part I found most satisfying to figure out was sharing port 4500 between native WebSocket and MQTT-over-WebSocket without conflict.
Both are WebSocket connections — how do you route them? By path. HTTP upgrades carry the request URL, so you can intercept the upgrade event on the HTTP server before handing it off:
// mqtt.service.ts
attachToServer(httpServer: HttpServer): void {
const wss = new WebSocketServer({ noServer: true });
httpServer.on('upgrade', (req, socket, head) => {
const url = req.url ?? '/';
if (!url.startsWith('/mqtt')) return; // route by path
wss.handleUpgrade(req, socket, head, (ws) => {
const stream = createWebSocketStream(ws);
this.broker.handle(stream, req); // hand off to aedes
});
});
}Native WebSocket clients connect to ws://localhost:4500 (any path except /mqtt) and are handled by the ws library. MQTT clients connect to ws://localhost:4500/mqtt and are handled by the embedded aedes broker. Same port, clean separation.
Embedded MQTT — No External Broker
The MQTT broker uses aedes, which runs entirely in-process. There’s nothing to install or configure externally. When a client publishes to any topic, the broker echoes it back under echo/<original-topic>:
this.broker.on('publish', (packet, client) => {
if (!client) return; // ignore internal broker messages
if (packet.topic.startsWith('echo/')) return; // prevent echo loops
const responsePacket = {
topic: `echo/${packet.topic}`,
payload: Buffer.from(JSON.stringify({
event: 'response',
requestTopic: packet.topic,
data: `Echo: ${packet.payload.toString()}`,
timestamp: new Date().toISOString(),
})),
// ...
};
this.broker.publish(responsePacket, ...);
});The loop-prevention check (startsWith('echo/')) is the kind of thing that’s obvious in hindsight but will bite you in your first test run if you miss it.
Socket.IO Gateway in NestJS
NestJS has first-class support for Socket.IO via @WebSocketGateway. The gateway runs on a separate port to avoid conflicts with the native WebSocket server:
@WebSocketGateway(parseInt(process.env.SIO_PORT ?? '4502'), {
cors: { origin: '*' },
})
export class SocketIoGateway implements OnGatewayConnection {
handleConnection(client: Socket) {
client.emit('welcome', {
protocol: 'Socket.IO',
connection: { id: client.id, connectedAt: new Date().toISOString() },
});
}
@SubscribeMessage('message')
handleMessage(@ConnectedSocket() client: Socket, @MessageBody() data: unknown) {
client.emit('response', {
data: `Echo: ${JSON.stringify(data)}`,
timestamp: new Date().toISOString(),
});
}
}Clean, declarative, and completely isolated from the HTTP/WebSocket logic on port 4500.
Running It
git clone https://github.com/abelrgr/socket-tester
cd socket-tester
npm install
npm run devOr with Docker:
docker compose upEnvironment variables (PORT, SIO_PORT, MQTT_TCP_PORT) let you override any port if you have conflicts on your machine.
When to Use It
- Testing WebSocket client code before the real backend is ready
- Verifying MQTT topic routing and QoS behavior in a client library
- Smoke-testing Socket.IO reconnect/retry logic
- Demoing real-time features without a full backend setup
It’s not a production server — it’s a local development tool. But having all three protocols available from one command has saved me setup time on more than a few projects.
The code is on GitHub at abelrgr/socket-tester — contributions and issues welcome.
// tagged with