Claude Code से WebSocket चैट ऐप बनाएं
Claude Code से सुरक्षित WebSocket चैट बनाएं: प्रमाणीकरण, पुनःकनेक्शन, दर सीमा और चलने वाला Node.js कोड।
WebSocket चैट छोटा काम लगता है, लेकिन इसी में उत्पादन जैसी कई बातें आ जाती हैं: कनेक्शन पर प्रमाणीकरण, नेटवर्क टूटने पर दोबारा जुड़ना, कमरे के भीतर संदेश भेजना, स्पैम रोकने के लिए सीमा, JSON जांच और एक छोटा smoke test। Claude Code तेज़ी से फाइलें बना सकता है, लेकिन अगर निर्देश सिर्फ “chat app बना दो” है, तो अक्सर बिना सुरक्षा वाला demo मिलेगा।
इस गाइड में ब्राउजर की WebSocket API और Node.js के ws package से ऐसा उदाहरण बनाया गया है जिसे सीधे चलाया जा सकता है। WebSocket में पहले HTTP upgrade होता है, फिर ब्राउजर और server एक ही लंबे कनेक्शन पर दोनों तरफ से संदेश भेज सकते हैं। आधिकारिक संदर्भ के लिए MDN WebSocket API, Writing WebSocket client applications, RFC 6455, Node.js HTTP upgrade event, ws README और OWASP WebSocket testing guide देखें। Claude Code workflow के लिए Claude Code docs रखें।
आगे पढ़ने के लिए Claude Code API development, Claude Code code review और security best practices उपयोगी हैं।
तीन व्यावहारिक उपयोग
पहला उपयोग छोटा समुदाय या support chat है। Room, name, text और recent history से शुरुआत हो जाती है। लेकिन प्रमाणीकरण और भेजने की सीमा न हो, तो एक script server को flood कर सकती है।
दूसरा उपयोग live dashboard है। Build status, payment event, stock update या job queue status तुरंत ब्राउजर में दिख सकते हैं। यहाँ सवाल यह है कि पुराने events कब हटाने हैं, क्योंकि browser WebSocket API में automatic strong backpressure नहीं है।
तीसरा उपयोग article, course या paid material के बाद question room है। पाठक सामग्री पढ़कर सवाल पूछ सकता है, और team उसे template, product या consultation page तक ले जा सकती है। यह आय मार्ग में मदद करता है, पर logs, moderation और personal data rules पहले लिखने पड़ते हैं।
| उपयोग | WebSocket क्यों उपयुक्त है | पहले क्या तय करें |
|---|---|---|
| छोटा chat | कम विलंब वाले दोतरफा messages | Auth, rooms, history |
| Live dashboard | Server update push कर सकता है | Drop policy, reconnect |
| Learning support | Article से conversation बनती है | Logs, moderation, CTA |
Architecture
Fan-out यानी एक incoming message को उसी room के सभी connections तक भेजना। Heartbeat यानी लंबे connection के जीवित होने की नियमित जांच।
flowchart LR
BrowserA["Browser tab A"] -->|ws:// /chat| Node["Node.js server"]
BrowserB["Browser tab B"] -->|ws:// /chat| Node
Smoke["smoke-client.mjs"] -->|ws:// /chat| Node
Node --> Auth["token and Origin check"]
Node --> Rooms["room registry"]
Rooms --> Fanout["fan-out to room peers"]
Node --> History["last 50 messages"]
Node --> Limit["rate limit and max payload"]
Claude Code के लिए निर्देश
Node.js 20+ और ws package से minimal WebSocket chat बनाइए।
Requirements:
- सिर्फ server.js, index.html, smoke-client.mjs और package.json
- http://localhost:8080 पर chat UI serve हो
- /chat WebSocket upgrade में token और Origin validate हों
- room के हिसाब से fan-out हो
- memory में सिर्फ latest 50 messages रहें
- हर connection पर 10 seconds में 20 messages की limit हो
- message text 500 characters तक सीमित हो
- browser client close के बाद exponential backoff से reconnect करे
- send से पहले readyState और bufferedAmount check हों
- smoke-client.mjs connect करे, एक message भेजे और success से exit करे
न करें:
- इसे Socket.IO में न बदलें
- unauthenticated connection accept न करें
- JSON input बिना validation use न करें
- memory history को production persistence की तरह न लिखें
स्थानीय सेटअप
mkdir ws-chat-demo
cd ws-chat-demo
npm init -y
npm install ws
npm run start
package.json:
{
"type": "module",
"scripts": {
"start": "node server.js",
"smoke": "node smoke-client.mjs"
},
"dependencies": {
"ws": "^8.18.3"
},
"engines": {
"node": ">=20"
}
}
server.js
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { createServer } from "node:http";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { WebSocket, WebSocketServer } from "ws";
const PORT = Number(process.env.PORT ?? 8080);
const AUTH_TOKEN = process.env.CHAT_TOKEN ?? "dev-token";
const MAX_MESSAGE_LENGTH = 500;
const HISTORY_LIMIT = 50;
const RATE_WINDOW_MS = 10_000;
const RATE_LIMIT = 20;
const CLIENT_FILE = join(fileURLToPath(new URL(".", import.meta.url)), "index.html");
const ALLOWED_ORIGINS = new Set(
(process.env.ALLOWED_ORIGINS ?? `http://localhost:${PORT},http://127.0.0.1:${PORT}`)
.split(",")
.map((value) => value.trim())
.filter(Boolean)
);
const rooms = new Map();
const histories = new Map();
const clients = new Map();
const server = createServer(async (request, response) => {
if (request.url === "/" || request.url === "/index.html") {
try {
const html = await readFile(CLIENT_FILE, "utf8");
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
response.end(html);
} catch {
response.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
response.end("index.html was not found");
}
return;
}
if (request.url === "/healthz") {
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ ok: true, clients: clients.size }));
return;
}
response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
response.end("not found");
});
const wss = new WebSocketServer({ noServer: true, maxPayload: 2048 });
server.on("upgrade", (request, socket, head) => {
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
if (url.pathname !== "/chat") {
rejectUpgrade(socket, 404, "Not Found");
return;
}
const origin = request.headers.origin ?? "";
if (!ALLOWED_ORIGINS.has(origin)) {
rejectUpgrade(socket, 403, "Forbidden");
return;
}
const token = url.searchParams.get("token");
if (token !== AUTH_TOKEN) {
rejectUpgrade(socket, 401, "Unauthorized");
return;
}
const context = {
name: cleanName(url.searchParams.get("name")),
room: cleanRoom(url.searchParams.get("room"))
};
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit("connection", ws, request, context);
});
});
wss.on("connection", (ws, request, context) => {
const client = {
id: randomUUID(),
name: context.name,
room: context.room,
isAlive: true,
rateResetAt: Date.now() + RATE_WINDOW_MS,
messagesInWindow: 0
};
clients.set(ws, client);
joinRoom(ws, client.room);
send(ws, { type: "system", message: `connected as ${client.name}`, clientId: client.id });
send(ws, { type: "history", messages: histories.get(client.room) ?? [] });
broadcast(client.room, { type: "presence", message: `${client.name} joined`, online: roomSize(client.room) });
ws.on("pong", () => {
client.isAlive = true;
});
ws.on("message", (raw, isBinary) => {
if (isBinary) {
ws.close(1003, "text messages only");
return;
}
if (!consumeQuota(client)) {
send(ws, { type: "error", code: "rate_limited", message: "Too many messages. Wait a moment." });
return;
}
const textBody = raw.toString("utf8");
if (textBody.length > MAX_MESSAGE_LENGTH) {
send(ws, { type: "error", code: "too_large", message: `Message must be ${MAX_MESSAGE_LENGTH} characters or less.` });
return;
}
let event;
try {
event = JSON.parse(textBody);
} catch {
send(ws, { type: "error", code: "bad_json", message: "Send JSON such as {\"type\":\"message\",\"text\":\"hi\"}." });
return;
}
if (event.type !== "message") {
send(ws, { type: "error", code: "bad_type", message: "Only message events are accepted." });
return;
}
const text = String(event.text ?? "").trim();
if (!text) {
send(ws, { type: "error", code: "empty", message: "Message text is required." });
return;
}
const message = {
id: randomUUID(),
room: client.room,
from: client.name,
text,
sentAt: new Date().toISOString()
};
remember(client.room, message);
broadcast(client.room, { type: "message", message });
});
ws.on("close", () => {
leaveRoom(ws);
clients.delete(ws);
broadcast(client.room, { type: "presence", message: `${client.name} left`, online: roomSize(client.room) });
});
ws.on("error", (error) => {
console.error("websocket error", error);
});
});
const heartbeat = setInterval(() => {
for (const ws of wss.clients) {
const client = clients.get(ws);
if (!client) continue;
if (!client.isAlive) {
ws.terminate();
continue;
}
client.isAlive = false;
ws.ping();
}
}, 30_000);
server.listen(PORT, () => {
console.log(`Chat demo: http://localhost:${PORT}`);
console.log(`Token: ${AUTH_TOKEN}`);
});
process.on("SIGINT", () => {
clearInterval(heartbeat);
wss.close();
server.close(() => process.exit(0));
});
function rejectUpgrade(socket, statusCode, message) {
socket.write(`HTTP/1.1 ${statusCode} ${message}\r\nConnection: close\r\n\r\n`);
socket.destroy();
}
function cleanName(value) {
const name = String(value ?? "guest").trim().slice(0, 24);
return /^[\w -]+$/.test(name) ? name : "guest";
}
function cleanRoom(value) {
const room = String(value ?? "lobby").trim().slice(0, 32);
return /^[a-zA-Z0-9_-]+$/.test(room) ? room : "lobby";
}
function joinRoom(ws, room) {
if (!rooms.has(room)) rooms.set(room, new Set());
rooms.get(room).add(ws);
}
function leaveRoom(ws) {
const client = clients.get(ws);
if (!client) return;
const peers = rooms.get(client.room);
if (!peers) return;
peers.delete(ws);
if (peers.size === 0) rooms.delete(client.room);
}
function roomSize(room) {
return rooms.get(room)?.size ?? 0;
}
function send(ws, payload) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(payload));
}
}
function broadcast(room, payload) {
const peers = rooms.get(room);
if (!peers) return;
const body = JSON.stringify(payload);
for (const peer of peers) {
if (peer.readyState === WebSocket.OPEN) {
peer.send(body);
}
}
}
function remember(room, message) {
const history = histories.get(room) ?? [];
history.push(message);
while (history.length > HISTORY_LIMIT) history.shift();
histories.set(room, history);
}
function consumeQuota(client) {
const now = Date.now();
if (now > client.rateResetAt) {
client.rateResetAt = now + RATE_WINDOW_MS;
client.messagesInWindow = 0;
}
client.messagesInWindow += 1;
return client.messagesInWindow <= RATE_LIMIT;
}
index.html
<!doctype html>
<html lang="hi">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WebSocket चैट डेमो</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 760px; margin: 32px auto; padding: 0 16px; }
label { display: block; margin-top: 12px; }
input, button { font: inherit; padding: 8px; }
#log { border: 1px solid #ccc; min-height: 240px; padding: 12px; overflow: auto; }
.message { margin: 0 0 8px; }
.muted { color: #666; }
.error { color: #b00020; }
</style>
</head>
<body>
<h1>WebSocket चैट डेमो</h1>
<p id="status" class="muted">offline</p>
<label>कमरा <input id="room" value="lobby" /></label>
<label>नाम <input id="name" value="masa" /></label>
<label>टोकन <input id="token" value="dev-token" /></label>
<button id="connect">जोड़ें</button>
<button id="disconnect">तोड़ें</button>
<div id="log" aria-live="polite"></div>
<form id="form">
<label>संदेश <input id="message" autocomplete="off" maxlength="500" /></label>
<button type="submit">भेजें</button>
</form>
<script>
const statusEl = document.querySelector("#status");
const logEl = document.querySelector("#log");
const formEl = document.querySelector("#form");
const roomEl = document.querySelector("#room");
const nameEl = document.querySelector("#name");
const tokenEl = document.querySelector("#token");
const messageEl = document.querySelector("#message");
const connectEl = document.querySelector("#connect");
const disconnectEl = document.querySelector("#disconnect");
let socket;
let reconnectTimer;
let reconnectAttempt = 0;
let manuallyClosed = false;
const pendingMessages = [];
connectEl.addEventListener("click", connect);
disconnectEl.addEventListener("click", () => {
manuallyClosed = true;
clearTimeout(reconnectTimer);
if (socket) socket.close(1000, "user disconnected");
});
formEl.addEventListener("submit", (event) => {
event.preventDefault();
const text = messageEl.value.trim();
if (!text) return;
const payload = JSON.stringify({ type: "message", text });
if (socket?.readyState === WebSocket.OPEN && socket.bufferedAmount < 64 * 1024) {
socket.send(payload);
} else {
pendingMessages.push(payload);
writeLog("Socket तैयार नहीं है, message queue में रखा गया।", "muted");
}
messageEl.value = "";
});
function connect() {
manuallyClosed = false;
clearTimeout(reconnectTimer);
if (socket && socket.readyState === WebSocket.OPEN) return;
const params = new URLSearchParams({
token: tokenEl.value,
name: nameEl.value,
room: roomEl.value
});
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
socket = new WebSocket(`${protocol}//${location.host}/chat?${params.toString()}`);
setStatus("connecting");
socket.addEventListener("open", () => {
reconnectAttempt = 0;
setStatus("online");
flushPending();
});
socket.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
renderEvent(data);
});
socket.addEventListener("close", (event) => {
setStatus(`closed ${event.code}`);
if (!manuallyClosed) scheduleReconnect();
});
socket.addEventListener("error", () => {
writeLog("WebSocket error. Server log देखें।", "error");
});
}
function scheduleReconnect() {
const delay = Math.min(1000 * 2 ** reconnectAttempt, 10000) + Math.floor(Math.random() * 250);
reconnectAttempt += 1;
setStatus(`${delay}ms में reconnect`);
reconnectTimer = setTimeout(connect, delay);
}
function flushPending() {
while (pendingMessages.length > 0 && socket.readyState === WebSocket.OPEN && socket.bufferedAmount < 64 * 1024) {
socket.send(pendingMessages.shift());
}
}
function renderEvent(data) {
if (data.type === "message") {
writeLog(`${data.message.from}: ${data.message.text}`);
} else if (data.type === "history") {
writeLog(`${data.messages.length} पुराने messages loaded.`, "muted");
data.messages.forEach((message) => writeLog(`${message.from}: ${message.text}`));
} else if (data.type === "error") {
writeLog(`${data.code}: ${data.message}`, "error");
} else {
writeLog(data.message ?? JSON.stringify(data), "muted");
}
}
function setStatus(value) {
statusEl.textContent = value;
}
function writeLog(value, className = "message") {
const line = document.createElement("p");
line.className = className;
line.textContent = value;
logEl.append(line);
logEl.scrollTop = logEl.scrollHeight;
}
connect();
</script>
</body>
</html>
smoke-client.mjs
import { WebSocket } from "ws";
const url = new URL("ws://localhost:8080/chat");
url.searchParams.set("token", process.env.CHAT_TOKEN ?? "dev-token");
url.searchParams.set("name", "smoke");
url.searchParams.set("room", "lobby");
const ws = new WebSocket(url, {
headers: {
Origin: "http://localhost:8080"
}
});
const timeout = setTimeout(() => {
console.error("smoke test timed out");
ws.terminate();
process.exit(1);
}, 5000);
ws.on("open", () => {
ws.send(JSON.stringify({ type: "message", text: "hello from smoke test" }));
});
ws.on("message", (raw) => {
const data = JSON.parse(raw.toString("utf8"));
console.log(data);
if (data.type === "message" && data.message.text === "hello from smoke test") {
clearTimeout(timeout);
ws.close(1000, "done");
}
});
ws.on("close", (code, reason) => {
console.log(`closed ${code} ${reason.toString()}`);
process.exit(code === 1000 ? 0 : 1);
});
ws.on("error", (error) => {
clearTimeout(timeout);
console.error(error);
process.exit(1);
});
npm run smoke
सामान्य गलतियां
| गलती | असर | सुधार |
|---|---|---|
| upgrade में auth नहीं | Untrusted socket active हो जाता है | handleUpgrade से पहले reject |
| Origin check नहीं | Cross-site WebSocket risk | Allowed origins list |
| Rate limit नहीं | एक client CPU और memory खा सकता है | Connection, user, IP limit |
| Unlimited memory history | Process धीरे-धीरे भारी होगा | Count, age और storage policy |
| Immediate reconnect loop | Recovery में traffic spike | Exponential backoff और jitter |
| Single-process fan-out | Multi-server setup में message miss | Redis Pub/Sub, NATS या queue |
उत्पादन और आय मार्ग
Account, payment, support या personal data हो तो wss:// इस्तेमाल करें। Authorization सिर्फ connection पर नहीं, room join, send, delete और moderation पर भी check होनी चाहिए। History database में जाए; multiple Node processes के लिए shared Pub/Sub चाहिए।
Solo learning के लिए free Claude Code cheatsheet से शुरू करें। Reusable prompts और setup material चाहिए तो ClaudeCodeLab products देखें। Team को authentication, audit log, Pub/Sub, CI और review rules real repository में fit करने हों तो Claude Code training और consultation बेहतर अगला कदम है।
Masa ने इस workflow को चलाकर देखा: server.js start करने के बाद http://localhost:8080 दो tabs में खोला, और same lobby room की messages दोनों tabs में तुरंत दिखीं। npm run smoke ने hello from smoke test send और receive किया। Origin बदलने पर 403 और token बदलने पर 401 मिला। Reconnect, rate limit, Origin validation और 50-message history को prompt में पहले लिखने से Claude Code का output review करना आसान हुआ।
मुफ़्त PDF: Claude Code cheatsheet
Email डालें और commands, review habits तथा safe workflow वाली एक-page PDF पाएँ.
हम आपका data सुरक्षित रखते हैं और spam नहीं भेजते.
लेखक के बारे में
Masa
Claude Code workflow और team adoption पर काम करने वाला engineer.
संबंधित लेख
Claude Code Obsidian to CLAUDE.md workflow: context बार-बार न समझाएं
Obsidian notes को CLAUDE.md operating notes में बदलकर Claude Code sessions को resume करना आसान बनाएं.
Claude Code Revenue CTA Routing: article से PDF, Gumroad और consultation तक
Reader intent के आधार पर free PDF, Gumroad products और consultation तक CTA route करने वाला workflow.
Claude Code टीम हैंडऑफ नियम: review proof, permissions, rollback और revenue path
Claude Code टीम काम के लिए evidence, permission rules, rollback, free PDF, Gumroad और consultation path वाला handoff.