Chapter 2 — Server Setup

On the server side, Node.js is the most popular choice for WebSocket because it's event-driven and non-blocking — perfect for handling thousands of persistent connections. The ws library is the standard: fast, minimal, zero dependencies.


Installation

npm init -y
npm install ws

# For TypeScript:
npm install ws @types/ws

Basic Standalone Server

// server.js
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws, req) => {
  const clientIP = req.socket.remoteAddress;
  console.log('New client:', clientIP);
  console.log('Total connected:', wss.clients.size);

  // Send welcome message to THIS client only
  ws.send(JSON.stringify({
    type:    'welcome',
    message: 'Connected! 🎉',
    clients: wss.clients.size,
  }));

  // Handle incoming messages
  ws.on('message', (rawData) => {
    let msg;
    try {
      msg = JSON.parse(rawData);
    } catch {
      ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' }));
      return;
    }

    console.log('Received:', msg);

    // Echo back with server timestamp
    ws.send(JSON.stringify({
      type:      'echo',
      original:  msg,
      timestamp: Date.now(),
    }));
  });

  ws.on('close', (code, reason) => {
    console.log('Client left. Code:', code, '| Reason:', reason.toString());
    console.log('Remaining clients:', wss.clients.size);
  });

  ws.on('error', (err) => {
    console.error('Socket error:', err.message);
  });
});

wss.on('listening', () => {
  console.log('🚀 WebSocket server running on ws://localhost:8080');
});

Browser Client

// index.html — inside <script> tag
const ws = new WebSocket('ws://localhost:8080');

ws.onopen = () => {
  console.log('✅ Connected!');
  ws.send(JSON.stringify({ type: 'hello', text: 'Hey server!' }));
};

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

ws.onerror = (error) => {
  console.error('❌ Error:', error);
};

ws.onclose = (event) => {
  console.log('🔌 Disconnected. Code:', event.code, '| Clean:', event.wasClean);
};

// Check readyState before sending:
// ws.readyState === 0 → CONNECTING
// ws.readyState === 1 → OPEN (can send)
// ws.readyState === 2 → CLOSING
// ws.readyState === 3 → CLOSED

Production Pattern — Express + WebSocket on Same Port

In production, attach WebSocket to an existing HTTP server so both REST and WebSocket share port 443:

// server.js
const express   = require('express');
const http      = require('http');
const WebSocket = require('ws');

const app    = express();
const server = http.createServer(app);       // HTTP server
const wss    = new WebSocket.Server({ server }); // WS on SAME port!

// Normal REST routes still work
app.use(express.json());
app.get('/health', (req, res) => res.json({ status: 'ok' }));
app.get('/api/users', (req, res) => res.json({ users: [] }));

// WebSocket connections
wss.on('connection', (ws) => {
  ws.send(JSON.stringify({ type: 'connected' }));

  ws.on('message', (data) => {
    // handle messages...
  });
});

// One port for everything
server.listen(3000, () => {
  console.log('REST API  → <http://localhost:3000/api>');
  console.log('WebSocket → ws://localhost:3000');
});

readyState Reference

Value Constant Meaning
0 CONNECTING Handshake in progress
1 OPEN Ready to send and receive
2 CLOSING Close handshake started
3 CLOSED Connection terminated

Always check ws.readyState === 1 before calling ws.send() — sending on a non-OPEN socket throws an error.