Socket.IO do Zero: Chat Completo com Salas, Typing Indicator e Histórico de Mensagens
O problema com tutoriais de chat
A maioria dos tutoriais de Socket.IO para no "Hello World": um emit no cliente, um on no servidor, e pronto. O resultado é um chat que perde mensagens quando o usuário recarrega a página, não separa conversas por sala, e mostra "fulano está digitando" para todos os usuários conectados ao servidor, independente de onde estão.
Este post constrói um chat que resolve esses três problemas de verdade: salas isoladas com controle de entrada e saída, typing indicator com debounce que não inunda o servidor de eventos, e histórico persistido em PostgreSQL via Prisma. O código roda, tem tipos, e você pode copiar e adaptar.
Arquitetura do servidor com Express e Socket.IO
O servidor usa Express como base HTTP e Socket.IO como camada de WebSocket. A separação importa: Express serve o health check e qualquer endpoint REST que você precise, enquanto Socket.IO cuida exclusivamente da comunicação bidirecional.
// src/server.ts
import express from "express";
import { createServer } from "node:http";
import { Server } from "socket.io";
import { PrismaClient } from "@prisma/client";
const app = express();
const httpServer = createServer(app);
const prisma = new PrismaClient();
// cors configurado para o domínio do frontend; "*" só em dev
const io = new Server(httpServer, {
cors: {
origin: process.env.CLIENT_ORIGIN ?? "http://localhost:3000",
methods: ["GET", "POST"],
},
// pingTimeout alto evita desconexões falsas em redes móveis
pingTimeout: 30_000,
pingInterval: 25_000,
});
app.get("/health", (_req, res) => {
res.json({ status: "ok", connections: io.engine.clientsCount });
});
const PORT = Number(process.env.PORT) || 4000;
httpServer.listen(PORT, () => {
console.log(`Server listening on :${PORT}`);
});
export { io, prisma };
Se você precisa escalar para múltiplas instâncias, o Socket.IO sozinho não resolve: cada processo Node.js mantém seu próprio mapa de sockets. A solução padrão é o adapter do Redis, que sincroniza eventos entre processos via Pub/Sub.
Schema do banco: mensagens e salas
O modelo é simples de propósito. Uma tabela de salas, uma de mensagens com foreign key para a sala, e o username como string (sem autenticação neste escopo, mas você pode integrar com NextAuth.js quando precisar).
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Room {
id String @id @default(cuid())
name String @unique
createdAt DateTime @default(now())
messages Message[]
}
model Message {
id String @id @default(cuid())
content String
username String
roomId String
room Room @relation(fields: [roomId], references: [id])
createdAt DateTime @default(now())
@@index([roomId, createdAt]) // índice composto para paginação por sala
}
O índice composto [roomId, createdAt] é o que permite buscar "as últimas 50 mensagens da sala X" sem full scan. Sem ele, a query de histórico degrada linearmente com o volume de mensagens. Se você quer entender mais sobre modelagem de dados com Prisma, o post sobre API REST com Fastify e Prisma cobre o setup completo.
Tipos compartilhados entre cliente e servidor
Definir os tipos dos eventos em um arquivo compartilhado evita o problema clássico de emitir "mesage" (com typo) no cliente e escutar "message" no servidor.
// src/types/events.ts
export interface ServerToClientEvents {
"message:new": (message: MessagePayload) => void;
"room:joined": (data: { roomId: string; username: string }) => void;
"room:left": (data: { roomId: string; username: string }) => void;
"typing:update": (data: { username: string; isTyping: boolean }) => void;
"
---
Leia o artigo completo em [https://www.vivodecodigo.com.br/backend/socketio-chat-salas-typing-indicator-historico-mensagens-1786104123738](https://www.vivodecodigo.com.br/backend/socketio-chat-salas-typing-indicator-historico-mensagens-1786104123738)