A learning project to understand WebSocket concepts using Node.js, Express, Socket.IO, and React. Demonstrates JWT authentication, room-based messaging, and secure cookie-based socket connections for educational purposes.
Project Goal: Learn how WebSockets work, authentication flows, and real-time bidirectional communication between client and server.
- What You'll Learn
- Key Concepts
- Tech Stack
- Project Structure
- Prerequisites
- Installation
- Configuration
- Running the Application
- How It Works
- API Documentation
- Socket Events
- Authentication Flow
- Debugging & Learning Tips
- Known Issues & Improvements
By building and understanding this project, you'll learn:
- โ WebSocket Basics - How real-time, bidirectional communication works
- โ Socket.IO Library - Fallbacks, reconnection, and event-driven architecture
- โ JWT Authentication - Token-based auth and secure cookie handling
- โ Room-based Messaging - Broadcasting to specific groups
- โ CORS with Credentials - Cross-origin requests with authentication
- โ Client-Server Architecture - Full-stack communication patterns
- โ React State Management - Managing socket events with hooks
- โ Express Middleware - Custom middleware for authentication
HTTP (Traditional):
Client โ [Request] โ Server
Client โ [Response] โ Server
(One request = One response, must initiate from client)
WebSocket (Real-time):
Client โท [Two-way connection] โท Server
(Persistent connection, either side can send data anytime)
// Client sends to server
socket.emit("event-name", data);
// Server receives from client
socket.on("event-name", (data) => {});
// Server sends to client
io.emit("event-name", data);
// Client receives from server
socket.on("event-name", (data) => {});// Send to everyone in a specific room
io.to("room-name").emit("event", data);
// Send to everyone (global broadcast)
io.emit("event", data);
// Send to everyone except sender
socket.broadcast.emit("event", data);HTTP-only Cookie โ Cannot access via JavaScript (safer)
โ Automatically sent with every request
โ Socket.IO can read it in middleware
- Node.js - JavaScript runtime (built on Chrome's V8 engine)
- Express - Web framework for routing and middleware
- Socket.IO - WebSocket library with fallbacks and features
- JWT (jsonwebtoken) - Token-based authentication
- CORS - Allow cross-origin requests
- Cookie-Parser - Parse HTTP cookies
- React - UI library with hooks
- Socket.IO Client - WebSocket client for browsers
- Material-UI (MUI) - Pre-built UI components
- Vite - Fast build tool for development
root/
โโโ server/
โ โโโ app.js # Express + Socket.IO server
โ โโโ package.json # Dependencies
โ โโโ node_modules/ # Installed packages
โโโ client/
โ โโโ src/
โ โ โโโ App.jsx # Main React component
โ โ โโโ main.jsx # Entry point
โ โ โโโ index.css # Styles
โ โโโ package.json # Dependencies
โ โโโ node_modules/ # Installed packages
โโโ README.md # This file
Before starting, you need:
- Node.js (v14 or higher) - Download
- npm or yarn - Comes with Node.js
- Code Editor - VSCode recommended
- Git (optional) - For version control
node --version # Should show v14+
npm --version # Should show version# Option A: If cloning from GitHub
git clone https://github.com/preranah7/WebSocket-concept.git
cd project-directory
# Option B: Create from scratch
mkdir socket-io-chat
cd socket-io-chatcd server
npm init -y
npm install express socket.io cors jsonwebtoken cookie-parserThis creates package.json and installs all required packages.
cd ../client
npm create vite@latest . -- --template react
npm install
npm install socket.io-client @mui/material @emotion/react @emotion/styled# In server/ directory
ls node_modules # Should show many folders
# In client/ directory
ls node_modules # Should show many foldersOptional: Create server/.env file for easy variable management:
PORT=3000
NODE_ENV=development
JWT_SECRET=poiuytrewq
CLIENT_URL=http://localhost:5173Backend (server/app.js):
const io = new Server(server, {
cors: {
origin: "http://localhost:5173", // Frontend URL
methods: ["GET", "POST"],
credentials: true // Allow cookies
}
});Frontend (client/src/App.jsx):
const socket = io("http://localhost:3000", {
withCredentials: true // Send cookies with request
});Important: Both
credentials: truemust be set, or cookies won't be sent!
Important: Change this for production! Add to .env:
const secretKeyJWT = process.env.JWT_SECRET || "poiuytrewq";cd server
node app.jsExpected output:
Server is running on port 3000
The server is now listening for connections at http://localhost:3000
cd client
npm run devExpected output:
VITE v4.x.x ready in XXX ms
โ Local: http://localhost:5173/
โ press h to show help
- Go to
http://localhost:5173/ - Open Browser DevTools (F12)
- Go to Console tab to see logs
- Go to Network tab โ WS to see WebSocket connection
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ Browser โ โ Server โ
โ (React) โ โ (Express) โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ โ
โ 1. HTTP Request to /login โ
โ (cookies: "include") โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ>โ
โ โ
โ 2. JWT Token generated โ
โ 3. Cookie set (httpOnly) โ
โ โ
โ 4. Socket.IO Connection Attempt โ
โ (includes cookies automatically) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ>โ
โ โ
โ 5. Middleware verifies JWT โ
โ โ
โ 6. Connection Established โ
โ<โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ 7. Emit "join-room" event โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ>โ
โ โ
โ 8. Emit "message" event โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ>โ
โ โ
โ 9. Receive "receive-message" event โ
โ<โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
Scenario: User sends message "Hello" to room "general"
-
Frontend (React):
socket.emit("message", { message: "Hello", room: "general" });
-
Backend (Express):
socket.on("message", ({room, message}) => { console.log("Received:", message, "in room:", room); io.to(room).emit("receive-message", message); });
-
Frontend (React - All users in room):
socket.on("receive-message", (data) => { setMessages(prev => [...prev, data]); });
GET http://localhost:3000/Response:
Hello World
Purpose: Verify server is running
GET http://localhost:3000/loginResponse (JSON):
{
"message": "Login Success"
}Important: This sets a cookie named token with JWT inside
Usage in Frontend:
// This will set the cookie automatically
const response = await fetch("http://localhost:3000/login", {
credentials: "include" // Important!
});
const data = await response.json();
console.log(data.message); // "Login Success"User joins a specific chat room
socket.emit("join-room", "general");What happens on server:
socket.on("join-room", (room) => {
socket.join(room); // Add to room
console.log(`User joined room ${room}`);
});Learning Point: socket.join() adds the socket to a room, so later messages can be broadcast to that room.
User sends a message to a room
socket.emit("message", {
message: "Hello everyone!",
room: "general"
});Server receives:
socket.on("message", ({room, message}) => {
io.to(room).emit("receive-message", message);
// Send to everyone in that room
});Learning Point: io.to(room) is like a radio broadcast - only people listening to that channel get it.
Client receives messages from the room
socket.on("receive-message", (data) => {
console.log("New message:", data);
setMessages(prev => [...prev, data]);
});Welcome message when connection established
socket.on("welcome", (message) => {
console.log(message);
});Step 1: Login Request
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Frontend calls GET /login โ
โ Sends: {credentials: "include"} โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
Step 2: JWT Generation
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Backend generates JWT token โ
โ Payload: {_id: "qwertyuiop"} โ
โ Secret: "poiuytrewq" โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
Step 3: Cookie Set
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Backend sends response with cookie โ
โ Cookie name: "token" โ
โ httpOnly: true (JS can't access) โ
โ secure: true (HTTPS only in prod) โ
โ sameSite: "none" (cross-origin) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
Step 4: Socket.IO Connection
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Frontend creates socket connection โ
โ Socket.IO auto-sends cookies โ
โ (because withCredentials: true) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
Step 5: Middleware Authentication
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Server middleware runs BEFORE โ
โ connection is established โ
โ 1. Parse cookies from request โ
โ 2. Extract JWT token from cookie โ
โ 3. Verify token with secret โ
โ 4. If valid โ Allow connection โ
โ 5. If invalid โ Reject โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
io.use((socket, next) => {
// 1. Parse cookies from request
cookieParser()(socket.request, socket.request.res, (err) => {
if (err) return next(err);
// 2. Extract token from cookies
const token = socket.request.cookies.token;
// 3. Check if token exists
if (!token) return next(new Error("Authentication Error"));
// 4. Verify token signature
const decoded = jwt.verify(token, secretKeyJWT);
// If invalid, jwt.verify() throws error automatically
// 5. If we reach here, token is valid
next(); // Allow connection
});
});Learning Point: Middleware runs BEFORE the connection handler, so you can reject invalid connections before they're fully established.
io.to("general").emit("receive-message", "Hello!");Who receives: Only users in "general" room Use case: Chat room messages
io.emit("receive-message", "Hello everyone!");Who receives: Everyone connected to the server Use case: System announcements
socket.broadcast.emit("receive-message", "Hello!");Who receives: Everyone EXCEPT the sender Use case: Notify others without echoing back
io.to(specificSocketId).emit("receive-message", "Private!");Who receives: Only the socket with that specific ID Use case: Direct/private messages
Frontend - Already in code:
console.log(messages); // See all messages
console.log(socket.id); // See your socket ID
console.log("connected", socket.id); // Connection log- Open Browser DevTools (F12)
- Go to Network tab
- Filter by WS (WebSocket)
- Click on the
localhost:3000/WebSocket - Go to Messages tab to see all events
The server already logs important events:
console.log("User Connected", socket.id);
console.log({room, message});
console.log(`User joined room ${room}`);
console.log("User Disconnected", socket.id);- Open
http://localhost:5173/in Tab 1 - Open
http://localhost:5173/in Tab 2 - Join same room in both tabs
- Send message in Tab 1
- See message appear in Tab 2 in real-time! ๐
-
Message Display Format
- Issue: Frontend receives only message string, no sender info
- Problem: Can't tell who sent what message
Fix:
// Server - include metadata io.to(room).emit("receive-message", { message, sender: socket.id, // Who sent it timestamp: new Date(), // When sent room // Which room }); // Frontend - display with metadata messages.map((m) => ( <div key={m.timestamp}> <strong>{m.sender}:</strong> {m.message} </div> ))
-
Missing Error Handling
- Issue: No error events for connection failures
- Problem: If connection fails, user doesn't know why
Fix:
socket.on("connect_error", (error) => { console.error("Connection Error:", error.message); // Show error to user }); socket.on("disconnect", (reason) => { console.log("Disconnected:", reason); // Attempt reconnection or notify user });
-
No User Tracking
- Issue: Can't see who's online or in each room
- Problem: No presence indicators
Fix:
// Server - notify when user joins socket.on("join-room", (room) => { socket.join(room); const usersInRoom = io.sockets.adapter.rooms.get(room).size; io.to(room).emit("user-joined", { socketId: socket.id, totalUsers: usersInRoom }); });
-
Hard-coded Credentials
- Issue: JWT secret, URLs, and ports are hard-coded
- Problem: Not flexible for different environments
Fix (for learning):
// Use environment variables const PORT = process.env.PORT || 3000; const CLIENT_URL = process.env.CLIENT_URL || "http://localhost:5173"; const JWT_SECRET = process.env.JWT_SECRET || "dev-secret-change-me";
-
UI/UX Issues
- Issue: TextFields have no spacing/margin
- Problem: Form looks cramped and unprofessional
Fix:
<TextField fullWidth margin="normal" value={roomName} onChange={(e) => setRoomName(e.target.value)} label="Room Name" variant="outlined" />
Start with these to deepen understanding:
- Add sender identification - See who sent each message
- Add timestamps - See when messages were sent
- Add online user list - Display active users in room
- Add disconnect/reconnection - Handle connection losses
- Add typing indicators - Show when someone is typing
- Add private messaging - Send messages to specific users
- Add message history - Persist messages (use MongoDB or localStorage)
- Add emoji reactions - React to messages
- โ HTTP-only cookies prevent XSS attacks
- โ JWT token validation before socket connection
- โ CORS configured with credentials
โ ๏ธ Hard-coded JWT secret (not suitable for production)โ ๏ธ No HTTPS (fine for development/learning)
// DON'T hardcode secrets
const secretKeyJWT = "poiuytrewq";
// DON'T skip credential validation
cors: {
origin: "*", // Never use * with credentials
credentials: true
}
// DON'T send sensitive data in messages
io.emit("message", {
password: user.password, // NEVER!
creditCard: user.card // NEVER!
});// Use environment variables
const secretKeyJWT = process.env.JWT_SECRET;
// Specify exact origins
cors: {
origin: process.env.CLIENT_URL,
credentials: true
}
// Use HTTPS only
secure: process.env.NODE_ENV === "production"
// Add rate limiting
npm install express-rate-limitUser 1 Actions:
// 1. Login first
await fetch("http://localhost:3000/login", {
credentials: "include"
});
// 2. Connect socket (automatically uses cookie)
const socket = io("http://localhost:3000", {
withCredentials: true
});
// 3. Join room
socket.emit("join-room", "learn-websockets");
// 4. Send message
socket.emit("message", {
message: "Hi, how's learning WebSockets?",
room: "learn-websockets"
});
// 5. Receive messages
socket.on("receive-message", (msg) => {
console.log("Received:", msg);
});User 2 (Same Flow):
- Logs in (gets different socket.id but same JWT)
- Connects socket
- Joins "learn-websockets" room
- Can now see User 1's messages in real-time!
Server Processing:
- User 1 emits "message" event
- Server receives and broadcasts to room "learn-websockets"
- All users in that room (User 1 + User 2) get the message
-
Understand the Code
- Read every line in
app.js(server) - Read every line in
App.jsx(client) - Add console.logs to track data flow
- Read every line in
-
Experiment
- Change the room name
- Add more listeners
- Modify the JWT payload
- Try different broadcast patterns
-
Extend the Project
- Add the improvements from "Known Issues"
- Build a user list
- Add message timestamps
- Persist messages to file or database
-
Deep Dive Topics
- How does Socket.IO handle fallbacks?
- What happens during reconnection?
- How do namespaces work?
- What about scaling with Redis?
- Socket.IO Official Docs
- WebSocket Basics (MDN)
- JWT Explained
- Express.js Guide
- React Hooks Documentation
Happy Learning!
Built to understand real-time web communication. Keep experimenting!