Back to Scaling Node.js Applications guides

Getting Started with Express WebSockets

Stanley Ulili
Updated on August 12, 2025

Express.js remains the preferred framework for developing Node.js web applications, offering ease of use and adaptability. Besides handling traditional HTTP requests, Express works smoothly with WebSocket libraries to power real-time apps like live dashboards, multiplayer games, and instant messaging services.

Since Express doesn't have built-in WebSocket support, combining it with the popular ws library creates a strong setup for real-time communication. This method provides the familiar Express routing and middleware system along with reliable WebSocket capabilities.

This tutorial guides you through building WebSocket features with Express from scratch. You'll learn about connection management, message broadcasting, and client handling. Once you're done, you'll have the skills to add live updates to any Express app.

Prerequisites

To develop WebSocket applications with Express, you need Node.js version 14 or higher and npm installed on your system. This tutorial assumes familiarity with Express.js fundamentals and basic JavaScript asynchronous programming concepts.

Creating your Express WebSocket project

Working with WebSockets requires a dedicated development environment, so we'll set up a new project. Start by initializing a fresh Node.js project and organizing the required directory structure.

 
mkdir express-websockets && cd express-websockets
 
npm init -y
 
npm pkg set type="module"

The first command creates and navigates into a new project folder. The npm init -y command sets up a package.json file with default settings, while the third command configures your project to use ES modules, allowing modern import/export syntax.

Install Express alongside the WebSocket library and additional tools for a complete development setup:

 
npm install express ws cors

Create your main server file with the foundational Express configuration using ES modules:

server.js
import express from 'express';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';

const app = express();
const server = createServer(app);

app.use(express.static('public'));

const PORT = process.env.PORT || 3000;

This code imports the necessary modules using ES module syntax. The createServer function wraps your Express app in an HTTP server, which is required for WebSocket integration. The static middleware serves files from a public directory we'll create next.

Now, build a client-side interface for testing your WebSocket implementation. Create a public directory and add an HTML file that will serve as your testing ground:

 
mkdir public
server.js
import express from 'express';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';

const app = express();
const server = createServer(app);

app.use(express.static('public'));

const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Express WebSocket Demo</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
#messages { border: 1px solid #ccc; height: 300px;
overflow-y: scroll; padding: 10px; margin-bottom: 10px; }
#messageInput { width: 300px; padding: 5px; }
button { padding: 5px 10px; }
</style>
</head>
<body>
<h1>Express WebSocket Demo</h1>
<div id="messages"></div>
<input type="text" id="messageInput" placeholder="Enter your message">
<button onclick="sendMessage()">Send Message</button>
<script>
const ws = new WebSocket('ws://localhost:3000');
const messages = document.getElementById('messages');
ws.onmessage = function(event) {
const messageDiv = document.createElement('div');
messageDiv.textContent = event.data;
messages.appendChild(messageDiv);
messages.scrollTop = messages.scrollHeight;
};
function sendMessage() {
const input = document.getElementById('messageInput');
if (input.value) {
ws.send(input.value);
input.value = '';
}
}
document.getElementById('messageInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
</script>
</body>
</html>
`);
});
server.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});

This route serves an HTML page with embedded CSS and JavaScript. The client-side script creates a WebSocket connection to ws://localhost:3000 and handles message display and sending. The interface includes auto-scrolling messages and Enter key support for better user experience.

Launch your development server to confirm the basic setup functions correctly:

 
node --watch server.js

Navigate to http://localhost:3000 in your browser to view the demo interface:

Screenshot of the demo interface

The WebSocket functionality isn't operational yet since we haven't implemented the WebSocket server.

Building your first WebSocket server

Now that your Express application and client interface are ready, you need to create a WebSocket server that handles real-time connections. This enables your browser client to establish persistent connections for instant message exchange.

Add the WebSocket server implementation to your server.js file:

server.js
import express from 'express';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';

const app = express();
const server = createServer(app);

app.use(express.static('public'));

const PORT = process.env.PORT || 3000;

// ... HTML template code from above ...

const wss = new WebSocketServer({ server });
wss.on('connection', function connection(ws) {
console.log('New client connected');
ws.on('message', function message(data) {
const messageText = data.toString();
console.log('Received:', messageText);
ws.send(`Echo: ${messageText}`);
});
ws.on('close', function close() {
console.log('Client disconnected');
});
});
server.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); });

The WebSocketServer constructor creates a WebSocket server attached to your HTTP server. The connection event fires when clients connect, giving you access to individual WebSocket instances. The message event handler processes incoming text data, while the close event manages disconnections.

Your server should restart and then return to http://localhost:3000. Type a message and press Enter or click Send Message:

Screenshot of message input

Your message should appear echoed back with an "Echo:" prefix in real time:

Screenshot of echoed message

You've successfully created your first Express WebSocket server. Next, you'll learn how to handle connection errors robustly.

Handling WebSocket connection states

WebSocket connections in Express applications have distinct lifecycle phases that require careful management for production-ready applications. Understanding these states helps you build resilient real-time features that gracefully handle network interruptions and client disconnections.

Your current implementation works for basic scenarios but lacks comprehensive error handling. Let's enhance it with proper connection state management:

server.js
import express from 'express';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';

const app = express();
const server = createServer(app);

app.use(express.static('public'));

const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
    res.send(`
...
    `);
});

const wss = new WebSocketServer({
server,
clientTracking: true
});
wss.on('connection', function connection(ws, request) {
const clientIP = request.socket.remoteAddress;
console.log(`New client connected from ${clientIP}`);
// Send welcome message
ws.send('Welcome to the WebSocket server!');
ws.on('message', function message(data) {
try {
const messageText = data.toString();
console.log('Received:', messageText);
if (ws.readyState === ws.OPEN) {
ws.send(`Echo: ${messageText}`);
}
} catch (error) {
console.error('Error processing message:', error);
}
});
ws.on('close', function close(code, reason) {
console.log(`Client disconnected - Code: ${code}, Reason: ${reason}`);
});
ws.on('error', function error(err) {
console.error('WebSocket error:', err);
});
// Handle connection ping/pong for keep-alive
ws.on('pong', function heartbeat() {
ws.isAlive = true;
});
ws.isAlive = true;
});
// Ping clients periodically to detect broken connections
const interval = setInterval(function ping() {
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) {
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, 30000);
wss.on('close', function close() {
clearInterval(interval);
});
server.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); });

This enhanced version includes several critical improvements. The clientTracking option enables automatic client management, while the readyState check ensures messages only go to open connections. The ping/pong mechanism detects broken connections that haven't closed properly, preventing memory leaks.

The connection now captures client IP addresses and provides detailed disconnection information. The interval-based heartbeat system pings clients every 30 seconds to maintain connection health.

Restart your server and test at http://localhost:3000:

Screenshot of enhanced server

When you close the browser tab or refresh the page, you'll see detailed disconnection information in your terminal instead of silent failures:

Output
Restarting 'server.js'
Server running on http://localhost:3000
New client connected from ::1
Client disconnected - Code: 1001, Reason: 

This robust error handling is crucial for applications where connection stability matters.

Broadcasting messages to multiple clients

Real-world WebSocket applications typically need to communicate with multiple clients simultaneously. Your current setup only echoes messages back to the sender. To create interactive experiences like chat rooms or live updates, you need broadcasting capabilities.

Implement a message broadcasting system that shares messages among all connected clients:

server.js
...
app.use(express.static("public"));

const PORT = process.env.PORT || 3000;

app.get("/", (req, res) => {
  res.send(`
...
    `);
});

class ConnectionManager {
constructor() {
this.clients = new Set();
}
addClient(ws) {
this.clients.add(ws);
console.log(`Client added. Total clients: ${this.clients.size}`);
}
removeClient(ws) {
this.clients.delete(ws);
console.log(`Client removed. Total clients: ${this.clients.size}`);
}
broadcast(message, sender = null) {
this.clients.forEach(client => {
if (client !== sender && client.readyState === client.OPEN) {
try {
client.send(message);
} catch (error) {
console.error('Error broadcasting to client:', error);
this.removeClient(client);
}
}
});
}
getClientCount() {
return this.clients.size;
}
}
const connectionManager = new ConnectionManager();
const wss = new WebSocketServer({ server, clientTracking: true }); wss.on("connection", function connection(ws, request) { const clientIP = request.socket.remoteAddress; console.log(`New client connected from ${clientIP}`); // Send welcome message ws.send('Welcome to the WebSocket server!');
connectionManager.addClient(ws);
ws.send(`Welcome! There are ${connectionManager.getClientCount()} clients connected.`);
// Notify other clients about new connection
connectionManager.broadcast(`A new user joined the chat!`, ws);
ws.on("message", function message(data) { try { const messageText = data.toString(); console.log("Received:", messageText);
// Broadcast message to all other clients
connectionManager.broadcast(`User says: ${messageText}`, ws);
} catch (error) { console.error('Error processing message:', error); } }); ws.on("close", function close(code, reason) {
connectionManager.removeClient(ws);
connectionManager.broadcast(`A user left the chat.`);
console.log(`Client disconnected - Code: ${code}, Reason: ${reason}`); }); ... }); ...

The ConnectionManager class maintains a Set of active WebSocket connections, providing clean methods for adding, removing, and broadcasting to clients. The broadcast method excludes the sender to prevent message echoing and includes error handling for broken connections.

When new clients connect, they receive a welcome message showing the current client count, while existing clients are notified of the new arrival. Messages from any client are broadcast to all others, creating a real-time chat experience.

Restart your server and open multiple browser tabs to http://localhost:3000. You'll see welcome messages, client count updates, and join notifications as new tabs connect:

Screenshot showing multiple welcome messages and user join notifications

Send a message from one tab and watch it appear instantly in all other tabs:

Screenshot of broadcast message

You've just built a functional real-time chat system with Express and WebSockets.

Final thoughts

You've built a comprehensive real-time WebSocket application with Express.js. From a simple echo server, you've advanced to a multi-client broadcasting system that handles connections smoothly.

For more tips on WebSocket optimization and advanced techniques, don't forget to check out the ws library documentation and Express.js advanced guides.

Got an article suggestion? Let us know
Next article
Getting Started with Fastify WebSockets
Learn to build real-time WebSocket applications with Fastify. Complete tutorial covering connection handling, message broadcasting, and error management.
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.