Estimated reading time: 7 min read

Spin Up a Tiny Socket.IO Server for Local Debugging

#websockets#nodejs#realtime#backend#express#debugging#development-tools#socket-io

This is a short, practical guide to run a Socket.IO service locally so you can build and debug any socket client (web, mobile, or server). You’ll get a streaming endpoint that emits sample data every 2 seconds, plus a minimal client to verify events.

What you’ll need

  • Node.js 18+
  • A terminal
  • An editor

1) Project setup

Create a new folder and add a package.json with the essentials:

json
{
  "name": "socket-server",
  "version": "1.0.0",
  "description": "Tiny Socket.IO server for local debugging",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "license": "ISC",
  "dependencies": {
    "express": "^5.1.0",
    "socket.io": "^4.8.1"
  },
  "devDependencies": {
    "nodemon": "^3.1.0"
  }
}

Install:

bash
npm install

2) Server — `server.js`

This server accepts connections from any origin and emits a stream-data event every 2 seconds. It logs connects and disconnects. We also clear the timer on shutdown.

js
// server.js
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");

const app = express();
const server = http.createServer(app);
const io = new Server(server, { cors: { origin: "*" } });

io.on("connection", (socket) => {
  console.log("Connected:", socket.id);

  socket.on("disconnect", (reason) => {
    console.log("-- Disconnected:", socket.id, "reason:", reason);
  });
});

// Emit sample data every 2s
const interval = setInterval(() => {
  const data = {
    timestamp: Date.now(),
    value: Math.random(),
  };
  io.emit("stream-data", data);
}, 2000);

// Simple health endpoint
app.get("/", (_req, res) => res.json({ ok: true }));

// Clean shutdown
function shutdown() {
  console.log("\nShutting down...");
  if (interval) clearInterval(interval);
  io.close(() => {
    server.close(() => process.exit(0));
  });
}
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Socket running on http://localhost:${PORT}`);
});

Run it:

bash
npm run start
# or with hot reload
npm run dev

You should see:

typescript
Socket running on http://localhost:3000

3) Minimal web client — quick sanity check

Create a simple client.html next to server.js and open it in your browser. It will connect and print events.

html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Socket.IO Client</title>
    <style>
      body { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; padding: 16px; }
      #log { white-space: pre-wrap; }
    </style>
    <script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
  </head>
  <body>
    <h1>Socket.IO Client</h1>
    <div id="log"></div>
    <script>
      const log = (msg) => (document.getElementById("log").textContent += msg + "\n");
      const socket = io("http://localhost:3000", { transports: ["websocket"] });

      socket.on("connect", () => log("connected: " + socket.id));
      socket.on("disconnect", (reason) => log("disconnected: " + reason));
      socket.on("stream-data", (payload) => log("stream-data: " + JSON.stringify(payload)));

      window.addEventListener("beforeunload", () => {
        socket.close();
      });
    </script>
  </body>
</html>

Open client.html in the browser. You should see stream-data entries every 2 seconds.

4) Example: simulated device feed (optional)

If you need more “device-like” messages (e.g., chargers), swap the emitter with this example that updates or inserts devices by deviceId. This mirrors the logic you sketched and demonstrates incremental updates.

js
// Replace the 2s interval with this 1s device ticker
const devices = new Map();

function upsertDevice(msg) {
  devices.set(msg.deviceId, msg);
  io.emit("stream-data", msg);
}

function randomDeviceId() {
  return "charge-00" + Math.floor(Math.random() * 10);
}

function makeDevice() {
  return {
    deviceId: randomDeviceId(),
    timestamp: Date.now(),
    status: Math.random() > 0.2 ? "online" : "offline",
    currentPowerKw: parseFloat((Math.random() * 100).toFixed(1)),
    activeSessions: Math.floor(Math.random() * 3),
    alarms: Math.random() > 0.8 ? ["highTemperature"] : [],
  };
}

// Seed a couple of starters
upsertDevice({
  deviceId: "charge-001",
  timestamp: Date.now(),
  status: "online",
  currentPowerKw: 33.4,
  activeSessions: 1,
  alarms: [],
});
upsertDevice({
  deviceId: "charge-002",
  timestamp: Date.now(),
  status: "offline",
  currentPowerKw: 21.8,
  activeSessions: 0,
  alarms: ["noConnection", "error"],
});

// Tick every second
const deviceInterval = setInterval(() => {
  upsertDevice(makeDevice());
}, 1000);

// Remember to clear both on shutdown
function shutdown() {
  console.log("\nShutting down...");
  if (deviceInterval) clearInterval(deviceInterval);
  if (interval) clearInterval(interval);
  io.close(() => {
    server.close(() => process.exit(0));
  });
}
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);

This produces realistic updates that your client can merge by deviceId. If an id exists, overwrite it; if not, add it. That keeps your UI in sync with the stream.

5) Minimal Node client (for integration tests)

If you prefer to test from Node, run this client in a separate process:

js
// node-client.js
const { io } = require("socket.io-client");

const socket = io("http://localhost:3000", { transports: ["websocket"] });

socket.on("connect", () => console.log("connected:", socket.id));
socket.on("stream-data", (msg) => console.log("stream-data:", msg));
socket.on("disconnect", (reason) => console.log("disconnected:", reason));

process.on("SIGINT", () => {
  socket.close();
  process.exit(0);
});

Run:

bash
node node-client.js

You should see live messages in the terminal.

6) Event names you can use

  • connection (server-side) — when a client connects
  • disconnect (both sides) — when either end closes
  • stream-data (custom) — your live payload
  • You can add more channels like device-updated, telemetry, heartbeat

Keep event names short and consistent.

7) Common tweaks

  • Port: set PORT=4000 to move the server.
  • CORS: lock origin down to your app’s URL when you move beyond local testing.
  • Transport: force WebSocket in clients with { transports: ["websocket"] } if you want to skip long-polling.

8) Clean shutdown

For local dev, avoid orphaned intervals and open sockets. Clear timers and close the server on SIGINT/SIGTERM as shown above. If you add more timers, clear them in the same shutdown() function.

You’re done

You now have a tiny Socket.IO service that emits predictable data and a minimal client to consume it. Use this to iterate fast on your UI state, reconnection logic, and data merge strategy.