<dev/>

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.

Apr 17, 2026 ~4 min read
share:
Multi-Protocol Echo Server with NestJS: WebSocket, Socket.IO, and MQTT in a Single App

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:

ProtocolEndpoint
HTTPhttp://localhost:4500/
Native WebSocketws://localhost:4500
Socket.IOhttp://localhost:4502
MQTT over TCPmqtt://localhost:1883
MQTT over WebSocketws://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 dev

Or with Docker:

docker compose up

Environment 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.