Executando verificação de segurança...
0

🚀 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.yml para 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?

  1. LĂŞ o modelo Message do seu schema.prisma.
  2. Gera os arquivos em src/features/message/.
  3. Cria um message.controller.ts com ações CRUD (Create, Read, Update, Delete).
  4. Gera validações Zod a partir dos tipos do Prisma, garantindo que os dados que chegam na sua API são seguros.
  5. 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:

  1. Habilitar o Stream na Ação list: Adicionamos a flag stream: true. Isso diz ao Igniter.js que este endpoint será uma conexão SSE persistente, para a qual os clientes podem se inscrever.

  2. 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ção createMessage.mutate() para chamar nossa API de criação. É totalmente type-safe.
  • Simplicidade: Note que nĂŁo há cĂłdigo manual de EventSource ou 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:

Obrigado por construir junto comigo hoje. Até a próxima! 🚀

Carregando publicação patrocinada...