DEV Community

1xApi
1xApi

Posted on • Originally published at 1xapi.com

How to Build Real-Time APIs with WebSockets in Node.js (2026 Guide)

How to Build Real-Time APIs with WebSockets in Node.js (2026 Guide)

As of February 2026, real-time communication has become essential for modern applications. Whether you are building chat apps, live dashboards, or collaborative tools, WebSockets provide the bidirectional, low-latency communication that HTTP request-response cannot match.

In this guide, you will learn how to build production-ready real-time APIs using Node.js and the native WebSocket API (available since Node.js 21.7.0).

Why WebSockets in 2026?

HTTP polling wastes bandwidth with repeated headers. WebSockets maintain a persistent TCP connection, enabling:

  • True bidirectional communication - server pushes data without client requests
  • Lower latency - no connection overhead after initial handshake
  • Reduced bandwidth - minimal framing overhead vs HTTP polling

Node.js 22+ includes a stable native WebSocket implementation in the ws module—no external dependencies required for basic use.

Setting Up Your Project

Initialize a new Node.js project with ES modules:

\bash
mkdir realtime-api && cd realtime-api
npm init -y
\
\

Create your server file:

\`javascript
// server.mjs
import { createServer } from "http";
import { WebSocketServer } from "ws";

const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok", time: new Date().toISOString() }));
});

const wss = new WebSocketServer({ server });

// Track connected clients
const clients = new Set();

wss.on("connection", (ws, req) => {
const clientId = crypto.randomUUID();
ws.id = clientId;
clients.add(ws);

console.log(Client connected: ${clientId} from ${req.socket.remoteAddress});

// Send welcome message
ws.send(JSON.stringify({
type: "welcome",
clientId,
message: "Connected to real-time API"
}));

ws.on("message", (data) => {
try {
const message = JSON.parse(data.toString());
handleMessage(ws, message);
} catch (err) {
ws.send(JSON.stringify({ type: "error", message: "Invalid JSON" }));
}
});

ws.on("close", () => {
clients.delete(ws);
console.log(Client disconnected: ${clientId});
});

ws.on("error", (err) => {
console.error(WebSocket error for ${clientId}:, err.message);
});
});

function handleMessage(ws, message) {
switch (message.type) {
case "ping":
ws.send(JSON.stringify({ type: "pong", timestamp: Date.now() }));
break;
case "broadcast":
broadcastToAll({ type: "broadcast", data: message.data, from: ws.id });
break;
default:
ws.send(JSON.stringify({
type: "error",
message: Unknown message type: ${message.type}
}));
}
}

function broadcastToAll(message) {
const payload = JSON.stringify(message);
clients.forEach((client) => {
if (client.readyState === 1) { // WebSocket.OPEN
client.send(payload);
}
});
}

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(Server running on http://localhost:${PORT});
console.log(WebSocket server ready at ws://localhost:${PORT});
});
`\

Handling Message Types

A robust real-time API needs structured message handling:

\`javascript
// Message handler with typed messages
const messageHandlers = {
// Client requests current state
getState: (ws) => {
ws.send(JSON.stringify({
type: "state",
data: { connectedClients: clients.size, uptime: process.uptime() }
}));
},

// Client sends private message to another client
privateMessage: (ws, { targetId, content }) => {
const target = findClientById(targetId);
if (target) {
target.send(JSON.stringify({
type: "privateMessage",
content,
from: ws.id,
timestamp: Date.now()
}));
} else {
ws.send(JSON.stringify({
type: "error",
message: "Target client not found"
}));
}
},

// Client subscribes to a channel
subscribe: (ws, { channel }) => {
ws.channels = ws.channels || new Set();
ws.channels.add(channel);
ws.send(JSON.stringify({
type: "subscribed",
channel
}));
}
};

function findClientById(id) {
for (const client of clients) {
if (client.id === id) return client;
}
return null;
}
`\

Broadcasting to Channels

For larger applications, implement channel-based messaging:

\`javascript
const channels = new Map();

function joinChannel(ws, channelName) {
if (!channels.has(channelName)) {
channels.set(channelName, new Set());
}
channels.get(channelName).add(ws);
ws.send(JSON.stringify({ type: "joined", channel: channelName }));
}

function broadcastToChannel(channelName, message) {
const channel = channels.get(channelName);
if (!channel) return;

const payload = JSON.stringify(message);
channel.forEach((client) => {
if (client.readyState === 1) {
client.send(payload);
}
});
}
`\

Client-Side Implementation

Here is how to connect from a browser or Node.js client:

\`javascript
// client.mjs
const ws = new WebSocket("ws://localhost:3000");

ws.onopen = () => {
console.log("Connected to WebSocket server");

// Request current state
ws.send(JSON.stringify({ type: "getState" }));

// Subscribe to updates
ws.send(JSON.stringify({ type: "subscribe", channel: "updates" }));
};

ws.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log("Received:", message.type, message);
};

ws.onclose = () => {
console.log("Disconnected from server");
};

// Send messages
ws.send(JSON.stringify({ type: "broadcast", data: "Hello everyone!" }));
`\

Implementing Heartbeats

WebSocket connections can drop silently. Implement heartbeats to detect dead connections:

\`javascript
// Server-side heartbeat
const HEARTBEAT_INTERVAL = 30000; // 30 seconds

wss.on("connection", (ws) => {
ws.isAlive = true;

ws.on("pong", () => {
ws.isAlive = true;
});
});

// Heartbeat interval
setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
clients.delete(ws);
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, HEARTBEAT_INTERVAL);
`\

Running and Testing

Start your server:

\bash
node server.mjs
\
\

Test with a simple HTML client:

\html
<!DOCTYPE html>
<html>
<head><title>WebSocket Test</title></head>
<body>
<pre id="output"></pre>
<script>
const ws = new WebSocket("ws://localhost:3000");
ws.onmessage = (e) => {
document.getElementById("output").textContent += e.data + "\\n";
};
</script>
</body>
</html>
\
\

Production Considerations

For production deployments in 2026:

  1. Use TLS (WSS) - Secure WebSocket connections with SSL certificates
  2. Implement reconnection logic - Clients should handle connection drops gracefully
  3. Add rate limiting - Prevent message flooding attacks
  4. Scale with Redis - For multi-server deployments, use Redis Pub/Sub
  5. Monitor with observability - Track connection counts, message throughput, latency

Conclusion

WebSockets enable real-time, bidirectional communication that transforms user experiences. With native WebSocket support in Node.js 22+, building real-time APIs has never been simpler.

Start with the basics outlined in this guide, then add features like authentication, message persistence, and channel management as your application grows.

Top comments (0)