Use Cases (Atualizado: 03/06/2026)

Criar um chat WebSocket com Claude Code

Guia para chat WebSocket com Claude Code: autenticação, reconexão, limites e código Node.js executável.

Criar um chat WebSocket com Claude Code

Um chat WebSocket é pequeno o bastante para construir rápido, mas realista o bastante para mostrar problemas de produção: autenticação no momento da conexão, reconexão depois de queda, envio para a sala correta, limite contra spam, validação de JSON e um teste simples para garantir que a função ainda funciona.

Neste guia usamos a API WebSocket do navegador e o pacote ws do Node.js. WebSocket é uma conexão persistente e bidirecional: depois do upgrade HTTP, navegador e servidor podem enviar mensagens pela mesma conexão. Use fontes oficiais enquanto adapta o exemplo: MDN WebSocket API, Writing WebSocket client applications, RFC 6455, evento upgrade do Node.js HTTP, README do ws e guia OWASP para teste de WebSockets. Para o fluxo da ferramenta, consulte a documentação oficial do Claude Code.

Para continuar, veja também desenvolvimento de API com Claude Code, code review com Claude Code e boas práticas de segurança.

Três usos práticos

O primeiro uso é um chat pequeno para comunidade ou suporte. Sala, nome, texto e histórico recente bastam para começar. Sem autenticação e limite, porém, um único script pode enviar mensagens sem parar.

O segundo uso é um painel em tempo real. Eventos de deploy, pagamentos, estoque e filas podem aparecer no navegador assim que acontecem. O ponto crítico é decidir quais eventos antigos podem ser descartados, porque a API WebSocket do navegador não oferece contrapressão automática forte.

O terceiro uso é uma sala de perguntas ligada a artigos, cursos ou materiais pagos. O leitor passa do conteúdo para a conversa, e a equipe pode indicar templates, produtos ou consultoria. Isso ajuda a conversão, mas exige regras claras de log, moderação e dados pessoais.

UsoPor que WebSocket ajudaDecisão inicial
Chat pequenoMensagens bidirecionais rápidasAutenticação, salas, histórico
Painel ao vivoServidor envia eventosDescarte e reconexão
Suporte educacionalConversa segue o artigoLogs, moderação, chamada para ação

Arquitetura

Fan-out é distribuir uma mensagem recebida para todos os clientes da mesma sala. Heartbeat é verificar periodicamente se a conexão longa ainda está viva.

flowchart LR
  BrowserA["Aba A"] -->|ws:// /chat| Node["Node.js server"]
  BrowserB["Aba 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"]

Prompt para Claude Code

Crie um chat WebSocket mínimo com Node.js 20+ e o pacote ws.

Requisitos:
- Usar apenas server.js, index.html, smoke-client.mjs e package.json
- Servir a interface em http://localhost:8080
- Validar token e Origin durante o upgrade WebSocket de /chat
- Fazer fan-out por sala
- Manter só as últimas 50 mensagens em memória
- Limitar cada conexão a 20 mensagens por 10 segundos
- Limitar texto a 500 caracteres
- Reconnect no cliente com backoff exponencial após close
- Verificar readyState e bufferedAmount antes de enviar
- smoke-client.mjs deve conectar, enviar uma mensagem e sair com sucesso

Não faça:
- Trocar por Socket.IO
- Aceitar conexão sem autenticação
- Usar JSON sem validar
- Descrever histórico em memória como persistência de produção

Executar localmente

mkdir ws-chat-demo
cd ws-chat-demo
npm init -y
npm install ws
npm run start

Troque o 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="pt">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Demonstração de chat 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>Demonstração de chat WebSocket</h1>
    <p id="status" class="muted">offline</p>

    <label>Sala <input id="room" value="lobby" /></label>
    <label>Nome <input id="name" value="masa" /></label>
    <label>Token <input id="token" value="dev-token" /></label>
    <button id="connect">Conectar</button>
    <button id="disconnect">Desconectar</button>

    <div id="log" aria-live="polite"></div>

    <form id="form">
      <label>Mensagem <input id="message" autocomplete="off" maxlength="500" /></label>
      <button type="submit">Enviar</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 ainda não está pronto; mensagem em fila.", "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("conectando");

        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(`fechado ${event.code}`);
          if (!manuallyClosed) scheduleReconnect();
        });

        socket.addEventListener("error", () => {
          writeLog("Erro WebSocket. Verifique o log do servidor.", "error");
        });
      }

      function scheduleReconnect() {
        const delay = Math.min(1000 * 2 ** reconnectAttempt, 10000) + Math.floor(Math.random() * 250);
        reconnectAttempt += 1;
        setStatus(`reconectando em ${delay}ms`);
        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} mensagens anteriores carregadas.`, "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

Armadilhas comuns

FalhaImpactoCorreção
Não autenticar no upgradeSocket não confiável fica ativoRejeitar antes de handleUpgrade
Não validar OriginRisco de conexão por outro siteLista de origens permitidas
Sem limite de envioUm cliente consome CPU e memóriaLimitar por conexão, usuário e IP
Histórico infinito em memóriaProcesso cresce sem pararLimitar quantidade, idade e armazenamento
Reconexão imediataPico de tráfego na recuperaçãoBackoff exponencial com variação
Fan-out só em um processoOutros servidores não recebemRedis Pub/Sub, NATS ou fila

Produção e chamada para ação

Use wss:// sempre que houver conta, pagamento, suporte ou dados pessoais. A autorização precisa existir ao conectar, entrar em sala, enviar, apagar e moderar. O histórico deve ir para banco de dados, e múltiplos processos Node precisam de Pub/Sub compartilhado.

Se você está estudando sozinho, comece pelo cheatsheet gratuito de Claude Code. Para prompts e materiais reutilizáveis, veja os produtos ClaudeCodeLab. Equipes que precisam adaptar autenticação, auditoria, Pub/Sub, CI e revisão ao repositório real podem usar treinamento e consultoria Claude Code.

Resultado testado: Masa iniciou server.js, abriu http://localhost:8080 em duas abas e viu as mensagens da sala lobby aparecerem imediatamente nas duas. npm run smoke enviou e recebeu hello from smoke test. Alterar Origin retornou 403, e alterar token retornou 401. Colocar reconexão, limite, validação de Origin e histórico de 50 mensagens no pedido inicial ao Claude Code deixou a revisão muito mais simples.

#Claude Code #WebSocket #chat #tempo real #Node.js
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.