Real-Time WebSocket Architecture Series: Part 2 – Building Your First WebSocket Server (Node.js)

Real-Time WebSocket Architecture Series: Part 2 – Building Your First WebSocket Server (Node.js)

This entry is part 2 of 8 in the series Real-Time WebSocket Architecture Series

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.

Navigate<< Real-Time WebSocket Architecture Series: Part 1 – Understanding WebSocket FundamentalsReal-Time WebSocket Architecture Series: Part 3 – Essential Features (Rooms, Namespaces & Events) >>

Written by:

373 Posts

View All Posts
Follow Me :

One thought on “Real-Time WebSocket Architecture Series: Part 2 – Building Your First WebSocket Server (Node.js)

Comments are closed.