Tech TutorBangla Web Documentation
Portfolio
HomeHTMLCSSTailwind CSSJavaScriptTypeScriptReactNext.jsNode.jsExpressSocket.IOExpoWordPressPostgreSQLPrismaPostgresNestJSMongoDBDockerTanStackTanStack TableRTK QueryGitGitHub
  • Socket.IO Documentation25টি অধ্যায়
প্রযুক্তি নির্বাচন
HomeHTMLCSSTailwind CSSJavaScriptTypeScriptReactNext.jsNode.jsExpressSocket.IOExpoWordPressPostgreSQLPrismaPostgresNestJSMongoDBDockerTanStackTanStack TableRTK QueryGitGitHub
  • Introduction
  • Why Use Socket.IO
  • Socket.IO vs WebSocket
  • Installation
  • Client-Server Setup
  • Connection Lifecycle
  • Events
  • Data Transfer
  • Broadcasting
  • Rooms
  • Namespaces
  • Private & Group Messaging
  • Acknowledgements & Timeouts
  • Connection State Recovery
  • Reconnection & Errors
  • Authentication
  • Middleware
  • CORS Configuration
  • Express Integration
  • Next.js Integration
  • TypeScript
  • Chat Application
  • Best Practices
  • Next.js + Express Chat App
  • Next.js + Express Notifications
  1. Docs
  2. Socket.IO
  3. Build a Real-Time Chat App with Next.js, Express.js, and Socket.IO
Socket.IOAdvanced

Build a Real-Time Chat App with Next.js, Express.js, and Socket.IO

Next.js (TypeScript) frontend, Express (TypeScript) backend আর Socket.IO v4 দিয়ে সম্পূর্ণ real-time chat app — step-by-step Bangla project গাইড।

91টি অংশবাংলা ভাষায়

এখন পর্যন্ত আলাদা আলাদা lesson-এ যা শিখেছেন সেই সবকিছু একসাথে কাজে লাগিয়ে একটি পূর্ণ full-stack real-time chat application বানাই। Frontend হবে Next.js (TypeScript), backend থাকবে Express (TypeScript), আর real-time যোগাযোগের জন্য Socket.IO v4।

লক্ষ্য শুধু code চালানো নয় — বরং একটি সত্যিকারের project-এর মতো কাঠামো: shared typed event contract, CORS, online presence, typing indicator, reconnection handling আর safe rendering।

শেখার লক্ষ্য

  • Next.js client-কে Express + Socket.IO server-এর সাথে যুক্ত করা
  • server ও client-এর মধ্যে shared event contract দিয়ে event নাম মিলিয়ে রাখা
  • username registration, online presence, message আর typing indicator implement করা
  • React hooks দিয়ে socket lifecycle ও cleanup সঠিকভাবে handle করা
  • client থেকে আসা সব ডেটা validate করা এবং নিরাপদে render করা
Documentation example মাত্র

এটি শুধু একটি শেখার example — Tech Tutor ওয়েবসাইটে app হিসেবে যোগ করা হয়নি। এখানে code পড়ে নিজের project-এ ব্যবহার করবেন (দেখুন Real-Time Chat Application)।

Project Overview

আমাদের chat app-এর দশটি target:

#Feature
1Next.js client → Express Socket.IO server-এ connect
2Connection status display
3Username entry
4Real-time message পাঠানো ও পাওয়া
5Username, message ও timestamp display
6Online users list ও live count
7Typing indicator
8Connection error ও reconnection handling
9Input validation ও safe rendering
10Responsive chat UI

Architecture

দুইটি আলাদা process চলে — Next.js (port 3000) আর Express + Socket.IO (port 4000)। Browser সরাসরি Socket.IO server-এ connect করে; REST লাগলে Express route-ও থাকে। এই দুই process-এর মধ্যে কোনো ভাগ করা memory নেই — যোগাযোগ শুধু Socket.IO event দিয়ে।

request flowtext
text
Browser (Next.js UI)
        │  WebSocket / polling (Socket.IO)
        ▼
Express + Socket.IO server (port 4000)
        │
        ├── in-memory: online users
        └── in-memory: typing state

Event flow:

  1. Client user:join পাঠায় → server username যাচাই করে presence:update broadcast করে
  2. Client message:send পাঠায় → server validate করে message:new সবাইকে broadcast করে
  3. Client typing:start / typing:stop পাঠায় → server typing:update broadcast করে
  4. Disconnect হলে server online list আপডেট করে

Prerequisites

  • Node.js 20+ ও npm
  • TypeScript-এর মৌলিক ধারণা (Socket.IO with TypeScript)
  • React hooks (useState, useEffect, useRef, useCallback) সম্পর্কে ধারণা
  • Rooms, broadcasting ও presence সম্পর্কে ধারণা (Rooms, Broadcasting)

Folder Structure

আমরা একটি ছোট monorepo বানাবো — root-এ npm workspaces, তার তিনটি package: shared, server, client। shared শুধু TypeScript type ধরে রাখে, যাতে server ও client একই event contract ব্যবহার করে।

folder structuretext
text
socketio-chat-app/
├── package.json            # workspaces: shared, server, client
├── shared/
│   ├── package.json
│   └── src/events.ts       # shared event + data types
├── server/
│   ├── .env
│   ├── package.json
│   ├── tsconfig.json
│   └── src/
│       ├── config.ts
│       ├── index.ts        # Express + http server
│       └── socket.ts       # Socket.IO logic
└── client/                 # create-next-app দিয়ে তৈরি
    ├── .env.local
    ├── app/
    │   ├── layout.tsx
    │   └── page.tsx
    ├── components/
    │   └── chat.tsx
    └── hooks/
        └── use-chat.ts

Step 1 — Project Init

Root-এ workspace বানান, তারপর তিনটি package install করুন।

terminalbash
bash
mkdir socketio-chat-app
cd socketio-chat-app

# root package.json (নিচের ফাইলটি বানান)
# shared workspace
mkdir shared/src

# server workspace
mkdir server/src

# Next.js client
npx create-next-app@latest client --ts --tailwind --eslint --app --no-src-dir --import-alias "@/*" --use-npm

# root-এ সব install (workspaces)
npm install
package.jsonjson
json
{
  "name": "socketio-chat-app",
  "private": true,
  "workspaces": ["shared", "server", "client"]
}

তারপর server ও client-এর dependency যোগ করুন। ভাগ করা @chat/shared টাইপ-only, তাই এতে কোনো runtime dependency লাগে না।

install dependenciesbash
bash
# Express + Socket.IO server
npm install -w server express socket.io cors dotenv
npm install -w server -D typescript tsx @types/express @types/cors @types/node

# Next.js-এ Socket.IO client
npm install -w client socket.io-client
shared/package.jsonjson
json
{
  "name": "@chat/shared",
  "version": "1.0.0",
  "private": true,
  "types": "./src/events.ts",
  "main": "./src/events.ts"
}

Step 2 — Shared Event Contract

সবচেয়ে গুরুত্বপূর্ণ ভিত্তি হলো একটি মাত্র সত্যের উৎস (single source of truth) event ও type-এর জন্য। server ও client একই events.ts import করায় event নাম কখনো mismatch হবে না — TypeScript ভুল নাম বা ভুল payload compile-time-এ ধরবে।

shared/src/events.tstypescript
typescript
// shared/src/events.ts
export interface ChatMessage {
  id: string;
  username: string;
  text: string;
  timestamp: number;
}

export interface OnlineUser {
  id: string;
  username: string;
}

/** server থেকে client-এ যাওয়া event */
export interface ServerToClientEvents {
  "presence:update": (users: OnlineUser[]) => void;
  "message:new": (message: ChatMessage) => void;
  "typing:update": (usernames: string[]) => void;
}

/** client থেকে server-এ যাওয়া event */
export interface ClientToServerEvents {
  "user:join": (username: string, ack: (ok: boolean) => void) => void;
  "message:send": (text: string) => void;
  "typing:start": () => void;
  "typing:stop": () => void;
}

/** প্রতিটি socket-এ রাখা server-side data */
export interface SocketData {
  username?: string;
}

export type InterServerEvents = Record<string, never>;
server/tsconfig.jsonjson
json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "noEmit": true,
    "types": ["node"]
  },
  "include": ["src"]
}
server/package.jsonjson
json
{
  "name": "server",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "start": "tsx src/index.ts",
    "typecheck": "tsc --noEmit"
  }
}

Step 3 — Express Backend

Express app আর node:http server তৈরি করি। Socket.IO ঠিক এই একই HTTP server-এর উপরে বসবে, তাই একই port-এ REST + realtime দুটোই চলবে (Socket.IO with Express.js)।

server/.envbash
bash
# server/.env
PORT=4000
CLIENT_ORIGIN=http://localhost:3000
server/src/config.tstypescript
typescript
// server/src/config.ts
import "dotenv/config";

export const PORT = Number(process.env.PORT ?? 4000);
export const CLIENT_ORIGIN =
  process.env.CLIENT_ORIGIN ?? "http://localhost:3000";
server/src/index.tstypescript
typescript
// server/src/index.ts
import http from "node:http";
import express from "express";
import cors from "cors";

import { CLIENT_ORIGIN, PORT } from "./config";
import { createSocketServer } from "./socket";

const app = express();
app.use(cors({ origin: CLIENT_ORIGIN }));
app.use(express.json());

app.get("/health", (_req, res) => {
  res.json({ ok: true });
});

// Express app-কে একটি http.Server-এ মুড়ে দিন — Socket.IO এখানেই বসবে
const httpServer = http.createServer(app);
createSocketServer(httpServer);

httpServer.listen(PORT, () => {
  console.log(`Server চলছে: http://localhost:${PORT}`);
});
app.listen নয়

Socket.IO যোগ করার পর app.listen(PORT) ব্যবহার করবেন না — httpServer.listen(PORT) করুন। নাহলে Socket.IO কখনো bind হবে না (দেখুন Express Integration)।

Step 3.1 — CORS (explicit origin)

Socket.IO v3 থেকে CORS ডিফল্টভাবে বন্ধ। client আর server আলাদা origin-এ (:3000 বনাম :4000) চলায় আমাদের স্পষ্টভাবে অনুমোদিত origin দিতে হবে। "*" নয় — একটি নির্দিষ্ট origin ব্যবহার করুন, যেটি .env থেকে আসে। বিস্তারিত CORS Configuration-এ।

explicit origintypescript
typescript
// server/src/socket.ts (CORS অংশ)
const io = new Server(httpServer, {
  cors: {
    origin: CLIENT_ORIGIN,   // e.g. http://localhost:3000
    methods: ["GET", "POST"],
  },
});

Step 4 — Socket.IO Server (presence, message, typing)

এখন সম্পূর্ণ server-side logic। আমরা Socket.IO-কে shared type দিয়ে typed করি, আর socket.data-তে username রাখি।

server/src/socket.tstypescript
typescript
// server/src/socket.ts
import type http from "node:http";
import { Server } from "socket.io";

import type {
  ChatMessage,
  ClientToServerEvents,
  InterServerEvents,
  OnlineUser,
  ServerToClientEvents,
  SocketData,
} from "@chat/shared";

import { CLIENT_ORIGIN } from "./config";

type ChatServer = Server<
  ClientToServerEvents,
  ServerToClientEvents,
  InterServerEvents,
  SocketData
>;

export function createSocketServer(httpServer: http.Server): ChatServer {
  const io: ChatServer = new Server(httpServer, {
    cors: { origin: CLIENT_ORIGIN, methods: ["GET", "POST"] },
  });

  // in-memory state — শুধু একটি server instance-এর জন্য
  const online = new Map<string, string>(); // socket.id -> username
  const typing = new Map<string, string>(); // socket.id -> username

  const presence = (): OnlineUser[] =>
    [...online].map(([id, username]) => ({ id, username }));

  const typingNames = (): string[] => [...new Set(typing.values())];

  io.on("connection", (socket) => {
    // 1) username register + presence update
    socket.on("user:join", (username, ack) => {
      const clean = String(username ?? "").trim().slice(0, 24);
      if (!clean) {
        ack(false);
        return;
      }

      socket.data.username = clean;
      online.set(socket.id, clean);
      ack(true);
      io.emit("presence:update", presence());
    });

    // 2. message receive → validate → সবাইকে broadcast
    socket.on("message:send", (text) => {
      const username = socket.data.username;
      if (!username) return; // join না করা client ignore

      const clean = String(text ?? "").trim().slice(0, 1000);
      if (!clean) return;

      io.emit("message:new", {
        id: `${socket.id}-${Date.now()}`,
        username,
        text: clean,
        timestamp: Date.now(),
      });
    });

    // typing indicator
    socket.on("typing:start", () => {
      if (!socket.data.username) return;
      typing.set(socket.id, socket.data.username);
      io.emit("typing:update", [...new Set(typing.values())]);
    });

    socket.on("typing:stop", () => {
      typing.delete(socket.id);
      io.emit("typing:update", [...new Set(typing.values())]);
    });

    // disconnect — সব state পরিষ্কার
    socket.on("disconnect", () => {
      online.delete(socket.id);
      typing.delete(socket.id);
      io.emit("presence:update", presence());
      io.emit("typing:update", [...new Set(typing.values())]);
    });
  });

  return io;
}

Line-by-line:

  • new Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData> — server যে event শোনে প্রথমে, যে event পাঠায় দ্বিতীয়ে (ক্রম মনে রাখুন)।
  • socket.data.username — typed, তাই handler-এ type-safe।
  • online / typing — Map-এ রাখা in-memory state।
  • String(x ?? "").trim().slice(...) — client-এর পাঠানো যেকোনো কিছু সর্বদা যাচাই ও সীমিত করা হয়।
server/.envbash
bash
// server/.env
PORT=4000
CLIENT_ORIGIN=http://localhost:3000
run serverbash
bash
# server চালু করুন
npm run dev -w server

# output: Server চলছে: http://localhost:4000

Step 5 — Next.js Client Setup

Next.js app-এ Socket.IO client URL একটি environment variable থেকে নেওয়া হবে — কোনো production URL hardcode নয়।

client/.env.localbash
bash
# client/.env.local
NEXT_PUBLIC_SOCKET_URL=http://localhost:4000

Step 6 — Chat UI with React Hooks

6.1 — useChat hook (socket lifecycle)

এই hook-ই socket তৈরি, listener যোগ, state ব্যবস্থাপনা ও cleanup করে। socket কেবল useEffect-এর ভিতরে তৈরি হয় — render-এর সময় নয় — যাতে প্রতিটি render-এ নতুন connection না খোলে।

client/hooks/use-chat.tstypescript
typescript
// client/hooks/use-chat.ts
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { io, type Socket } from "socket.io-client";

import type {
  ChatMessage,
  ClientToServerEvents,
  OnlineUser,
  ServerToClientEvents,
} from "@chat/shared";

type ChatSocket = Socket<ServerToClientEvents, ClientToServerEvents>;
type Status = "connecting" | "connected" | "disconnected";

const SOCKET_URL =
  process.env.NEXT_PUBLIC_SOCKET_URL ?? "http://localhost:4000";

export function useChat(username: string | null) {
  const socketRef = useRef<ChatSocket | null>(null);

  const [status, setStatus] = useState<Status>("connecting");
  const [error, setError] = useState<string | null>(null);
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [users, setUsers] = useState<OnlineUser[]>([]);
  const [typingUsers, setTypingUsers] = useState<string[]>([]);

  useEffect(() => {
    const socket: ChatSocket = io(SOCKET_URL);
    socketRef.current = socket;

    const onConnect = () => {
      setStatus("connected");
      setError(null);
    };
    const onDisconnect = () => setStatus("disconnected");
    const onConnectError = (err: Error) => {
      setStatus("disconnected");
      setError(err.message);
    };
    const onPresence = (list: OnlineUser[]) => setUsers(list);
    const onMessage = (message: ChatMessage) =>
      setMessages((prev) => [...prev, message]);
    const onTyping = (names: string[]) => setTypingUsers(names);

    socket.on("connect", onConnect);
    socket.on("disconnect", onDisconnect);
    socket.on("connect_error", onConnectError);
    socket.on("presence:update", onPresence);
    socket.on("message:new", onMessage);
    socket.on("typing:update", onTyping);

    // cleanup — listener সরান ও connection বন্ধ করুন
    return () => {
      socket.off("connect", onConnect);
      socket.off("disconnect", onDisconnect);
      socket.off("connect_error", onConnectError);
      socket.off("presence:update", onPresence);
      socket.off("message:new", onMessage);
      socket.off("typing:update", onTyping);
      socket.disconnect();
      socketRef.current = null;
    };
  }, []);

  // username সেট হলে join — reconnect হলেও আবার join
  useEffect(() => {
    const socket = socketRef.current;
    if (!username || !socket) return;

    const join = () =>
      socket.emit("user:join", username, (ok) => {
        if (!ok) setError("username গ্রহণ করা যায়নি");
      });

    if (socket.connected) join();
    socket.on("connect", join);

    return () => {
      socket.off("connect", join);
    };
  }, [username]);

  const sendMessage = useCallback((text: string) => {
    socketRef.current?.emit("message:send", text);
  }, []);

  const startTyping = useCallback(() => {
    socketRef.current?.emit("typing:start");
  }, []);

  const stopTyping = useCallback(() => {
    socketRef.current?.emit("typing:stop");
  }, []);

  return {
    status,
    error,
    messages,
    users,
    typingUsers,
    sendMessage,
    startTyping,
    stopTyping,
  };
}

6.2 — Chat component (UI)

এখন UI। message text হিসেবে render করা হয়েছে ({message.text}) — React নিজেই HTML escape করে, তাই client-এর পাঠানো <script> জাতীয় কিছু নিরাপদে টেক্সট হয়ে যায়। কখনো dangerouslySetInnerHTML ব্যবহার করবেন না।

client/components/chat.tsxtsx
tsx
// client/components/chat.tsx
"use client";

import {
  useEffect,
  useMemo,
  useRef,
  useState,
  type FormEvent,
} from "react";

import { useChat } from "@/hooks/use-chat";

const STATUS_LABEL = {
  connecting: "সংযোগ হচ্ছে...",
  connected: "Connected",
  disconnected: "Disconnected",
} as const;

function formatTime(timestamp: number) {
  return new Date(timestamp).toLocaleTimeString();
}

export function Chat() {
  const [username, setUsername] = useState<string | null>(null);
  const [nameInput, setNameInput] = useState("");
  const [draft, setDraft] = useState("");

  const {
    status,
    error,
    messages,
    users,
    typingUsers,
    sendMessage,
    startTyping,
    stopTyping,
  } = useChat(username);

  const typingTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const bottomRef = useRef<HTMLLIElement | null>(null);

  useEffect(() => {
    bottomRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages]);

  useEffect(() => {
    return () => {
      if (typingTimer.current) clearTimeout(typingTimer.current);
    };
  }, []);

  const othersTyping = useMemo(
    () => typingUsers.filter((name) => name !== username),
    [typingUsers, username],
  );

  const handleJoin = (event: FormEvent) => {
    event.preventDefault();
    const clean = nameInput.trim().slice(0, 24);
    if (!clean) return;
    setUsername(clean);
  };

  const handleDraft = (value: string) => {
    setDraft(value);
    startTyping();
    if (typingTimer.current) clearTimeout(typingTimer.current);
    typingTimer.current = setTimeout(stopTyping, 1500);
  };

  const handleSend = (event: FormEvent) => {
    event.preventDefault();
    const clean = draft.trim();
    if (!clean) return;
    sendMessage(clean);
    setDraft("");
    stopTyping();
  };

  if (!username) {
    return (
      <form
        onSubmit={handleJoin}
        className="mx-auto flex max-w-sm flex-col gap-3 p-6"
      >
        <h1 className="text-xl font-semibold">চ্যাটে যোগ দিন</h1>
        <input
          value={nameInput}
          onChange={(event) => setNameInput(event.target.value)}
          placeholder="আপনার username"
          maxLength={24}
          className="rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-slate-100"
        />
        <button
          type="submit"
          disabled={!nameInput.trim()}
          className="rounded-lg bg-emerald-600 px-3 py-2 font-medium text-white disabled:opacity-50"
        >
          Join
        </button>
        {error && <p className="text-sm text-red-400">{error}</p>}
      </form>
    );
  }

  return (
    <div className="mx-auto flex h-screen max-w-2xl flex-col gap-3 p-4">
      <header className="flex items-center justify-between rounded-xl bg-slate-900 px-4 py-3">
        <div>
          <h1 className="font-semibold">Real-Time Chat</h1>
          <p className="text-xs text-slate-400">
            {STATUS_LABEL[status]} · অনলাইন: {users.length}
          </p>
        </div>
        <span
          aria-label={status}
          className={{
            connected: "size-2.5 rounded-full bg-emerald-500",
            connecting: "size-2.5 rounded-full bg-amber-500",
            disconnected: "size-2.5 rounded-full bg-red-500",
          }[status]}
        />
      </header>

      {error && (
        <p className="rounded-lg bg-red-500/10 px-3 py-2 text-sm text-red-400">
          {error}
        </p>
      )}

      <ul className="flex-1 space-y-3 overflow-y-auto rounded-xl bg-slate-900/60 p-4">
        {messages.map((message) => (
          <li key={message.id}>
            <p className="text-xs text-slate-400">
              {message.username} · {formatTime(message.timestamp)}
            </p>
            <p className="text-sm">{message.text}</p>
          </li>
        ))}
        <li ref={bottomRef} aria-hidden className="h-0" />
      </ul>

      <p className="h-4 text-xs text-slate-400">
        {othersTyping.length > 0 &&
          `${othersTyping.join(", ")} লিখছেন...`}
      </p>

      <form onSubmit={handleSend} className="flex gap-2">
        <input
          value={draft}
          onChange={(event) => handleDraft(event.target.value)}
          placeholder="একটি message লিখুন..."
          maxLength={1000}
          className="flex-1 rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-slate-100"
        />
        <button
          type="submit"
          disabled={!draft.trim()}
          className="rounded-lg bg-emerald-600 px-4 py-2 font-medium text-white disabled:opacity-50"
        >
          পাঠান
        </button>
      </form>

      <ul className="flex flex-wrap gap-2 text-xs text-slate-400">
        {users.map((user) => (
          <li key={user.id} className="rounded-full bg-slate-800 px-2 py-1">
            {user.username}
          </li>
        ))}
      </ul>
    </div>
  );
}

6.3 — Page

client/app/page.tsxtypescript
typescript
// client/app/page.tsx
import { Chat } from "@/components/chat";

export default function Home() {
  return (
    <main className="min-h-screen bg-slate-950 text-slate-100">
      <Chat />
    </main>
  );
}

Step 7 — Socket Lifecycle ও Cleanup

React-এ socket নিয়ে দুটি বড় ভুল হয়:

  • render-এ socket তৈরি — কম্পোনেন্ট body-তে io() কল করলে প্রতিটি render-এ নতুন connection খোলে। সমাধান: useEffect (empty deps) বা একটি module-level singleton/context।
  • cleanup না করা — route বদলালে listener ও connection জমে leak হয়। useEffect-এর return-এ socket.off(...) ও socket.disconnect() দিন।
duplicate connection pitfalltypescript
typescript
// ❌ ভুল — প্রতিটি render-এ নতুন connection
function Bad() {
  const socket = io("http://localhost:4000"); // render body!
  // ...
}

// ✅ ঠিক — effect-এ একবার
useEffect(() => {
  const socket = io(SOCKET_URL);
  socket.on("message:new", onMessage);
  return () => {
    socket.off("message:new", onMessage);
    socket.disconnect();
  };
}, []);
reconnect jointypescript
typescript
// server/src/socket.ts (reconnect-পরবর্তী)
// client একই socket নিয়ে আবার যুক্ত হয়; join listener আবার user:join পাঠায়,
// তাই reconnect হলেও online list ঠিক থাকে।
// (দেখুন: [Reconnection](/tech-tutor/socketio/reconnection-error-handling))
connection state recoverytypescript
typescript
// আরও নিশ্চিত করতে চাইলে Connection State Recovery চালু করুন
const io = new Server(httpServer, {
  cors: { origin: CLIENT_ORIGIN },
  connectionStateRecovery: {
    maxDisconnectionDuration: 2 * 60 * 1000,
  },
});
error ও reconnecttypescript
typescript
// client — connect_error স্বয়ংক্রিয়ভাবে reconnection শুরু করে
socket.on("connect_error", (err) => {
  console.error("connect error:", err.message);
});
socket.io.on("reconnect", (attempt) => {
  console.log("reconnected after", attempt, "attempts");
});
pagetypescript
typescript
// client/app/page.tsx
import { Chat } from "@/components/chat";

export default function Home() {
  return (
    <main className="min-h-screen bg-slate-950 text-slate-100">
      <Chat />
    </main>
  );
}

Running Locally

দুইটি terminal খুলুন — একটি server, একটি client।

run bothbash
bash
# terminal 1 — backend (port 4000)
npm run dev -w server

# terminal 2 — frontend (port 3000)
npm run dev -w client
env recapbash
bash
# server/.env (আগেই তৈরি)
PORT=4000
CLIENT_ORIGIN=http://localhost:3000

# client/.env.local (আগেই তৈরি)
NEXT_PUBLIC_SOCKET_URL=http://localhost:4000

তারপর browser-এ http://localhost:3000 খুলুন, একটি username দিন এবং চ্যাট শুরু করুন।

Testing the Complete Application

  1. Connection status — page খুললে header-এ Connected ও সবুজ dot দেখুন
  2. Username — একটি নাম দিয়ে Join করুন
  3. Presence — দ্বিতীয় browser tab/incognito-তে অন্যদের online list ও count বাড়তে দেখুন
  4. Messages — দুই window থেকে message পাঠান; username, text ও timestamp সব দেখুন
  5. Typing — একটি window-তে টাইপ করুন; অন্যটিতে "… লিখছেন..." দেখুন
  6. Reconnection — server বন্ধ করলে status Disconnected হবে; আবার চালু করলে স্বয়ংক্রিয়ভাবে reconnect হবে (কিছু সেকেন্ড অপেক্ষা করুন)
  7. Validation — খালি message বা খুব ছোট username আটকে যাবে
  8. XSS — <b>test</b> পাঠান; এটি bold না হয়ে টেক্সট হিসেবে দেখাবে
StrictMode ও client

Next.js dev mode-এ React StrictMode effect দুইবার চালায় — তাই cleanup ঠিকভাবে লেখা জরুরি, নাহলে duplicate listener হবে। client component-এ অবশ্যই "use client" দিন ও socket কেবল effect-এ তৈরি করুন।

Troubleshooting

সমস্যাকারণ ও সমাধান
connect_error / CORSserver-এর cors.origin-এ client origin (http://localhost:3000) দিন; \"*\" নয়
Connection refusedserver চালু আছে কি? http://localhost:4000/health খুলে দেখুন
দুবার message আসছেduplicate listener — event-এ on বারবার নয়, cleanup করুন
NEXT_PUBLIC_SOCKET_URL কাজ করছে না.env.local বানানোর পর dev server restart করুন
Type mismatchserver ও client একই @chat/shared type import করছে কি না দেখুন
পৃষ্ঠা reload-এ id বদলায়এটা স্বাভাবিক; user identify করতে socket.id নয়, username/DB id ব্যবহার করুন
Online list মুছে যাচ্ছেmemory-ভিত্তিক; server restart-এ হারায় (নিচে দেখুন)

Security Considerations

  • Client-এর ডেটা কখনো বিশ্বাস করবেন না — username ও message সবসময় server-এ validate ও length-limit করুন (এই server তাই করে)।
  • Safe rendering — message {message.text} হিসেবে দেখান; dangerouslySetInnerHTML এড়িয়ে চলুন।
  • Authentication — production-এ JWT/middleware দিয়ে identity যাচাই করুন (Authentication)। username client-এর দাবিমাত্র।
  • Rate limiting — spam ঠেকাতে প্রতি connection/event-এ throttle (Middleware)।
  • HTTPS/WSS — production-এ ট্রাফিক encrypt করুন।
  • CORS — নির্দিষ্ট trusted origin (CORS Configuration)।

Possible Improvements

  • Database যোগ করা — message persist করলে স্থায়ী history পাওয়া যাবে (এই app-এ নেই)।
  • Authentication — logged-in user ও authorization।
  • Private message ও rooms (Private & Group Messaging)।
  • Acknowledgements — delivery confirm (Acknowledgements)।
  • Redis adapter — একাধিক server instance-এ presence ও broadcast ভাগ করা (Best Practices)।

Limitations

in-memorytext
text
in-memory state:
- online users Map, typing Map server process-এর memory-তে
- server restart / crash → সব হারায়
- একাধিক node → state প্রতিটিতে আলাদা
- durable/persistent নয়; production-এ Redis adapter + DB লাগবে
History নেই

এই app-এ কোনো database নেই — server message সংরক্ষণ করে না। তাই নতুন join করা user আগের message দেখবে না, আর page reload করলে message list মুছে যায়।

typing statetypescript
typescript
# client/components/chat.tsx (typing state)
// typing indicator server-এর memory-ভিত্তিক Set থেকে আসে
// reconnect হলে client আবার join করে online list ঠিক হয়
hardentypescript
typescript
// server verify (production-এ যোগ করুন)
// - username: zod/validation library দিয়ে schema check
// - message: max length, নিষিদ্ধ content filter
// - connection: IP rate limit
// - auth: handshake.auth.token verify middleware
In-memory, single instance

Online list ও typing state শুধু memory-তে — server restart-এ হারায় এবং একাধিক node-এ ভাগ হয় না। একাধিক instance চলালে Redis adapter (@socket.io/redis-adapter) ও external storage লাগবে (Best Practices)।

Practice Task

  • project-টি উপরের সব file দিয়ে সম্পূর্ণভাবে locally চালান
  • একটি rooms feature যোগ করুন — user একটি room বেছে নিয়ে শুধু সেই room-এ message দেখুক
  • server-এ একটি rate limiter যোগ করুন — প্রতি connection-এ সীমিত message
  • db (যেমন SQLite/Postgres) দিয়ে সর্বশেষ ৫০টি message সংরক্ষণ করে নতুন user-কে দেখান
  • handshake auth দিয়ে JWT ভিত্তিক login যোগ করুন

সমাপ্তি

এই project-এ আপনি একটি সম্পূর্ণ full-stack real-time chat app বানিয়েছেন — shared typed event contract, presence, message, typing, reconnection handling, validation ও safe rendering সহ। এখন নিজের idea-তে (notification, live dashboard, multiplayer) একই ভিত্তি ব্যবহার করে এগিয়ে যান।

সম্পর্কিত অধ্যায়: Building a Real-Time Chat Application · Socket.IO with Next.js · Socket.IO with Express.js · Best Practices

শেষ প্যারায় নিজে practice করুন — কোড কপি করে নিজের এডিটরে চালিয়ে দেখুন।
আগেরSocket.IO Best Practices and Common MistakesপরেরBuild a Real-Time Notification System with Next.js, Express.js, and Socket.IO
এই পেজে
Tech Tutor

বাংলায় ওয়েব ডেভেলপমেন্ট ডকুমেন্টেশন শিখুন — ধাপে ধাপে।

Homeসব ডকুমেন্টেশনQuick Startআমার নোটস

© 2026 Tech Tutor. বাংলায় শিখুন, বাংলায় গড়ুন।

mdanamolhasan1998@gmail.com