🚀 Desafio: Um Chat em Tempo Real com Next.js em 5 Minutos? Conheça o Igniter.js Realtime!
Fala, Dev! đź‘‹ Felipe Barcelos aqui.
Hoje, quero começar com uma pergunta e um desafio. Quantas vezes você já se pegou pensando: "Preciso adicionar notificações em tempo real... um feed de atividades... um chat...", e logo em seguida sentiu aquele calafrio ao pensar na complexidade que viria pela frente? Configurar WebSockets, gerenciar conexões, lidar com canais, Pub/Sub, e garantir que tudo isso escale sem virar um pesadelo... Muitas vezes, a ideia morre antes mesmo de virar a primeira linha de código.
E se eu te dissesse que podemos construir a espinha dorsal de um chat em tempo real, funcional e robusto, em menos tempo do que vocĂŞ leva para terminar seu cafĂ©? Soa como uma promessa exagerada? Eu concordo. Mas Ă© exatamente por isso que estou aqui hoje: para provar que nĂŁo Ă©. O tĂtulo Ă© um desafio, mas tambĂ©m Ă© o resultado de meses de trabalho obsessivo no Igniter.js, um projeto que nasceu com um Ăşnico propĂłsito: devolver o nosso tempo.
Para quem acompanha minha jornada building in public aqui no TabNews, sabe que compartilhei desde a criação do framework até as soluções que ele traz para os 90% dos problemas comuns de API. A resposta de vocês foi clara: "Queremos ver na prática!".
Este artigo Ă© a minha resposta a esse pedido.
Hoje, vamos construir juntos, do zero, um aplicativo de chat completo. Não é um exemplo de brinquedo. Ao final deste guia, você terá em mãos:
- Uma API RESTful e Real-Time construĂda com Igniter.js.
- Um Frontend Reativo com Next.js (App Router) e React.
- PersistĂŞncia de dados com Prisma em um banco PostgreSQL.
- Comunicação em tempo real eficiente usando Server-Sent Events (SSE).
- Um cĂłdigo 100% Type-Safe do inĂcio ao fim, onde uma mudança no banco de dados se reflete automaticamente na sua API e no seu frontend.
Vamos explorar como a CLI do Igniter.js pode gerar uma feature inteira com um único comando, como o sistema de revalidação simplifica o gerenciamento de estado e, mais importante, como podemos construir software de alta qualidade de forma incrivelmente rápida.
Este guia é uma adaptação do tutorial oficial do nosso novo site, mas com a conversa e os insights que só a nossa comunidade aqui do TabNews proporciona.
Preparado para o desafio? Vamos ao cĂłdigo!
Pré-requisitos
Para esta jornada, vocĂŞ vai precisar de:
- Node.js 20 ou superior.
- Conhecimento básico de TypeScript, Next.js e Prisma.
- Docker para rodar um banco de dados PostgreSQL. O repositório de exemplo já inclui um
docker-compose.ymlpara facilitar.
Passo 1: O Ponto de Partida – A CLI do Igniter.js
Todo projeto começa com o setup. Em um setup manual, você estaria criando a estrutura de pastas, configurando o Next.js, o Prisma, o ESLint, o TypeScript... um processo que pode levar horas. A CLI do Igniter.js foi criada para eliminar essa fricção.
Vamos começar usando o igniter init para criar um novo projeto a partir de um template otimizado.
npx @igniter-js/cli@latest init
OBS: VocĂŞ pode ter detalhes do template aqui Template Next.js e inclusive, usar o nosso builder para adequar o template.
A CLI irá te guiar. Escolha o template Next.js App Router. Isso vai gerar um novo diretório com uma estrutura completa e pronta para produção.
cd seu-nome-de-projeto
Neste ponto, ao abrir o projeto no seu editor, eu lhe apresento o projeto configurado do Igniter.js, segue um pouco da estrutura dele abaixo:
src/
├── app/ # Next.js App Router pages and layouts
│ └── api/ # API route handlers
│ └── [[...all]]/
│ └── route.ts # Igniter.js API entry point
├── components/ # Shared, reusable UI components
├── features/ # Business logic, grouped by feature
│ └── example/
│ └── controllers/ # API endpoint definitions
├── services/ # Service initializations (Redis, Prisma, etc.)
├── igniter.ts # Igniter.js core instance
├── igniter.client.ts # Auto-generated type-safe API client
├── igniter.router.ts # Main API router
└── layout.tsx # Root layout, includes providers
Passo 2: A Fundação dos Dados – Schema com Prisma
Com a estrutura pronta, vamos definir nosso modelo de dados. Usaremos o Prisma como nossa fonte Ăşnica da verdade. Abra o arquivo prisma/schema.prisma e defina o modelo Message.
// ./prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Message {
id String @id @default(uuid())
content String
sender String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("messages")
}
Agora, vamos sincronizar nosso schema com o banco de dados. Este comando cria as tabelas necessárias.
npx prisma migrate dev --name init
O Prisma não só prepara o banco, mas também gera tipos TypeScript que usaremos em toda a aplicação, garantindo consistência.
Passo 3: Aceleração Máxima – Gerando a API com um Comando
É aqui que a produtividade do Igniter.js se destaca. Em vez de escrevermos manualmente cada endpoint (a rota, o controller, a validação de entrada), usamos a CLI para gerar uma feature completa baseada no nosso modelo Prisma.
npx @igniter-js/cli generate feature message --schema prisma:Message
O que este comando faz?
- LĂŞ o modelo
Messagedo seuschema.prisma. - Gera os arquivos em
src/features/message/. - Cria um
message.controller.tscom ações CRUD (Create, Read, Update, Delete). - Gera validações Zod a partir dos tipos do Prisma, garantindo que os dados que chegam na sua API são seguros.
- Cria uma
message.procedure.ts, que abstrai a lĂłgica de banco de dados (pense nisso como um service ou repository), mantendo seus controllers limpos.
Isso Ă© automação inteligente. VocĂŞ economizou horas de trabalho repetitivo e eliminou uma classe inteira de possĂveis bugs.
Ajustando para Tempo Real
Agora, vamos adaptar o código gerado para o nosso chat. Precisamos de duas modificações no src/features/message/controllers/message.controller.ts:
-
Habilitar o Stream na Ação
list: Adicionamos a flagstream: true. Isso diz ao Igniter.js que este endpoint será uma conexão SSE persistente, para a qual os clientes podem se inscrever. -
Modificar a Ação
create: Após criar uma mensagem, precisamos notificar todos os clientes conectados. Fazemos isso com o método.revalidate(['message.list']). Ele publica a nova mensagem no canal de stream e instrui os clientes a recarregarem a lista de mensagens.
// src/features/message/controllers/message.controller.ts
import { igniter } from '@/igniter'
import { z } from 'zod'
import { messageProcedure } from '../procedures/message.procedure'
export const messageController = igniter.controller({
path: '/messages',
actions: {
// ...outras ações geradas...
// 1. Habilitar o stream para tempo real
list: igniter.query({
stream: true, // Diz ao Igniter para tratar como um endpoint SSE
use: [messageProcedure()],
handler: async ({ context, response }) => {
const records = await context.messageRepository.findAll()
return response.success(records)
},
}),
// 2. Modificar a criação para notificar os clientes
create: igniter.mutation({
path: '/',
method: 'POST',
body: z.object({ content: z.string().min(1), sender: z.string().min(1) }),
use: [messageProcedure()],
handler: async ({ request, context, response }) => {
const newRecord = await context.messageRepository.create(request.body)
// Invalida o cache da query 'list' e publica a mensagem no stream
return response.created(newRecord).revalidate(['message.list'])
},
}),
},
})
Passo 4: Conectando os Fios na API
O controller está pronto, mas a aplicação precisa saber que ele existe. Registramos ele no router principal.
Abra src/igniter.router.ts:
// src/igniter.router.ts
import { igniter } from '@/igniter'
import { messageController } from '@/features/message/controllers/message.controller'
export const AppRouter = igniter.router({
controllers: {
// Registra o controller sob a chave 'message'
message: messageController,
},
})
export type AppRouter = typeof AppRouter
Além disso, execute o seguinte comando na sua CLI:
npx @igniter-js/cli@latest generate schema
Pronto. Nosso backend está completo. Sem a CLI, este processo envolveria criar múltiplos arquivos, escrever código repetitivo de CRUD e configurar validação manualmente.
Passo 5: Construindo o Frontend Reativo
Vamos para o frontend. Crie o arquivo src/features/message/presentation/chat.tsx.
// src/features/message/presentation/chat.tsx
'use client'
import { useEffect, useRef, useState } from 'react'
import { api } from '@/igniter.client'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Send } from 'lucide-react'
export function Chat() {
const [sender, setSender] = useState('')
const [message, setMessage] = useState('')
const [isSenderSet, setIsSenderSet] = useState(false)
const scrollAreaRef = useRef<HTMLDivElement>(null)
const messages = api.message.list.useQuery({
refetchOnWindowFocus: false,
staleTime: 0
})
const createMessage = api.message.create.useMutation()
useEffect(() => {
const storedSender = localStorage.getItem('chat-sender')
if (storedSender) {
setSender(storedSender)
setIsSenderSet(true)
}
}, [])
const handleSendMessage = async (e: React.FormEvent) => {
e.preventDefault()
if (message.trim() && sender.trim()) {
await createMessage.mutate({
body: {
content: message,
sender: sender,
},
})
setMessage('')
}
}
const handleSetSender = () => {
if (sender.trim()) {
localStorage.setItem('chat-sender', sender.trim())
setIsSenderSet(true)
}
}
useEffect(() => {
if (scrollAreaRef.current) {
const scrollElement = scrollAreaRef.current.querySelector('div > div')
if (scrollElement) {
scrollElement.scrollTo({
top: scrollElement.scrollHeight,
behavior: 'smooth',
})
}
}
}, [messages])
return (
<div className="relative flex flex-col">
<Card className="w-full rounded-none pt-0 border-none flex-1 flex flex-col">
<CardHeader className="border-b !pt-4 !pb-2 px-4">
<CardTitle>Real-time Chat with Igniter.js</CardTitle>
</CardHeader>
<CardContent className='px-4 py-0'>
<ScrollArea className=" w-full h-[calc(100vh-16.5rem)] pb-10 relative" ref={scrollAreaRef}>
<div className="space-y-4">
{messages.data?.map((msg) => (
<div
key={msg.id}
className={`flex ${
msg.sender === sender ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`p-3 rounded-lg max-w-xs ${
msg.sender === sender
? 'bg-primary text-primary-foreground'
: 'bg-muted'
}`}
>
<p className="text-sm font-bold">{msg.sender}</p>
<p>{msg.content}</p>
<p className="text-xs text-right opacity-70 mt-1">
{new Date(msg.createdAt).toLocaleTimeString()}
</p>
</div>
</div>
))}
</div>
</ScrollArea>
<form
onSubmit={handleSendMessage}
className="flex w-full items-center space-x-2"
>
<Input
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Type a message..."
disabled={!isSenderSet}
/>
<Button
type="submit"
disabled={createMessage.isLoading || !isSenderSet}
>
<Send className="h-4 w-4" />
</Button>
</form>
</CardContent>
</Card>
{!isSenderSet && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-background/80 backdrop-blur-sm">
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Welcome!</CardTitle>
<CardDescription>
Please enter your name to join the chat.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex w-full items-center space-x-2">
<Input
value={sender}
onChange={(e) => setSender(e.target.value)}
placeholder="Enter your name..."
onKeyDown={(e) => e.key === 'Enter' && handleSetSender()}
/>
<Button onClick={handleSetSender}>Join Chat</Button>
</div>
</CardContent>
</Card>
</div>
)}
</div>
)
}
Desconstruindo o Frontend
api.message.list.useQuery(): Este hook, gerado pelo Igniter.js, faz o fetch inicial dos dados. Mais importante, ele escuta os eventos de revalidação enviados pelo backend. Quando o.revalidate(['message.list'])é chamado no servidor, este hook sabe que precisa buscar os dados novamente, atualizando a UI automaticamente.api.message.create.useMutation(): Fornece a funçãocreateMessage.mutate()para chamar nossa API de criação. É totalmente type-safe.- Simplicidade: Note que não há código manual de
EventSourceou gerenciamento de estado complexo. A revalidação do Igniter.js abstrai essa complexidade.
Passo 6: Finalizando e Testando
Adicione o componente <Chat /> à sua página principal em src/app/page.tsx.
// src/app/page.tsx
import { Chat } from '@/features/message/presentation/chat'
export default function HomePage() {
return (
<main>
<Chat />
</main>
)
}
Agora, inicie o servidor de desenvolvimento:
bun dev
Abra http://localhost:3000 em duas janelas de navegador. Defina um nome de usuário em cada uma. Envie uma mensagem. Veja-a aparecer instantaneamente na outra.
Conseguimos. Um chat em tempo real, type-safe, com uma fração do código que seria necessário em um setup tradicional. Isso é o que me motiva a continuar desenvolvendo o Igniter.js: encontrar maneiras de nos permitir, como desenvolvedores, focar no que realmente importa.
ConclusĂŁo e PrĂłximos Passos
Espero que esta jornada tenha tornado tangĂvel a visĂŁo por trás do Igniter.js. Para mim, nunca foi sobre criar apenas mais uma ferramenta, mas sim sobre refinar a forma como construĂmos software. Acredito que, ao automatizar o repetitivo, liberamos nosso recurso mais valioso: o tempo para criar, experimentar e inovar.
Estou há oito meses imerso neste projeto, e a jornada tem sido incrĂvel. O Igniter.js já Ă© a base de todos os meus projetos e testes, e a produtividade que ele me trouxe Ă© a prova de que estamos no caminho certo. Sei que a estrada Ă© longa, mas estou totalmente comprometido em entregar algo que realmente faça a diferença no nosso dia a dia como desenvolvedores.
E não estou sozinho nisso. Já estamos perto de alcançar 100 estrelas no GitHub, e com o site lançado na última sexta-feira, mais de 500 pessoas já começaram a explorar o framework. Este é só o começo. O futuro do Igniter.js será moldado por vocês. Se você testar, encontrar um bug ou tiver uma ideia brilhante, sua voz é o que impulsionará a inovação. Por favor, abra uma issue no GitHub.
Se você curte acompanhar a jornada de building in public – com os desafios, as vitórias e os spoilers das próximas features – me siga nas redes sociais. É lá que eu compartilho o dia a dia do desenvolvimento:
- Instagram: @vibedev.oficial
- Threads: @felipebarcelospro
- X/Twitter: @IgniterJs
- YouTube: Felipe Barcelos (VibeDev)
Obrigado por construir junto comigo hoje. Até a próxima! 🚀