Aplicações de chat, painéis de monitoramento, jogos online e notificações precisam atualizar a tela assim que um evento acontece. Fazer várias requisições HTTP em intervalos curtos funciona em alguns casos, mas aumenta o número de chamadas e pode atrasar a experiência. Neste tutorial, você vai aprender a usar WebSocket com Node.js para criar um chat em tempo real com servidor, interface web, mensagens em JSON e cuidados importantes para produção.
O projeto será pequeno o bastante para quem está começando, mas já mostrará uma estrutura que pode evoluir para salas, autenticação, histórico e integração com banco de dados. Caso ainda esteja conhecendo o ambiente, leia primeiro o que é Node.js e revise como o JavaScript funciona.
O que é WebSocket?
WebSocket é um protocolo que mantém uma conexão aberta entre cliente e servidor. Depois da negociação inicial, os dois lados podem enviar dados quando necessário, sem abrir uma nova requisição para cada mensagem. Esse modelo é chamado de comunicação bidirecional ou full duplex.
Em uma API REST comum, o navegador faz uma solicitação e espera uma resposta. No WebSocket, o servidor também pode iniciar o envio de uma mensagem. Por isso, ele é útil para chats, placares ao vivo, rastreamento de entregas, editores colaborativos e dashboards. A documentação da API WebSocket no MDN apresenta os objetos e eventos disponíveis no navegador.
WebSocket substitui uma API REST?
Não necessariamente. As duas abordagens resolvem problemas diferentes e frequentemente trabalham juntas. Uma API REST é ótima para login, cadastro, consulta de dados, envio de formulários e operações que têm começo e fim claros. O WebSocket é mais adequado quando o servidor precisa entregar atualizações contínuas.
Em um sistema real, você pode autenticar o usuário por uma API, carregar as mensagens antigas por HTTP e abrir um WebSocket apenas para receber as novas. O tutorial como criar uma API com Node.js ajuda a entender essa combinação.
Preparando o projeto
Crie uma pasta para o projeto e inicialize o arquivo package.json:
mkdir chat-websocket
cd chat-websocket
npm init -y
npm install wsA biblioteca ws fornece uma implementação de cliente e servidor WebSocket para Node.js. O repositório oficial do projeto contém exemplos de servidor, broadcast, autenticação e conexão com HTTPS. Consulte a documentação oficial da biblioteca ws para detalhes sobre sua API.
Na raiz do projeto, crie esta estrutura:
chat-websocket/
├── public/
│ └── index.html
├── package.json
└── server.jsCriando o servidor WebSocket
Abra o arquivo server.js e adicione o código abaixo. Ele cria um servidor HTTP para entregar a página e conecta o WebSocket na mesma porta.
const http = require("http");
const fs = require("fs");
const path = require("path");
const { WebSocketServer, WebSocket } = require("ws");
const server = http.createServer((req, res) => {
const filePath = path.join(__dirname, "public", "index.html");
fs.readFile(filePath, (error, content) => {
if (error) {
res.writeHead(500, { "Content-Type": "text/plain" });
res.end("Erro ao carregar a página");
return;
}
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(content);
});
});
const wss = new WebSocketServer({ server });
function broadcast(data) {
for (const client of wss.clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
}
}
wss.on("connection", (socket) => {
socket.send(JSON.stringify({
type: "system",
text: "Você entrou no chat."
}));
socket.on("message", (data) => {
let message;
try {
message = JSON.parse(data.toString());
} catch {
socket.send(JSON.stringify({
type: "error",
text: "Mensagem inválida."
}));
return;
}
if (!message.name || !message.text) {
socket.send(JSON.stringify({
type: "error",
text: "Informe nome e mensagem."
}));
return;
}
const payload = JSON.stringify({
type: "message",
name: String(message.name).slice(0, 30),
text: String(message.text).slice(0, 500),
sentAt: new Date().toISOString()
});
broadcast(payload);
});
});
server.listen(3000, () => {
console.log("Servidor em http://localhost:3000");
});O evento connection é executado quando um navegador entra no servidor. Cada conexão recebe um objeto socket. O evento message captura os dados enviados pelo cliente. A função broadcast percorre os clientes conectados e repassa a mensagem para todos que estiverem com a conexão aberta.
Criando a interface do chat
Agora crie o arquivo public/index.html. A página terá dois campos, um botão e uma lista de mensagens.
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chat com WebSocket</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 720px;
margin: 40px auto;
padding: 0 16px;
}
form {
display: grid;
grid-template-columns: 140px 1fr auto;
gap: 8px;
}
input, button {
padding: 10px;
font-size: 16px;
}
#status {
margin: 12px 0;
font-weight: bold;
}
#messages {
padding: 0;
list-style: none;
}
#messages li {
padding: 10px;
border-bottom: 1px solid #ddd;
}
</style>
</head>
<body>
<h1>Chat em tempo real</h1>
<div id="status">Conectando...</div>
<form id="chat-form">
<input id="name" maxlength="30" placeholder="Seu nome" required>
<input id="message" maxlength="500" placeholder="Digite uma mensagem" required>
<button type="submit">Enviar</button>
</form>
<ul id="messages"></ul>
<script>
const statusElement = document.querySelector("#status");
const form = document.querySelector("#chat-form");
const nameInput = document.querySelector("#name");
const messageInput = document.querySelector("#message");
const messages = document.querySelector("#messages");
const protocol = location.protocol === "https:" ? "wss" : "ws";
const socket = new WebSocket(`${protocol}://${location.host}`);
function addMessage(text) {
const item = document.createElement("li");
item.textContent = text;
messages.appendChild(item);
}
socket.addEventListener("open", () => {
statusElement.textContent = "Conectado";
});
socket.addEventListener("close", () => {
statusElement.textContent = "Desconectado";
});
socket.addEventListener("error", () => {
statusElement.textContent = "Erro na conexão";
});
socket.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
if (data.type === "message") {
const time = new Date(data.sentAt).toLocaleTimeString();
addMessage(`[${time}] ${data.name}: ${data.text}`);
return;
}
addMessage(data.text);
});
form.addEventListener("submit", (event) => {
event.preventDefault();
if (socket.readyState !== WebSocket.OPEN) {
addMessage("A conexão ainda não está disponível.");
return;
}
socket.send(JSON.stringify({
name: nameInput.value.trim(),
text: messageInput.value.trim()
}));
messageInput.value = "";
messageInput.focus();
});
</script>
</body>
</html>Observe que o texto recebido é inserido com textContent, não com innerHTML. Essa escolha reduz o risco de executar HTML ou JavaScript enviado por outra pessoa. O navegador usa ws:// durante o desenvolvimento local e wss:// quando a página está protegida por HTTPS.
Executando e testando
Inicie o servidor:
node server.jsAcesse http://localhost:3000 em duas abas ou navegadores diferentes. Digite nomes distintos e envie mensagens. Como o servidor faz broadcast, cada cliente conectado deve receber a atualização imediatamente.
Abra as ferramentas de desenvolvimento do navegador e procure a seção de rede dedicada a WebSocket. Ali é possível acompanhar a negociação da conexão e os frames enviados e recebidos. Esse recurso ajuda a identificar JSON inválido, desconexões e mensagens inesperadas.
Como organizar as mensagens
Evite transmitir apenas textos soltos. Um formato JSON com um campo type permite que o cliente saiba como tratar cada evento. Por exemplo:
{
"type": "message",
"name": "Marina",
"text": "Olá!",
"sentAt": "2026-07-22T18:30:00.000Z"
}Mais tarde, você pode adicionar tipos como user_joined, user_left, typing, notification e error. Essa convenção evita vários formatos incompatíveis e facilita a criação de testes.
Reconexão automática
Conexões podem cair por troca de rede, suspensão do celular, reinício do servidor ou falha do provedor. Uma aplicação real deve tentar reconectar com atraso progressivo, em vez de abrir dezenas de conexões imediatamente. Uma estratégia comum é esperar um segundo, depois dois, quatro e assim por diante, impondo um limite máximo.
Também é importante mostrar o estado na interface. O usuário precisa saber se está conectado, reconectando ou offline. Mensagens digitadas durante a queda podem ser mantidas localmente e enviadas somente quando a conexão voltar, desde que a aplicação controle duplicidade.
Segurança em WebSocket
- Use WSS em produção: o tráfego deve passar por TLS, assim como ocorre com HTTPS.
- Valide todas as mensagens: o cliente não é uma fonte confiável.
- Limite tamanho e frequência: mensagens enormes ou repetidas podem consumir memória e processamento.
- Autentique a conexão: relacione cada socket a um usuário autorizado.
- Valide a origem: rejeite conexões vindas de domínios não permitidos.
- Escape a saída: nunca injete conteúdo recebido diretamente como HTML.
O exemplo limita o nome e o texto, mas isso é apenas uma primeira camada. Em produção, use validação de esquema, controle de acesso, logs, bloqueio por IP quando necessário e regras claras para cada tipo de evento.
Escalabilidade e desempenho
Um único processo funciona bem para aprender e para projetos pequenos. Quando há vários servidores, cada instância conhece apenas seus próprios clientes. Para transmitir uma mensagem a todos, é comum usar um sistema intermediário, como Redis Pub/Sub, para compartilhar eventos entre as instâncias.
Monitore quantidade de conexões, memória, latência, taxa de mensagens e desconexões. Aplique heartbeat com mensagens de ping e pong para remover clientes que perderam a rede sem fechar corretamente o socket. Para outras práticas no backend, veja como otimizar APIs RESTful em Node.js.
Testes e deploy
Separe a lógica de validação e criação de mensagens em funções independentes. Assim, você consegue testá-las sem abrir uma conexão real. Depois, adicione testes de integração que conectam clientes, enviam eventos e verificam o broadcast. O guia de testes unitários com Jest pode servir como ponto de partida.
No deploy, confirme que o proxy reverso aceita o upgrade de conexão e não encerra sockets ativos com um tempo limite muito curto. Para automatizar a entrega do projeto, consulte o tutorial sobre deploy com GitHub Actions. Também é possível empacotar a aplicação seguindo o guia sobre Docker e contêineres.
Próximos recursos para o chat
- salas separadas por assunto;
- autenticação de usuários;
- lista de participantes online;
- indicador de digitação;
- confirmação de entrega e leitura;
- histórico salvo em banco de dados;
- moderação e bloqueio;
- upload de arquivos com limites;
- notificações quando a aba estiver em segundo plano.
Implemente um recurso de cada vez e defina claramente os eventos trocados entre cliente e servidor. Um pequeno documento com nome do evento, campos obrigatórios e exemplo de payload evita inconsistências conforme o projeto cresce.
Erros comuns
- Enviar antes de conectar: confira
readyStateantes de usarsend. - Usar WS em página HTTPS: em produção, utilize
wss://. - Confiar no JSON recebido: valide formato, tamanho e permissões.
- Esquecer clientes desconectados: faça heartbeat e trate o evento
close. - Usar innerHTML com mensagens: prefira
textContent. - Não planejar múltiplas instâncias: adote um canal compartilhado quando escalar.
Conclusão
Criar um chat com WebSocket com Node.js é uma maneira prática de aprender comunicação em tempo real. O servidor mantém conexões abertas, recebe mensagens, valida os dados e distribui eventos para os clientes conectados. O navegador, por sua vez, reage aos eventos de abertura, mensagem, erro e encerramento.
O exemplo deste artigo oferece uma base funcional, mas a etapa seguinte é tornar o projeto resistente a falhas. Adicione autenticação, reconexão, validação de esquema, heartbeat, limites de uso, armazenamento e observabilidade. Com esses cuidados, o mesmo princípio pode atender chats, painéis, jogos, notificações e diversas aplicações colaborativas.




