Você já perdeu uma tarde inteira debugando um erro de tipo que só aparecia em runtime, numa query que "devia" estar certa? Se a resposta é sim, você vai gostar do que o Prisma resolve.
- O problema que o Prisma resolve
- Instalando e configurando o Prisma
- Modelando o schema do banco
- Rodando migrations sem drama
- Fazendo queries type-safe
- Relações e o problema do N+1
- Erros comuns com Prisma e PostgreSQL
- FAQ
- Próximos passos
O problema que o Prisma resolve
Query builders tradicionais te dão flexibilidade, mas cobram um preço: você escreve SQL como string, e o TypeScript não tem como saber se aquela coluna existe. O erro só aparece quando o código roda — geralmente em produção, geralmente numa sexta-feira.
O Prisma ataca isso de um jeito diferente. Ele lê o schema do banco, gera um client TypeScript inteiro baseado nisso, e qualquer query que você escrever é validada em tempo de compilação. Errou o nome de uma coluna? O editor te avisa antes do build.
Isso não é mágica. É geração de código a partir de introspecção de schema, e funciona bem justamente porque o PostgreSQL é rígido com tipos — o Prisma aproveita essa rigidez a favor do dev.
Instalando e configurando o Prisma
Parte com um projeto Node novo ou existente, tanto faz:
npm install prisma --save-dev
npm install @prisma/client
npx prisma init --datasource-provider postgresql
O prisma init cria uma pasta prisma/ com um schema.prisma e um .env com a variável DATABASE_URL. Ajusta ela pro seu banco:
DATABASE_URL="postgresql://usuario:senha@localhost:5432/meubanco?schema=public"
⚠️ Atenção: nunca versiona o
.envcom credenciais reais. Parece óbvio, mas já vi repositório público com string de conexão de produção — e o banco levou menos de um dia pra ser varrido por bot.
Com a conexão configurada, o schema.prisma fica assim por padrão:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
Esse arquivo é a fonte de verdade do projeto. Todo o resto — migrations, client gerado, tipos — deriva dele.
Modelando o schema do banco
Aqui é onde a maioria dos tutoriais mostra um exemplo bonito demais. Vamos de algo mais realista: um blog com usuários e posts, com uma relação de verdade.
model User {
id String @id @default(uuid())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id String @id @default(uuid())
title String
content String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
@@index([authorId])
}
Repara no @@index([authorId]). É fácil esquecer índices em foreign keys, e o PostgreSQL não cria um automaticamente pra colunas de relação — só pra chave primária e colunas com @unique. Sem esse índice, qualquer query que filtra posts por autor vai fazer um sequential scan conforme a tabela cresce.
Rodando migrations sem drama
Com o schema definido, você gera a migration:
npx prisma migrate dev --name init
Esse comando faz três coisas: cria o arquivo SQL da migration, aplica no banco de desenvolvimento e regenera o Prisma Client. Cada mudança no schema pede um novo migrate dev com um nome descritivo — isso vira o histórico de mudanças do banco, versionado junto com o código.
Em produção, o comando é outro:
npx prisma migrate deploy
Esse não gera nada novo, só aplica as migrations pendentes. É o que você roda no CI/CD, nunca o migrate dev.
💡 Dica: se o CI quebrou com erro de "drift detected", normalmente é porque alguém alterou o banco direto, fora do fluxo de migrations. O Prisma detecta a divergência entre o schema esperado e o estado real do banco — e ele está certo em reclamar.
Fazendo queries type-safe
Com o client gerado, as queries ficam assim:
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function criarPost(authorId: string, title: string, content: string) {
return prisma.post.create({
data: {
title,
content,
authorId,
},
})
}
❌ O que não fazer — instanciar PrismaClient em todo arquivo:
// em cada rota, cada service, cada arquivo
const prisma = new PrismaClient()
Isso esgota o pool de conexões do PostgreSQL rápido, principalmente em ambientes serverless onde cada invocação pode criar uma instância nova.
✓ O certo é centralizar numa única instância:
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = global as unknown as { prisma: PrismaClient }
export const prisma =
globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}
Esse padrão de singleton evita que o hot-reload do Next.js, por exemplo, crie uma nova conexão a cada mudança de arquivo em dev.
Relações e o problema do N+1
Buscar um post com o autor é direto:
const post = await prisma.post.findUnique({
where: { id: postId },
include: { author: true },
})
Até aqui, tranquilo. O problema aparece quando você busca uma lista e, pra cada item, faz outra query separada — o clássico N+1. Isso apareceu num PR meu em 2021, numa listagem de posts que buscava o autor de cada um num loop for. O revisor só comentou: "isso vira 50 queries pra 50 posts?". Era exatamente isso.
❌ Gerando N+1 sem perceber:
const posts = await prisma.post.findMany()
for (const post of posts) {
const author = await prisma.user.findUnique({ where: { id: post.authorId } })
}
✓ Resolvendo com include numa query só:
const posts = await prisma.post.findMany({
include: { author: true },
})
O Prisma monta um JOIN por trás — ou, dependendo da estratégia de query engine configurada, faz batching inteligente das buscas relacionadas. De qualquer forma, é uma query em vez de N.
Erros comuns com Prisma e PostgreSQL
| Erro comum | Por que acontece | Como evitar |
|---|---|---|
| Timeout de conexão em produção | Múltiplas instâncias de PrismaClient esgotando o pool | Usar singleton, configurar connection_limit na URL |
P2002 (unique constraint) não tratado | Falta de try/catch em torno de create com campo único | Capturar erro e responder com mensagem clara pro usuário |
| Migration falha em produção mas passou local | Dados existentes violam nova constraint (ex: NOT NULL) | Testar migration contra uma cópia do banco de produção antes |
| Query lenta sem índice | Filtro ou relação sem @@index no schema | Rodar EXPLAIN ANALYZE nas queries mais usadas |
Por que isso importa? Porque em produção você não tem segunda chance com uma migration mal testada — reverter schema de banco é bem mais caro que reverter um deploy de código.
FAQ
Prisma funciona bem com PostgreSQL em serverless (Vercel, Lambda)? Funciona, mas exige atenção ao pool de conexões. Serverless cria e destrói instâncias com frequência, então vale usar um pooler como o PgBouncer ou o Prisma Accelerate pra não estourar o limite de conexões do PostgreSQL.
Prisma substitui SQL puro?
Não totalmente. Pra queries complexas — relatórios pesados, agregações específicas — o $queryRaw continua sendo a saída, e o Prisma tipa o retorno se você passar um schema Zod ou uma interface manual.
Vale a pena usar Prisma em projeto pequeno? Sim, principalmente pelo DX: autocomplete, migrations versionadas e menos bug de tipo. O overhead de configuração inicial é baixo comparado ao ganho em manutenção.
Como lidar com transações no Prisma?
Use prisma.$transaction, passando um array de operações ou uma função. Ele garante atomicidade — se uma falhar, todas revertem.
Prisma tem suporte a soft delete nativo?
Não nativamente. O padrão é adicionar um campo deletedAt no model e filtrar manualmente, ou usar client extensions pra automatizar isso nas queries.
Próximos passos
- Configura o singleton do
PrismaClientantes de qualquer outra coisa, mesmo em projeto pequeno. - Adiciona
@@indexem toda foreign key usada em filtro ouinclude. - Roda
npx prisma studiopra inspecionar os dados sem sair do terminal — ajuda muito a debugar relação errada. - Testa migrations contra uma cópia real dos dados antes de rodar
migrate deployem produção. - Se o projeto crescer, avalia um connection pooler antes que o erro de timeout apareça sozinho.