Welcome back to our Real-Time WebSocket Architecture series! In Part 1, we covered WebSocket fundamentals, the protocol handshake, and when to use WebSockets. Now it’s time to get hands-on and build our first WebSocket server using Node.js.
By the end of this tutorial, you’ll have a working real-time chat application with both server and client implementations. Let’s dive in!
What We’ll Build
Today we’re building a simple but functional real-time chat server that demonstrates:
- WebSocket server setup with Node.js
- Client-side WebSocket connections
- Bidirectional message exchange
- Broadcasting messages to multiple clients
- Connection event handling
- Basic error management
Socket.io vs ws Library: Which Should You Choose?
Before we start coding, let’s understand the two main Node.js WebSocket libraries and when to use each:
The ws Library
Best for: Performance-critical applications, microservices, minimal overhead
Pros:
- Blazing fast (can handle 50K+ connections per server)
- Lightweight (~50KB)
- Low-level control over WebSocket protocol
- Pure WebSocket implementation (RFC 6455 compliant)
- Minimal dependencies
Cons:
- No automatic reconnection
- Manual heartbeat/ping-pong implementation
- No built-in rooms or namespaces
- More boilerplate code required
Socket.io Library
Best for: Full-featured applications, chat systems, collaborative tools
Pros:
- Automatic reconnection with exponential backoff
- Built-in rooms and namespaces
- Automatic fallback to HTTP long-polling
- Event-based architecture
- Broadcasting made easy
- Acknowledgment callbacks
Cons:
- Larger bundle size (~180KB)
- Custom protocol (not pure WebSocket)
- Slightly higher overhead
- Cannot connect with standard WebSocket clients
Decision Matrix
graph TD A[Need WebSocket Server?] --> B{What's your priority?} B -->|Raw Performance| C[Use ws] B -->|Developer Experience| D[Use Socket.io] B -->|Standard Protocol| C B -->|Rich Features| D C --> E{Need rooms/namespaces?} E -->|Yes| F[Build custom or use Socket.io] E -->|No| G[ws is perfect!] D --> H{Size matters?} H -->|Yes| I[Consider ws] H -->|No| J[Socket.io is great!] style C fill:#90EE90 style D fill:#87CEEB style G fill:#98FB98 style J fill:#ADD8E6
For this tutorial, we’ll use Socket.io because it provides excellent features out of the box and reduces boilerplate code, making it perfect for learning. In later parts, we’ll explore the ws library for performance optimization.
Prerequisites
Before we begin, ensure you have:
- Node.js installed (v18+ recommended)
- npm or yarn package manager
- Basic JavaScript knowledge
- A code editor (VS Code recommended)
- Terminal/command line familiarity
Verify your installation:
node --version
npm --version
Project Setup
Step 1: Create Project Directory
mkdir realtime-chat-app
cd realtime-chat-app
npm init -y
Step 2: Install Dependencies
npm install express socket.io
npm install --save-dev nodemon
What we installed:
- express: Web framework for serving our HTML client
- socket.io: WebSocket library for real-time communication
- nodemon: Auto-restarts server on file changes
Step 3: Project Structure
realtime-chat-app/
├── server.js
├── public/
│ ├── index.html
│ ├── style.css
│ └── client.js
└── package.json
mkdir public
Building the WebSocket Server
Create server.js
:
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const path = require('path');
const app = express();
const server = http.createServer(app);
const io = new Server(server);
app.use(express.static(path.join(__dirname, 'public')));
const users = new Map();
io.on('connection', (socket) => {
console.log(`User connected: ${socket.id}`);
socket.on('user-joined', (username) => {
users.set(socket.id, username);
io.emit('user-connected', {
username: username,
userId: socket.id,
timestamp: new Date().toISOString()
});
console.log(`${username} joined the chat`);
});
socket.on('send-message', (message) => {
const username = users.get(socket.id);
io.emit('receive-message', {
username: username,
message: message,
userId: socket.id,
timestamp: new Date().toISOString()
});
});
socket.on('typing', (isTyping) => {
const username = users.get(socket.id);
socket.broadcast.emit('user-typing', {
username: username,
isTyping: isTyping
});
});
socket.on('disconnect', () => {
const username = users.get(socket.id);
if (username) {
io.emit('user-disconnected', {
username: username,
userId: socket.id,
timestamp: new Date().toISOString()
});
users.delete(socket.id);
}
});
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Server Architecture
sequenceDiagram participant Client participant Server participant OtherClients Client->>Server: connect() Server->>Client: connection established Client->>Server: user-joined (username) Server->>Server: Store in users Map Server->>OtherClients: user-connected event Server->>Client: user-connected event Client->>Server: send-message (text) Server->>OtherClients: receive-message Server->>Client: receive-message Client->>Server: typing (true/false) Server->>OtherClients: user-typing event Client->>Server: disconnect Server->>Server: Remove from users Map Server->>OtherClients: user-disconnected event
Building the Client
Create public/index.html
:
<!DOCTYPE html>
<html>
<head>
<title>Real-Time Chat</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="username-modal" class="modal">
<div class="modal-content">
<h2>Welcome to Chat</h2>
<input type="text" id="username-input" placeholder="Enter username">
<button id="join-btn">Join</button>
</div>
</div>
<div id="chat-container" style="display:none">
<div class="chat-header">
<h1>Chat</h1>
<span id="user-info"></span>
</div>
<div id="messages-container"></div>
<div id="typing-indicator"></div>
<div class="input-container">
<input type="text" id="message-input" placeholder="Type message">
<button id="send-btn">Send</button>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="client.js"></script>
</body>
</html>
Create public/client.js
:
const socket = io();
let username = '';
const joinBtn = document.getElementById('join-btn');
const sendBtn = document.getElementById('send-btn');
const usernameInput = document.getElementById('username-input');
const messageInput = document.getElementById('message-input');
const usernameModal = document.getElementById('username-modal');
const chatContainer = document.getElementById('chat-container');
const messagesContainer = document.getElementById('messages-container');
joinBtn.onclick = () => {
username = usernameInput.value.trim();
if (username) {
socket.emit('user-joined', username);
usernameModal.style.display = 'none';
chatContainer.style.display = 'flex';
}
};
sendBtn.onclick = () => {
const message = messageInput.value.trim();
if (message) {
socket.emit('send-message', message);
messageInput.value = '';
}
};
socket.on('receive-message', (data) => {
const div = document.createElement('div');
div.className = 'message';
div.textContent = `${data.username}: ${data.message}`;
messagesContainer.appendChild(div);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
});
Running the Application
node server.js
Open http://localhost:3000
in multiple browser tabs to test!
What’s Next
In Part 3: Essential Features, we’ll add rooms, namespaces, and advanced event handling!
Part 2 of the 8-part Real-Time WebSocket Architecture Series.
One thought on “Real-Time WebSocket Architecture Series: Part 2 – Building Your First WebSocket Server (Node.js)”
Comments are closed.