Skip to content

Learn how WebSockets work, authentication flows, and real-time bidirectional communication between client and server.

Notifications You must be signed in to change notification settings

preranah7/WebSocket-concept

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

2 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Real-Time Chat Application with Socket.IO

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.

Table of Contents


What You'll Learn

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

Key Concepts

1. WebSockets vs HTTP

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)

2. Socket.IO Events

// 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) => {});

3. Room Broadcasting

// 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);

4. Authentication with Cookies

HTTP-only Cookie โ†’ Cannot access via JavaScript (safer)
                โ†’ Automatically sent with every request
                โ†’ Socket.IO can read it in middleware

๐Ÿ›  Tech Stack

Backend

  • 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

Frontend

  • 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

Project Structure

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

Prerequisites

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

Check Installation

node --version    # Should show v14+
npm --version     # Should show version

Installation

Step 1: Clone or Create Project

# 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-chat

Step 2: Install Backend Dependencies

cd server
npm init -y
npm install express socket.io cors jsonwebtoken cookie-parser

This creates package.json and installs all required packages.

Step 3: Install Frontend Dependencies

cd ../client
npm create vite@latest . -- --template react
npm install
npm install socket.io-client @mui/material @emotion/react @emotion/styled

Verify Installation

# In server/ directory
ls node_modules  # Should show many folders

# In client/ directory
ls node_modules  # Should show many folders

Configuration

Environment Setup

Optional: Create server/.env file for easy variable management:

PORT=3000
NODE_ENV=development
JWT_SECRET=poiuytrewq
CLIENT_URL=http://localhost:5173

CORS Configuration (Important!)

Backend (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: true must be set, or cookies won't be sent!

JWT Secret

Important: Change this for production! Add to .env:

const secretKeyJWT = process.env.JWT_SECRET || "poiuytrewq";

Running the Application

Terminal 1: Start Backend Server

cd server
node app.js

Expected output:

Server is running on port 3000

The server is now listening for connections at http://localhost:3000

Terminal 2: Start Frontend Development Server

cd client
npm run dev

Expected output:

  VITE v4.x.x  ready in XXX ms

  โžœ  Local:   http://localhost:5173/
  โžœ  press h to show help

Step 3: Open in Browser

  1. Go to http://localhost:5173/
  2. Open Browser DevTools (F12)
  3. Go to Console tab to see logs
  4. Go to Network tab โ†’ WS to see WebSocket connection

How It Works

Connection Flow (Visual)

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   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 โ”‚
       โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค

Data Flow Example

Scenario: User sends message "Hello" to room "general"

  1. Frontend (React):

    socket.emit("message", {
      message: "Hello",
      room: "general"
    });
  2. Backend (Express):

    socket.on("message", ({room, message}) => {
      console.log("Received:", message, "in room:", room);
      io.to(room).emit("receive-message", message);
    });
  3. Frontend (React - All users in room):

    socket.on("receive-message", (data) => {
      setMessages(prev => [...prev, data]);
    });

API Documentation

REST Endpoints

1. Health Check

GET http://localhost:3000/

Response:

Hello World

Purpose: Verify server is running

2. Login (Generate JWT Token)

GET http://localhost:3000/login

Response (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"

Socket Events

Client โ†’ Server Events

join-room

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.

message

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.


Server โ†’ Client Events

receive-message

Client receives messages from the room

socket.on("receive-message", (data) => {
  console.log("New message:", data);
  setMessages(prev => [...prev, data]);
});

welcome (Commented out in current code)

Welcome message when connection established

socket.on("welcome", (message) => {
  console.log(message);
});

Authentication Flow

Step-by-Step Authentication

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            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Middleware Code Explanation

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.


Message Broadcasting Patterns

Pattern 1: Room-Specific (Current Implementation)

io.to("general").emit("receive-message", "Hello!");

Who receives: Only users in "general" room Use case: Chat room messages

Pattern 2: Global Broadcast

io.emit("receive-message", "Hello everyone!");

Who receives: Everyone connected to the server Use case: System announcements

Pattern 3: Except Self

socket.broadcast.emit("receive-message", "Hello!");

Who receives: Everyone EXCEPT the sender Use case: Notify others without echoing back

Pattern 4: Specific User (Advanced)

io.to(specificSocketId).emit("receive-message", "Private!");

Who receives: Only the socket with that specific ID Use case: Direct/private messages


Debugging & Learning Tips

Enable Console Logging

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

View WebSocket Traffic

  1. Open Browser DevTools (F12)
  2. Go to Network tab
  3. Filter by WS (WebSocket)
  4. Click on the localhost:3000/ WebSocket
  5. Go to Messages tab to see all events

View Server Logs

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);

Test in Multiple Tabs

  1. Open http://localhost:5173/ in Tab 1
  2. Open http://localhost:5173/ in Tab 2
  3. Join same room in both tabs
  4. Send message in Tab 1
  5. See message appear in Tab 2 in real-time! ๐ŸŽ‰

Known Issues & Improvements

Current Issues

  1. 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>
    ))
  2. 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
    });
  3. 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
      });
    });
  4. 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";
  5. 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"
    />

Recommended Learning Enhancements

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

Security (For Learning)

Current Setup Security

  • โœ… 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)

What NOT to Do in Production

//  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!
});

Production Best Practices (For Future)

//  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-limit

Example: Complete Message Flow

Scenario: Two Users Chat

User 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:

  1. User 1 emits "message" event
  2. Server receives and broadcasts to room "learn-websockets"
  3. All users in that room (User 1 + User 2) get the message

Next Steps for Learning

  1. Understand the Code

    • Read every line in app.js (server)
    • Read every line in App.jsx (client)
    • Add console.logs to track data flow
  2. Experiment

    • Change the room name
    • Add more listeners
    • Modify the JWT payload
    • Try different broadcast patterns
  3. Extend the Project

    • Add the improvements from "Known Issues"
    • Build a user list
    • Add message timestamps
    • Persist messages to file or database
  4. Deep Dive Topics

    • How does Socket.IO handle fallbacks?
    • What happens during reconnection?
    • How do namespaces work?
    • What about scaling with Redis?

Resources for Learning


Happy Learning!

Built to understand real-time web communication. Keep experimenting!

About

Learn how WebSockets work, authentication flows, and real-time bidirectional communication between client and server.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published