Detailed explanation with flow:
1. What happens normally with Express
When you write:
import express from 'express';
const app = express();
app.listen(3000);
- Express automatically creates an internal HTTP server using Node’s
httpmodule. - You don’t see it, but internally it does:
const http = require('http');
const server = http.createServer(app);
server.listen(3000);
- This works fine for normal HTTP requests (
GET,POST, etc.). - However, WebSockets are not normal HTTP. They start as HTTP and then upgrade to a persistent TCP connection.
2. How WebSockets work under the hood
When a client connects via:
const ws = new WebSocket('ws://localhost:3000');
The browser doesn’t immediately open a WebSocket. It first sends an HTTP request like this:
GET / HTTP/1.1
Host: localhost:3000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: abc123==
Sec-WebSocket-Version: 13
This is called an HTTP Upgrade request.
The server must respond properly (with special headers) to switch the protocol from HTTP → WebSocket.
But Express does not know how to handle this kind of upgrade request — it’s built only for normal HTTP traffic.
3. Why you need createServer(app)
To handle this “Upgrade” event, you must access the raw Node HTTP server.
That’s why you write:
import { createServer } from 'http';
const server = createServer(app);
Now server is the actual low-level HTTP server that Express is attached to.
This gives you full control over its events, including:
-
'request'— normal HTTP requests (handled by Express) -
'upgrade'— special WebSocket handshake requests
You couldn’t access the 'upgrade' event if you just did app.listen().
4. Why you need WebSocketServer
The ws library provides a class called WebSocketServer that knows how to:
- Detect upgrade requests
- Perform the WebSocket handshake
- Maintain open socket connections
- Handle
message,close, etc.
You attach it to your existing HTTP server:
const wss = new WebSocketServer({ server });
This tells the ws library:
“Listen to this HTTP server. Whenever you see an Upgrade request, turn it into a WebSocket connection.”
So your one server now supports both:
- Express (normal HTTP routes)
- WebSockets (real-time communication)
5. Full flow diagram
Browser ───HTTP GET───> Express app (normal request)
───Upgrade req─> Node HTTP server (detected by ws)
↓
WebSocketServer
↓
Bi-directional socket open
6. Code summary
import express from 'express';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
const app = express();
const server = createServer(app); // step 1: expose HTTP server
const wss = new WebSocketServer({ server }); // step 2: attach WS to it
app.get('/', (req, res) => res.send('Hello HTTP'));
wss.on('connection', (socket) => {
console.log('New WebSocket connection');
socket.send('Hello WebSocket');
});
server.listen(3000, () => console.log('Server running on port 3000'));
In short:
| Component | Purpose |
|---|---|
createServer(app) |
Gives you control of the real HTTP server so you can handle upgrades |
WebSocketServer({ server }) |
Handles WebSocket handshakes and keeps live connections |
| Express | Handles normal HTTP routes |
| WebSocket | Handles persistent two-way communication |
Top comments (0)