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 গাইড।
এখন পর্যন্ত আলাদা আলাদা 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 করা
এটি শুধু একটি শেখার example — Tech Tutor ওয়েবসাইটে app হিসেবে যোগ করা হয়নি। এখানে code পড়ে নিজের project-এ ব্যবহার করবেন (দেখুন Real-Time Chat Application)।
Project Overview
আমাদের chat app-এর দশটি target:
| # | Feature |
|---|---|
| 1 | Next.js client → Express Socket.IO server-এ connect |
| 2 | Connection status display |
| 3 | Username entry |
| 4 | Real-time message পাঠানো ও পাওয়া |
| 5 | Username, message ও timestamp display |
| 6 | Online users list ও live count |
| 7 | Typing indicator |
| 8 | Connection error ও reconnection handling |
| 9 | Input validation ও safe rendering |
| 10 | Responsive 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 দিয়ে।
Browser (Next.js UI)
│ WebSocket / polling (Socket.IO)
▼
Express + Socket.IO server (port 4000)
│
├── in-memory: online users
└── in-memory: typing stateEvent flow:
- Client
user:joinপাঠায় → server username যাচাই করেpresence:updatebroadcast করে - Client
message:sendপাঠায় → server validate করেmessage:newসবাইকে broadcast করে - Client
typing:start/typing:stopপাঠায় → servertyping:updatebroadcast করে - 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 ব্যবহার করে।
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.tsStep 1 — Project Init
Root-এ workspace বানান, তারপর তিনটি package install করুন।
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{
"name": "socketio-chat-app",
"private": true,
"workspaces": ["shared", "server", "client"]
}তারপর server ও client-এর dependency যোগ করুন। ভাগ করা @chat/shared টাইপ-only, তাই এতে কোনো runtime dependency লাগে না।
# 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{
"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.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>;{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["node"]
},
"include": ["src"]
}{
"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/.env
PORT=4000
CLIENT_ORIGIN=http://localhost:3000// 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.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}`);
});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-এ।
// 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.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/.env
PORT=4000
CLIENT_ORIGIN=http://localhost:3000# server চালু করুন
npm run dev -w server
# output: Server চলছে: http://localhost:4000Step 5 — Next.js Client Setup
Next.js app-এ Socket.IO client URL একটি environment variable থেকে নেওয়া হবে — কোনো production URL hardcode নয়।
# client/.env.local
NEXT_PUBLIC_SOCKET_URL=http://localhost:4000Step 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.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.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.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()দিন।
// ❌ ভুল — প্রতিটি 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();
};
}, []);// server/src/socket.ts (reconnect-পরবর্তী)
// client একই socket নিয়ে আবার যুক্ত হয়; join listener আবার user:join পাঠায়,
// তাই reconnect হলেও online list ঠিক থাকে।
// (দেখুন: [Reconnection](/tech-tutor/socketio/reconnection-error-handling))// আরও নিশ্চিত করতে চাইলে Connection State Recovery চালু করুন
const io = new Server(httpServer, {
cors: { origin: CLIENT_ORIGIN },
connectionStateRecovery: {
maxDisconnectionDuration: 2 * 60 * 1000,
},
});// 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");
});// 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।
# terminal 1 — backend (port 4000)
npm run dev -w server
# terminal 2 — frontend (port 3000)
npm run dev -w client# 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
- Connection status — page খুললে header-এ
Connectedও সবুজ dot দেখুন - Username — একটি নাম দিয়ে Join করুন
- Presence — দ্বিতীয় browser tab/incognito-তে অন্যদের online list ও count বাড়তে দেখুন
- Messages — দুই window থেকে message পাঠান; username, text ও timestamp সব দেখুন
- Typing — একটি window-তে টাইপ করুন; অন্যটিতে "… লিখছেন..." দেখুন
- Reconnection — server বন্ধ করলে status
Disconnectedহবে; আবার চালু করলে স্বয়ংক্রিয়ভাবে reconnect হবে (কিছু সেকেন্ড অপেক্ষা করুন) - Validation — খালি message বা খুব ছোট username আটকে যাবে
- XSS —
<b>test</b>পাঠান; এটি bold না হয়ে টেক্সট হিসেবে দেখাবে
Next.js dev mode-এ React StrictMode effect দুইবার চালায় — তাই cleanup ঠিকভাবে লেখা জরুরি, নাহলে duplicate listener হবে। client component-এ অবশ্যই "use client" দিন ও socket কেবল effect-এ তৈরি করুন।
Troubleshooting
| সমস্যা | কারণ ও সমাধান |
|---|---|
connect_error / CORS | server-এর cors.origin-এ client origin (http://localhost:3000) দিন; \"*\" নয় |
| Connection refused | server চালু আছে কি? http://localhost:4000/health খুলে দেখুন |
| দুবার message আসছে | duplicate listener — event-এ on বারবার নয়, cleanup করুন |
NEXT_PUBLIC_SOCKET_URL কাজ করছে না | .env.local বানানোর পর dev server restart করুন |
| Type mismatch | server ও 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-memory state:
- online users Map, typing Map server process-এর memory-তে
- server restart / crash → সব হারায়
- একাধিক node → state প্রতিটিতে আলাদা
- durable/persistent নয়; production-এ Redis adapter + DB লাগবেএই app-এ কোনো database নেই — server message সংরক্ষণ করে না। তাই নতুন join করা user আগের message দেখবে না, আর page reload করলে message list মুছে যায়।
# client/components/chat.tsx (typing state)
// typing indicator server-এর memory-ভিত্তিক Set থেকে আসে
// reconnect হলে client আবার join করে online list ঠিক হয়// server verify (production-এ যোগ করুন)
// - username: zod/validation library দিয়ে schema check
// - message: max length, নিষিদ্ধ content filter
// - connection: IP rate limit
// - auth: handshake.auth.token verify middlewareOnline list ও typing state শুধু memory-তে — server restart-এ হারায় এবং একাধিক node-এ ভাগ হয় না। একাধিক instance চলালে Redis adapter (@socket.io/redis-adapter) ও external storage লাগবে (Best Practices)।
Practice Task
- project-টি উপরের সব file দিয়ে সম্পূর্ণভাবে locally চালান
- একটি
roomsfeature যোগ করুন — 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