You learned that native WebSockets give you a persistent connection with four events. That's the foundation, but building a real app on top of raw WebSockets means reinventing a lot of wheels: reconnection logic, room management, fallback handling. Socket.io builds those wheels for you.
What native WebSockets are missing
The browser WebSocket APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. is low-level by design. It sends strings and fires events. That's it. Everything else, rooms, reconnection, reliable delivery, fallbacks for corporate firewalls, you have to write yourself.
Socket.io adds all of that out of the box:
| Feature | Native WebSocket | Socket.io |
|---|---|---|
| Automatic reconnection | No, you build it | Yes, built in |
| Fallback if WebSocket is blocked | No | Yes (long polling) |
| Rooms / channels | No, you track manually | Yes, built in |
| Send to all except sender | No, loop through clients | Yes (socket.broadcast) |
| Message delivery confirmation | No | Yes (acknowledgments) |
| Middleware support | No | Yes |
| Namespaces | No | Yes |
Setting up Socket.io
Socket.io has two parts: a server package and a client package. Both must be installed, and their major versions should match.
Installation
# Server
npm install socket.io
# Client (or use the CDN the server exposes automatically)
npm install socket.io-clientServer setup with Express
Socket.io attaches to a Node.js HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. server, not directly to Express. You create the HTTP server from Express, then wrap it:
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: 'http://localhost:3000',
methods: ['GET', 'POST']
}
});
// Every new client connection triggers this callback
io.on('connection', (socket) => {
console.log('A user connected:', socket.id);
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
});
});
server.listen(3001, () => {
console.log('Server running on :3001');
});Each connected client gets a unique socket.id. The io object represents the entire server; socket represents one specific client.
Client setup
import { io } from 'socket.io-client';
const socket = io('http://localhost:3001');
socket.on('connect', () => {
console.log('Connected! My ID:', socket.id);
});
socket.on('disconnect', (reason) => {
console.log('Disconnected:', reason);
});const socket = io(). Socket.io resolves the server automatically from the current page's origin.Sending and receiving events
Socket.io is entirely event-based. You emit named events with data, and listen for named events on the other end. The pattern is identical on both client and server.
// CLIENT: send a chat message
socket.emit('chat-message', {
text: 'Hello everyone!',
room: 'general'
});
// SERVER: receive it and broadcast to the room
io.on('connection', (socket) => {
socket.on('chat-message', (data) => {
io.to(data.room).emit('chat-message', {
user: socket.username,
text: data.text,
timestamp: new Date().toISOString()
});
});
});
// CLIENT: listen for incoming messages
socket.on('chat-message', (data) => {
appendMessage(data.user, data.text);
});The different ways to send from the server
This is where people get confused first. There are several targets when emitting from the server side:
| Method | Who receives it? |
|---|---|
socket.emit('event', data) | Only this specific client |
socket.broadcast.emit('event', data) | Every client except this one |
io.emit('event', data) | Every connected client, including this one |
io.to('room').emit('event', data) | Everyone in a specific room |
socket.to('room').emit('event', data) | Everyone in the room except this client |
Rooms
Rooms are named channels that sockets can joinWhat is join?A SQL operation that combines rows from two or more tables based on a shared column, letting you query related data in one request. and leave freely. They're the core tool for organizing users, chat channels, game lobbies, document sessions, notification groups.
io.on('connection', (socket) => {
// Client requests to join a room
socket.on('join-room', (roomName) => {
socket.join(roomName);
// Notify everyone else already in the room
socket.to(roomName).emit('user-joined', {
user: socket.username,
room: roomName
});
});
// Client leaves a room
socket.on('leave-room', (roomName) => {
socket.leave(roomName);
socket.to(roomName).emit('user-left', {
user: socket.username
});
});
});A socket can be in multiple rooms simultaneously. Every socket is also automatically in a private room identified by its own socket.id, useful for direct messages between users.
socket.rooms (a JavaScript Set). To count how many clients are in a room: io.sockets.adapter.rooms.get('general')?.size ?? 0.Quick reference
| Operation | Code |
|---|---|
| Send to one client | socket.emit('event', data) |
| Send to everyone except sender | socket.broadcast.emit('event', data) |
| Send to everyone | io.emit('event', data) |
| Join a room | socket.join('room-name') |
| Leave a room | socket.leave('room-name') |
| Send to room (excluding sender) | socket.to('room').emit('event', data) |
| Send to room (including sender) | io.to('room').emit('event', data) |
| Get socket ID | socket.id |
| Disconnect a client | socket.disconnect() |
// Complete chat server with Socket.io
// ============ SERVER (server.js) ============
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = new Server(server, { cors: { origin: '*' } });
const users = new Map(); // socket.id → username
io.on('connection', (socket) => {
console.log('🟢 New connection:', socket.id);
// User sets their username
socket.on('set-username', (username) => {
socket.username = username;
users.set(socket.id, username);
socket.broadcast.emit('user-joined', { user: username });
io.emit('user-count', users.size);
});
// Join a room
socket.on('join-room', (room) => {
socket.join(room);
socket.currentRoom = room;
socket.to(room).emit('notification', {
text: `${socket.username} joined ${room}`
});
});
// Send a chat message
socket.on('chat-message', ({ text, room }) => {
const message = {
id: Date.now(),
user: socket.username,
text,
timestamp: new Date().toISOString()
};
io.to(room).emit('chat-message', message);
});
// Typing indicator
socket.on('typing', ({ room, isTyping }) => {
socket.to(room).emit('user-typing', {
user: socket.username,
isTyping
});
});
socket.on('disconnect', () => {
users.delete(socket.id);
socket.broadcast.emit('user-left', { user: socket.username });
io.emit('user-count', users.size);
console.log('🔴 Disconnected:', socket.id);
});
});
server.listen(3001, () => console.log('🚀 Server on :3001'));
// ============ CLIENT (chat.js) ============
import { io } from 'socket.io-client';
const socket = io('http://localhost:3001');
socket.on('connect', () => {
socket.emit('set-username', 'Alice');
socket.emit('join-room', 'general');
});
socket.on('chat-message', (message) => {
console.log(`${message.user}: ${message.text}`);
});
socket.on('user-joined', ({ user }) => console.log(`${user} joined`));
socket.on('user-left', ({ user }) => console.log(`${user} left`));
socket.on('user-typing', ({ user, isTyping }) => {
if (isTyping) console.log(`${user} is typing...`);
});
function sendMessage(text) {
socket.emit('chat-message', { text, room: 'general' });
}
let typingTimeout;
function onKeyPress() {
socket.emit('typing', { room: 'general', isTyping: true });
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
socket.emit('typing', { room: 'general', isTyping: false });
}, 1000);
}