Guia completo de Next.js + Prisma: da configuração à prática (com solução para vazamento de conexões)

O terminal exibiu de novo aquela linha vermelha desesperadora: Error: Can't reach database server at localhost:5432. Ou, para ser mais exato: FATAL: sorry, too many clients already.
O banco de dados estava ativo e a string de conexão estava correta. Então por que não era possível se conectar? Logo depois de iniciar o projeto, tudo funcionava. Bastava alterar o código algumas vezes para o erro aparecer. Reiniciar o servidor de desenvolvimento do Next.js? Não adiantava. Reiniciar o banco? O problema voltava pouco depois.
Mais tarde descobri que esse é um dos problemas mais clássicos no desenvolvimento com Next.js + Prisma: o hot reload causa vazamento de conexões com o banco de dados. Em termos simples, a cada alteração no código o hot reload do Next.js cria uma nova instância do Prisma, mas as conexões antigas não são encerradas automaticamente. No fim, o pool de conexões estoura.
Talvez você também já tenha enfrentado algo parecido: quis adicionar um banco de dados ao projeto Next.js e passou um tempão escolhendo entre Prisma, TypeORM e Drizzle; finalmente optou pelo Prisma, mas encontrou vários erros depois de configurá-lo; não soube como representar relações um para muitos ou muitos para muitos no Schema; ou, como aconteceu comigo, viu o banco de dados parar de responder no meio do desenvolvimento.
Na verdade, a curva de aprendizado do Prisma não é tão íngreme, e a configuração também não é complicada. O importante é conhecer alguns pontos essenciais: como preparar o ambiente, evitar vazamentos de conexões, criar o Schema e escrever as operações CRUD. Este artigo organiza tudo isso para poupar você de alguns tropeços.
Por que escolher o Prisma? Comparação com TypeORM e SQL puro
Há várias opções de banco de dados para Next.js. O TypeORM é uma solução tradicional e estável, o Drizzle é uma alternativa nova e leve, e o SQL puro entrega o máximo de desempenho. Então por que ainda usar o Prisma?
Segurança de tipos de verdade
Quando vi pela primeira vez os tipos TypeScript gerados automaticamente pelo Prisma, fiquei realmente impressionado. Não foi apenas um “parece bom”, mas a sensação de descobrir que aquilo podia funcionar daquele jeito.
Depois de escrever o arquivo de Schema e executar npx prisma generate, o Prisma gera as definições de tipo de todos os modelos. Não são interfaces simples, mas tipos completos com sugestões inteligentes. Por exemplo, ao digitar prisma.user.findUnique({ where: { id: , o VSCode informa automaticamente o tipo de id e quais outros campos podem ser usados em where.
Compare:
- TypeORM: você precisa escrever vários decorators, como
@Entity()e@Column(). As definições de tipo ficam separadas do schema do banco de dados e podem sair de sincronia. - SQL puro: o resultado da consulta é
any; você precisa escrever as interfaces à mão e atualizá-las manualmente sempre que a estrutura da tabela mudar. - Prisma: o Schema é a única fonte da verdade. Os tipos são sincronizados automaticamente; depois de alterar o Schema, basta gerar o cliente novamente.
Não é apenas uma questão de comodidade. Segurança de tipos significa encontrar erros enquanto você escreve o código, e não apenas quando eles explodem em tempo de execução.
Experiência de desenvolvimento: os detalhes fazem a diferença
Alguns recursos do Prisma tornam o trabalho especialmente agradável:
Prisma Studio: uma interface visual de gerenciamento do banco de dados. Execute npx prisma studio para visualizar e editar os dados no navegador. Durante o desenvolvimento, isso economiza bastante tempo porque dispensa instalar o TablePlus ou escrever consultas SQL.
Sistema de migrações: prisma migrate dev gera os arquivos de migração e os aplica, de forma bem mais simples do que no TypeORM. Ao alterar o Schema, o Prisma detecta as mudanças automaticamente e pergunta se você deseja gerar uma migração. Não é necessário escrever scripts SQL de migração à mão nem se preocupar tanto com a ordem das migrações.
Sintaxe de consulta: a forma de escrever consultas no Prisma combina muito bem com os hábitos de quem usa JavaScript. Veja:
const users = await prisma.user.findMany({
where: { email: { contains: '@gmail.com' } },
include: { posts: true },
orderBy: { createdAt: 'desc' }
})
É bastante intuitivo. Não há SQL nem decorators para memorizar, apenas objetos comuns e chamadas de método.
Compare com o QueryBuilder do TypeORM:
const users = await userRepository.createQueryBuilder("user")
.where("user.email LIKE :email", { email: "%@gmail.com%" })
.leftJoinAndSelect("user.posts", "posts")
.orderBy("user.createdAt", "DESC")
.getMany()
As duas versões fazem a mesma coisa, mas a sintaxe do Prisma é mais clara e menos sujeita a erros.
Desempenho e ecossistema
Algumas pessoas dizem: “ORM não tem desempenho ruim? Em produção não deveríamos usar SQL puro?”
Sinceramente, isso é um mal-entendido. O Prisma realmente introduz algum custo de desempenho, mas ele é totalmente aceitável para a maioria dos projetos. Além disso, o Prisma possui várias otimizações:
- Tratamento automático do problema de N+1: ao usar
includeem consultas relacionadas, o Prisma combina as consultas e evita solicitações repetidas. - Gerenciamento do pool de conexões: a configuração padrão já é razoável, com
num_cpus * 2 + 1conexões. - Otimização de consultas: ele consulta somente os campos necessários, e
selectpode reduzir ainda mais a transferência de dados.
Encontrou um gargalo de desempenho de verdade? O Prisma também aceita SQL puro:
const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${userId}`
Assim, você mantém a praticidade do ORM e pode otimizar pontos críticos com SQL puro.
Quanto ao ecossistema, o Prisma tem mais de 38 mil estrelas no GitHub, é recomendado oficialmente pela Vercel e possui documentação muito completa. Em geral, é fácil encontrar soluções para os problemas. A própria documentação do Next.js tem um guia de integração com o Prisma, o que mostra que ele é uma opção consolidada.
Isso não significa que o Prisma seja perfeito. Para a maioria dos projetos full stack com Next.js, porém, as vantagens são muito maiores do que as desvantagens. Segurança de tipos, experiência de desenvolvimento e suporte do ecossistema já são três bons motivos.
Preparação do ambiente: configurando o Prisma do zero
Depois de escolher o Prisma, é hora de configurar o ambiente. Essa etapa é rápida e pode ser concluída em cerca de dez minutos.
Instale as dependências e inicialize o projeto
Supondo que você já tenha um projeto Next.js, execute npx create-next-app@latest primeiro caso ainda não tenha criado um.
Instale o Prisma:
npm install prisma @prisma/client
São dois pacotes:
prisma: ferramenta de CLI usada para inicializar o projeto, gerar migrações e abrir o Studio.@prisma/client: cliente que de fato consulta o banco de dados.
Depois da instalação, inicialize o Prisma:
npx prisma init
Esse comando faz duas coisas:
- Cria o arquivo
prisma/schema.prisma, que contém a configuração principal. - Cria o arquivo
.envcom a variável de ambienteDATABASE_URL.
Configure a conexão com o banco de dados
Abra o arquivo .env. Você verá:
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
Esse é um exemplo de string de conexão do PostgreSQL. O formato é:
postgresql://usuário:senha@host:porta/nome_do_banco?schema=public
Para desenvolvimento local, recomendo o PostgreSQL. Ele tem muitos recursos, possui excelente suporte no Prisma e provavelmente também será usado em produção.
Ainda não instalou o PostgreSQL? O Docker é o caminho mais rápido.
Crie o arquivo docker-compose.yml:
version: '3.8'
services:
postgres:
image: postgres:15
restart: always
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: mydb
ports:
- '5432:5432'
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Inicie o serviço:
docker-compose up -d
O banco de dados estará ativo. Em seguida, altere o .env:
DATABASE_URL="postgresql://myuser:mypassword@localhost:5432/mydb?schema=public"
Importante: não envie o arquivo .env para o Git. Verifique se ele está no .gitignore:
.env
.env.local
Caso contrário, sua senha ficará exposta.
Se você usa MySQL ou SQLite, o formato da string de conexão é um pouco diferente.
MySQL:
DATABASE_URL="mysql://root:password@localhost:3306/mydb"
SQLite (banco local em arquivo, adequado para projetos pequenos):
DATABASE_URL="file:./dev.db"
Gere o Prisma Client
Depois de configurar a conexão, gere o cliente:
npx prisma generate
Esse comando lê o schema.prisma, gera os tipos TypeScript e os métodos de consulta e os salva em node_modules/@prisma/client.
Sempre que alterar o Schema, execute npx prisma generate novamente para atualizar os tipos. Caso contrário, o TypeScript indicará incompatibilidades de tipo.
O ambiente está pronto. O processo completo é: instalar os pacotes → inicializar → configurar o banco de dados → gerar o cliente.
A próxima etapa é resolver o vazamento de conexões durante o hot reload. Esse é um ponto importante e costuma causar muitos problemas.
Como resolver o vazamento de conexões no hot reload
Agora vamos tratar do erro que tira muita gente do sério: FATAL: sorry, too many clients already.
O que realmente está acontecendo?
No modo de desenvolvimento, o Next.js oferece um recurso chamado hot reload (Hot Module Replacement, HMR). Ao salvar uma alteração no código, a página é atualizada automaticamente, sem reiniciar o servidor de forma manual. É muito prático.
O problema aparece quando esse recurso encontra o Prisma.
Cada alteração no código faz o Next.js recarregar os módulos. Se você chamar new PrismaClient() diretamente em uma rota de API ou em um Server Component, cada hot reload criará uma nova instância do Prisma.
Ao ser criada, a nova instância abre novas conexões com o banco de dados. As antigas, porém, não são encerradas automaticamente e continuam abertas.
Por padrão, o PostgreSQL aceita no máximo 100 conexões. Esse valor pode ser alterado, mas normalmente não é necessário. Depois de algumas mudanças no código, o número de conexões cresce: 10, 20, 50, 100. Quando chega ao limite, o banco rejeita novas conexões e retorna too many clients already.
Na primeira vez em que encontrei esse problema, não entendi nada. O banco de dados parecia normal, então por que a conexão parava de funcionar? Reiniciar o servidor de desenvolvimento resolvia por pouco tempo, mas o erro logo voltava. Depois de consultar as Issues do Prisma no GitHub, encontrei muita gente reclamando do mesmo problema na Issue #10247.
Padrão singleton: uma solução simples
A solução é simples: use o padrão singleton para garantir que toda a aplicação crie apenas uma instância de PrismaClient.
Para isso, armazene a instância do Prisma no objeto globalThis. O globalThis é o objeto global do JavaScript e não é apagado durante o hot reload. Depois que a primeira instância é criada, os hot reloads seguintes reutilizam a mesma instância, sem abrir novas conexões.
O código fica assim.
Crie lib/prisma.ts na raiz do projeto:
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma || new PrismaClient({
log: ['query', 'error', 'warn'], // Permite ver todas as consultas durante o desenvolvimento
})
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}
Veja o que cada parte faz:
globalForPrismaadiciona um tipo aoglobalThis, para que o TypeScript consiga reconhecê-lo.export const prisma = globalForPrisma.prisma || new PrismaClient()é a linha principal: seglobalThis.prismajá tiver um valor, ele será usado; caso contrário, uma nova instância será criada.if (process.env.NODE_ENV !== 'production')garante que a instância seja armazenada noglobalThisapenas no ambiente de desenvolvimento. Em produção isso não é necessário, pois cada implantação é nova e não há hot reload.
Daqui em diante, importe prisma desse arquivo em todos os lugares.
Em um Server Component do App Router (Next.js 13+):
// app/users/page.tsx
import { prisma } from '@/lib/prisma'
export default async function UsersPage() {
const users = await prisma.user.findMany()
return (
<div>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
)
}
Em uma API Route:
// app/api/users/route.ts
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET() {
const users = await prisma.user.findMany()
return NextResponse.json(users)
}
Em uma rota de API do Pages Router (Next.js 12 e versões anteriores):
// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { prisma } from '@/lib/prisma'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const users = await prisma.user.findMany()
res.status(200).json(users)
}
O ponto principal é: nunca use new PrismaClient() diretamente em outros arquivos; sempre importe a instância de lib/prisma.ts.
Não é preciso se preocupar em produção
Talvez você esteja se perguntando se também precisa fazer isso em produção.
Não. Em produção, como em uma implantação na Vercel, não há hot reload. Além disso, o código já foi compilado em arquivos estáticos e não muda com frequência.
Por isso, if (process.env.NODE_ENV !== 'production') garante o comportamento esperado: singleton no ambiente de desenvolvimento e criação normal em produção.
Essa é a solução recomendada oficialmente pelo Prisma, que tem uma seção específica sobre o assunto na documentação. Ao segui-la, você praticamente elimina o problema.
Esse detalhe já causou dor de cabeça em muita gente, mas, depois de entender o motivo, a correção se resume a poucas linhas. Configure lib/prisma.ts e você não precisará mais se preocupar com vazamentos de conexões durante o desenvolvimento.
Boas práticas para criar o Schema
Com o ambiente pronto e o vazamento de conexões resolvido, chegou a hora de criar as tabelas do banco. O arquivo de Schema do Prisma é prisma/schema.prisma; todas as estruturas de tabela e relações são definidas nele.
Definição básica de modelos
Veja um exemplo simples com os modelos User e Post de um sistema de blog:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
password String
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Alguns pontos importantes:
@id: chave primária.@default(autoincrement()): ID com incremento automático.@unique: restrição de unicidade; por exemplo, o email não pode se repetir.String?: a interrogação indica um campo opcional, que pode ser null.@default(now()): define automaticamente a data e a hora atuais na criação.@updatedAt: atualiza automaticamente a data e a hora a cada alteração.
Convenções de nomenclatura:
- Use PascalCase nos nomes dos modelos:
User,Post,UserProfile. - Use camelCase nos nomes dos campos:
createdAt,authorId. - O Prisma converte automaticamente os nomes das tabelas do banco para minúsculas e para o plural, como
User→usersePost→posts.
Relações: um para muitos, muitos para muitos e um para um
As relações são o núcleo do Schema e também uma das partes que mais confundem quem está começando. Vamos analisar cada tipo.
Um para muitos (One-to-Many)
Um usuário pode ter vários posts, enquanto cada post pertence a apenas um usuário. Essa é uma relação um para muitos.
Os modelos User e Post do exemplo anterior usam esse tipo de relação:
- O modelo
Userpossuiposts Post[], indicando que um usuário tem vários Post. - O modelo
Postpossuiauthor UsereauthorId Int, indicando que um Post pertence a um User.
A linha @relation(fields: [authorId], references: [id]) define a chave estrangeira:
fields: [authorId]representa o campoauthorIddo modelo atual, Post.references: [id]indica a relação com o campoiddo modeloUser.
Muitos para muitos (Many-to-Many)
Um post pode ter várias tags, e uma tag pode pertencer a vários posts. Essa é uma relação muitos para muitos.
O Prisma oferece duas formas de representá-la: relação implícita e tabela intermediária explícita.
Relação implícita (mais simples, gerenciada automaticamente pelo Prisma):
model Post {
id Int @id @default(autoincrement())
title String
tags Tag[]
}
model Tag {
id Int @id @default(autoincrement())
name String
posts Post[]
}
O Prisma cria automaticamente uma tabela intermediária _PostToTag, sem exigir que você a gerencie. Na consulta, use include: { tags: true } para obter as tags relacionadas.
Tabela intermediária explícita (mais flexível e permite campos adicionais):
model Post {
id Int @id @default(autoincrement())
title String
postTags PostTag[]
}
model Tag {
id Int @id @default(autoincrement())
name String
postTags PostTag[]
}
model PostTag {
id Int @id @default(autoincrement())
postId Int
tagId Int
post Post @relation(fields: [postId], references: [id])
tag Tag @relation(fields: [tagId], references: [id])
createdAt DateTime @default(now())
@@unique([postId, tagId]) // Impede relações duplicadas
}
A vantagem da forma explícita é poder adicionar campos à tabela intermediária PostTag, como createdAt para registrar quando a tag foi associada. Se você não precisa de campos extras, a forma implícita é mais simples.
Um para um (One-to-One)
Um usuário possui um perfil, e cada perfil pertence a apenas um usuário.
model User {
id Int @id @default(autoincrement())
email String @unique
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
bio String?
userId Int @unique
user User @relation(fields: [userId], references: [id])
}
O ponto principal é adicionar @unique a userId, garantindo que um User tenha apenas um Profile.
Técnicas avançadas
Tipos enum (Enum)
Se um campo só pode ter alguns valores fixos, use um enum:
enum Role {
USER
ADMIN
MODERATOR
}
model User {
id Int @id @default(autoincrement())
email String @unique
role Role @default(USER)
}
Índice único composto
Vários campos podem ser únicos em conjunto:
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
slug String
@@unique([authorId, slug]) // O slug não pode se repetir para o mesmo autor
}
Elimine ambiguidades entre relações
Se houver várias relações entre dois modelos, use o parâmetro name:
model User {
id Int @id @default(autoincrement())
writtenPosts Post[] @relation("PostAuthor")
favoritePosts Post[] @relation("PostFavorites")
}
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
author User @relation("PostAuthor", fields: [authorId], references: [id])
favoritedBy User[] @relation("PostFavorites")
}
Sem name, o Prisma retorna um erro porque não consegue identificar qual relação corresponde a cada campo.
Exemplo completo: sistema de blog
Reunindo tudo, este é o Schema completo de um sistema de blog:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Role {
USER
ADMIN
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
password String
role Role @default(USER)
posts Post[]
comments Comment[]
profile Profile?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Profile {
id Int @id @default(autoincrement())
bio String?
avatar String?
userId Int @unique
user User @relation(fields: [userId], references: [id])
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
comments Comment[]
tags Tag[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Comment {
id Int @id @default(autoincrement())
content String
postId Int
post Post @relation(fields: [postId], references: [id])
authorId Int
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}
Com o Schema pronto, execute npx prisma migrate dev --name init para gerar a migração e aplicá-la ao banco de dados. O Prisma cria automaticamente as tabelas, os índices e as restrições de chave estrangeira.
Criar um Schema não é tão difícil. O essencial é entender as relações um para muitos, muitos para muitos e um para um. Com enum, índices únicos e algumas outras técnicas, você já cobre a maioria dos casos.
Operações CRUD na prática
Depois de criar o Schema, é hora de escrever o código que manipula os dados. A API de consulta do Prisma é muito intuitiva e quase sempre dá para entender seu uso só de olhar.
Lembre-se: todas as operações devem importar a instância prisma de lib/prisma.ts. Não crie seu próprio new PrismaClient().
Criar dados (Create)
Crie um registro:
import { prisma } from '@/lib/prisma'
const user = await prisma.user.create({
data: {
email: '[email protected]',
name: 'Alice',
password: 'hashed_password_here'
}
})
Crie vários registros:
const users = await prisma.user.createMany({
data: [
{ email: '[email protected]', name: 'Bob', password: 'pass1' },
{ email: '[email protected]', name: 'Charlie', password: 'pass2' }
],
skipDuplicates: true // Ignora emails que já existem
})
console.log(`${users.count} usuários criados`)
Criação aninhada (crie um Profile junto com o usuário):
const user = await prisma.user.create({
data: {
email: '[email protected]',
name: 'Dave',
password: 'pass',
profile: {
create: {
bio: 'Um desenvolvedor apaixonado por programação'
}
}
},
include: {
profile: true // Inclui profile no resultado
}
})
Consultar dados (Read)
Consulte um registro:
// Consulta por um campo único
const user = await prisma.user.findUnique({
where: { email: '[email protected]' }
})
// Consulta o primeiro registro correspondente
const firstPost = await prisma.post.findFirst({
where: { published: true },
orderBy: { createdAt: 'desc' }
})
Consulte vários registros:
const users = await prisma.user.findMany({
where: {
email: {
contains: '@gmail.com' // O email contém @gmail.com
}
},
orderBy: { createdAt: 'desc' },
take: 10, // Obtém apenas 10 registros
skip: 0 // Não ignora registros (usado na paginação)
})
Consultas relacionadas:
Use include para consultar os dados relacionados:
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: true, // Inclui todos os posts do usuário
profile: true // Inclui o profile do usuário
}
})
Use select para consultar apenas os campos necessários, melhorando o desempenho:
const user = await prisma.user.findUnique({
where: { id: 1 },
select: {
id: true,
email: true,
posts: {
select: {
id: true,
title: true
}
}
}
})
// O resultado contém apenas id, email e posts (somente id e title)
include consulta todos os campos, enquanto select consulta somente os campos especificados. Quando o volume de dados é grande, select pode economizar bastante banda.
Condições de filtro:
O Prisma aceita vários tipos de filtro:
const posts = await prisma.post.findMany({
where: {
OR: [
{ title: { contains: 'Next.js' } },
{ content: { contains: 'Prisma' } }
],
AND: [
{ published: true },
{ authorId: { not: 1 } } // Exclui o autor de ID 1
]
}
})
Operações de filtro comuns:
equals: igual a.not: diferente de.in: está no array (in: [1, 2, 3]).notIn: não está no array.contains: contém (string).startsWith: começa com.endsWith: termina com.gt/gte: maior que/maior ou igual a.lt/lte: menor que/menor ou igual a.
Atualizar dados (Update)
Atualize um registro:
const user = await prisma.user.update({
where: { id: 1 },
data: { name: 'Alice Updated' }
})
Atualize vários registros:
const result = await prisma.user.updateMany({
where: { email: { contains: '@gmail.com' } },
data: { role: 'ADMIN' }
})
console.log(`${result.count} usuários atualizados`)
Upsert (atualiza se existir e cria se não existir):
const user = await prisma.user.upsert({
where: { email: '[email protected]' },
update: { name: 'Alice Updated' },
create: {
email: '[email protected]',
name: 'Alice',
password: 'pass'
}
})
Esse recurso é muito útil porque elimina a necessidade de consultar primeiro e decidir depois entre criar ou atualizar.
Excluir dados (Delete)
Exclua um registro:
const user = await prisma.user.delete({
where: { id: 1 }
})
Exclua vários registros:
const result = await prisma.user.deleteMany({
where: {
createdAt: {
lt: new Date('2023-01-01') // Exclui usuários criados antes de 2023
}
}
})
console.log(`${result.count} usuários excluídos`)
Trabalhar com transações
Às vezes é preciso garantir que várias operações sejam concluídas juntas ou falhem juntas. Em uma transferência, por exemplo, o valor precisa sair da conta A e entrar na conta B; as duas operações devem ser concluídas ao mesmo tempo.
Transação em lote (várias operações independentes):
const [user, post] = await prisma.$transaction([
prisma.user.create({ data: { email: '[email protected]', password: 'pass' } }),
prisma.post.create({ data: { title: 'Test Post', authorId: 1 } })
])
As duas operações são concluídas ou ambas falham e são revertidas.
Transação interativa (as operações dependem umas das outras):
const transferMoney = await prisma.$transaction(async (tx) => {
// Retira o valor da conta A
const accountA = await tx.account.update({
where: { id: 1 },
data: { balance: { decrement: 100 } }
})
if (accountA.balance < 0) {
throw new Error('Saldo insuficiente')
}
// Adiciona o valor à conta B
const accountB = await tx.account.update({
where: { id: 2 },
data: { balance: { increment: 100 } }
})
return { accountA, accountB }
})
Se um erro for lançado no meio do processo, toda a transação será revertida e o saldo das duas contas permanecerá inalterado.
Exemplo completo: API Route do Next.js
Reunindo as operações anteriores, temos uma API CRUD completa:
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
// GET /api/posts - Obtém a lista de posts
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const posts = await prisma.post.findMany({
where: { published: true },
include: {
author: {
select: { id: true, name: true, email: true }
},
tags: true
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit
})
const total = await prisma.post.count({ where: { published: true } })
return NextResponse.json({ posts, total, page, limit })
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch posts' }, { status: 500 })
}
}
// POST /api/posts - Cria um post
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { title, content, authorId, tagIds } = body
const post = await prisma.post.create({
data: {
title,
content,
authorId,
tags: {
connect: tagIds.map((id: number) => ({ id })) // Relaciona tags existentes
}
},
include: { tags: true }
})
return NextResponse.json(post, { status: 201 })
} catch (error) {
return NextResponse.json({ error: 'Failed to create post' }, { status: 500 })
}
}
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
// GET /api/posts/:id - Obtém um post
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const post = await prisma.post.findUnique({
where: { id: parseInt(params.id) },
include: {
author: { select: { id: true, name: true } },
tags: true,
comments: {
include: {
author: { select: { id: true, name: true } }
}
}
}
})
if (!post) {
return NextResponse.json({ error: 'Post not found' }, { status: 404 })
}
return NextResponse.json(post)
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch post' }, { status: 500 })
}
}
// PATCH /api/posts/:id - Atualiza um post
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const body = await request.json()
const post = await prisma.post.update({
where: { id: parseInt(params.id) },
data: body
})
return NextResponse.json(post)
} catch (error) {
return NextResponse.json({ error: 'Failed to update post' }, { status: 500 })
}
}
// DELETE /api/posts/:id - Exclui um post
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
await prisma.post.delete({
where: { id: parseInt(params.id) }
})
return NextResponse.json({ message: 'Post deleted' })
} catch (error) {
return NextResponse.json({ error: 'Failed to delete post' }, { status: 500 })
}
}
Essas são as operações CRUD. Ao dominar create, findUnique, findMany, update e delete, além de filtros, consultas relacionadas e transações, você consegue lidar com a maioria dos cenários.
Técnicas avançadas e problemas comuns
Depois de aprender o CRUD básico, ainda há alguns pontos importantes em projetos reais. Esta seção trata de otimização de desempenho, gerenciamento de migrações e depuração.
Otimização de desempenho
Use select para reduzir os campos consultados
Por padrão, o Prisma consulta todos os campos. Se uma tabela tiver muitos campos ou textos grandes, use select para obter somente o necessário:
// Ruim: consulta todos os campos, incluindo o content, que pode ser grande
const posts = await prisma.post.findMany()
// Bom: consulta apenas os campos necessários
const posts = await prisma.post.findMany({
select: {
id: true,
title: true,
createdAt: true,
author: {
select: { name: true }
}
}
})
Em uma página de listagem, especialmente, você não precisa do conteúdo completo do post; o título e o resumo costumam ser suficientes.
Evite o problema de N+1
N+1 é um problema clássico em ORMs: primeiro você consulta N registros e depois executa outra consulta para os dados relacionados de cada um, totalizando N+1 consultas.
No Prisma, include pode resolver isso automaticamente:
// Ruim: consultas N+1
const users = await prisma.user.findMany()
for (const user of users) {
user.posts = await prisma.post.findMany({ where: { authorId: user.id } })
}
// Bom: tudo em uma consulta
const users = await prisma.user.findMany({
include: { posts: true }
})
O Prisma combina as consultas de forma inteligente, usando JOIN ou consultas em lote para evitar solicitações repetidas ao banco de dados.
Configure o pool de conexões
Por padrão, o tamanho do pool de conexões do Prisma é num_cpus * 2 + 1. Isso é suficiente na maioria dos casos, mas pode exigir ajustes em ambientes serverless, como a Vercel, ou em cenários de alta concorrência.
Adicione o parâmetro à DATABASE_URL:
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?connection_limit=5"
Em ambientes serverless, comece com 1 e aumente gradualmente. Um valor muito alto pode esgotar as conexões do banco de dados.
Se a implantação for feita na Vercel, a recomendação oficial é usar o Prisma Accelerate, que oferece pool de conexões via HTTP e cache global otimizado para ambientes serverless.
Gerenciamento de migrações
Ambiente de desenvolvimento
Depois de alterar o Schema, execute prisma migrate dev:
npx prisma migrate dev --name add-user-role
Esse comando:
- Detecta as alterações no Schema.
- Gera o arquivo SQL de migração.
- Aplica a migração ao banco de dados.
- Gera novamente o Prisma Client.
O parâmetro --name define o nome da migração. Use nomes descritivos, como add-user-role ou create-post-table.
Ambiente de produção
Em produção, nunca use prisma migrate dev, pois há risco de perda de dados. Use prisma migrate deploy:
npx prisma migrate deploy
Esse comando apenas aplica os arquivos de migração existentes; ele não gera novos arquivos.
No fluxo de CI/CD, execute prisma migrate deploy antes da implantação para manter a estrutura do banco de dados sincronizada com o código.
Reverta uma migração
O Prisma não possui um comando de rollback integrado, mas você pode fazer o processo manualmente:
- Consulte o histórico das migrações:
npx prisma migrate status
- Se precisar reverter, escreva manualmente o SQL que desfaz a mudança ou restaure um snapshot do banco de dados.
Em produção, faça backups do banco para poder restaurá-lo rapidamente em caso de problema.
Técnicas de depuração
Ative os logs de consulta
Para ver qual SQL o Prisma está executando, ative os logs:
// lib/prisma.ts
export const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error']
})
Isso é muito útil durante o desenvolvimento, pois mostra a instrução SQL, o tempo de execução e os parâmetros de cada consulta.
Em produção, ative apenas o nível error:
log: ['error']
Assim você evita que o excesso de logs afete o desempenho.
Use o Prisma Studio
O Prisma Studio é uma ferramenta visual para gerenciar o banco de dados:
npx prisma studio
Ele abre no navegador uma interface em que você pode:
- Visualizar todas as tabelas e seus dados.
- Adicionar, editar e excluir registros manualmente.
- Testar relações.
É especialmente prático no desenvolvimento e evita alternar para um cliente SQL.
Diagnóstico de erros comuns
P2002: Unique constraint failed
→ Uma restrição de unicidade foi violada, por exemplo, com um email duplicado. Verifique se o dado já existe.
P2025: Record not found
→ O registro que você tentou atualizar ou excluir não existe. Consulte-o primeiro com findUnique.
P1001: Can't reach database server
→ Falha na conexão com o banco. Verifique se DATABASE_URL está correta no .env e se o banco de dados está ativo.
Cuidados na implantação na Vercel
Ao implantar na Vercel, adicione prisma generate ao script build do package.json:
{
"scripts": {
"build": "prisma generate && next build"
}
}
Assim, cada build gera a versão mais recente do Prisma Client e mantém os tipos sincronizados com o Schema.
Configure DATABASE_URL nas configurações do projeto na Vercel. Não escreva a variável diretamente no código.
Se estiver usando PostgreSQL, Vercel Postgres e Supabase são boas opções. Os dois têm planos gratuitos e configuração simples.
Recapitulando
Os pontos centrais desta parte avançada são:
- Otimização de desempenho: use
selectpara reduzir os campos,includepara evitar N+1 e configure o pool de conexões de forma adequada. - Gerenciamento de migrações: use
migrate devem desenvolvimento emigrate deployem produção. Não confunda os ambientes. - Técnicas de depuração: ative logs, use o Prisma Studio e entenda os códigos de erro mais comuns.
- Cuidados na implantação: execute
prisma generateantes do build e configure corretamente as variáveis de ambiente.
Com isso, o uso do Prisma fica muito mais tranquilo.
Conclusão
Da preparação do ambiente às operações CRUD, do vazamento de conexões à criação do Schema, este artigo apresentou o fluxo completo de Next.js + Prisma.
Vale reforçar os pontos mais importantes:
Vazamento de conexões: crie lib/prisma.ts, use o padrão singleton e importe a instância desse arquivo em todos os lugares. Isso é obrigatório durante o desenvolvimento; caso contrário, mais cedo ou mais tarde as conexões do banco se esgotarão.
Criação do Schema: entenda as relações um para muitos, muitos para muitos e um para um, além do uso de @relation. Mantenha as convenções de nomenclatura consistentes e aproveite os tipos enum.
Operações CRUD: domine os métodos principais create, findMany, update e delete, além da diferença entre include e select. Use $transaction para manter a consistência dos dados.
Desempenho e implantação: use select para reduzir os campos consultados, include para evitar N+1, não misture os comandos de migração entre ambientes e lembre-se de executar prisma generate ao implantar na Vercel.
A curva de aprendizado do Prisma é realmente mais suave do que a de SQL puro, e sua segurança de tipos e experiência de desenvolvimento são excelentes. Ele não é perfeito: existe algum custo de desempenho e consultas complexas podem exigir SQL puro. Para a maioria dos projetos full stack com Next.js, porém, as vantagens são claras.
Agora você pode:
- Criar um projeto simples com Next.js + Prisma e praticar essas operações.
- Consultar a documentação oficial do Prisma para conhecer recursos avançados.
- Experimentar o Prisma Studio e gerenciar o banco de dados em uma interface visual.
- Acompanhar as Discussions do Prisma no GitHub. A comunidade é ativa e costuma ter respostas para os problemas mais comuns.
Se tiver dúvidas, deixe um comentário ou compartilhe os problemas que encontrou usando o Prisma. Sua experiência pode ajudar outras pessoas.
Fluxo completo de configuração de Next.js + Prisma
Etapas completas, da instalação à solução do vazamento de conexões, criação do Schema e operações CRUD
⏱️ Estimated time: 4 hr
- 1
Step 1: Instale e inicialize o Prisma
Instale as dependências:
• npm install prisma @prisma/client
• npx prisma init
A inicialização criará:
• prisma/schema.prisma: arquivo de definição do Schema
• .env: arquivo de variáveis de ambiente (inclui DATABASE_URL)
Configure a conexão com o banco de dados:
• Defina DATABASE_URL no arquivo .env
• Formato: postgresql://user:password@localhost:5432/dbname - 2
Step 2: Resolva o vazamento de conexões
Crie um singleton:
• Crie o arquivo lib/prisma.ts
• No ambiente de desenvolvimento, use globalThis para armazenar a instância em cache
• Em produção, exporte a instância diretamente
Código:
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
Isso evita que o hot reload crie várias conexões. - 3
Step 3: Crie o Schema
Defina os modelos:
• Use a palavra-chave model para definir tabelas
• Use @id para marcar a chave primária
• Use @default para definir valores padrão
• Use @relation para definir relações
Tipos de relação:
• Um para um: @relation(fields, references)
• Um para muitos: um modelo possui @relation e o outro não
• Muitos para muitos: use uma tabela intermediária (tabela @relation)
Gere a migração:
• npx prisma migrate dev --name init - 4
Step 4: Implemente operações CRUD
Crie dados:
• prisma.user.create({ data: { name, email } })
Consulte dados:
• prisma.user.findMany(): consulta vários registros
• prisma.user.findUnique({ where: { id } }): consulta um registro
• prisma.user.findFirst({ where: { ... } }): consulta o primeiro registro
Atualize dados:
• prisma.user.update({ where: { id }, data: { name } })
Exclua dados:
• prisma.user.delete({ where: { id } }) - 5
Step 5: Trabalhe com consultas relacionadas
Use include:
• prisma.user.findMany({ include: { posts: true } })
• Retorna os usuários e todos os seus posts
Use select:
• prisma.user.findMany({ select: { id: true, name: true } })
• Retorna apenas os campos especificados, reduzindo o volume consultado
Evite o problema de N+1:
• Use include para consultar os dados relacionados de uma só vez
• Não consulte dados relacionados dentro de um loop externo - 6
Step 6: Faça a implantação e aplique as migrações
Implantação em produção:
• Defina a variável de ambiente DATABASE_URL
• Execute npx prisma generate para gerar o cliente
• Execute npx prisma migrate deploy para aplicar as migrações
Implantação na Vercel:
• Defina as variáveis de ambiente no Vercel Dashboard
• Adicione prisma generate ao comando de build
• Use prisma migrate deploy para aplicar as migrações
Atenção: não execute migrate dev em produção.
FAQ
Por que aparece o erro 'too many clients already'?
A cada alteração no código, o Next.js cria uma nova instância de PrismaClient, mas as conexões antigas não são encerradas automaticamente. Com o tempo, o pool de conexões se esgota.
A solução é usar o padrão singleton e armazenar a instância de PrismaClient em cache com globalThis no ambiente de desenvolvimento.
Qual é a diferença entre Prisma, TypeORM e Drizzle?
• Oferece a melhor segurança de tipos e experiência de desenvolvimento
• Tem algum custo de desempenho
TypeORM:
• É poderoso e aceita consultas complexas
• Sua configuração é mais complexa
Drizzle:
• É leve e tem bom desempenho
• Sua segurança de tipos não é tão completa quanto a do Prisma
Em projetos Next.js, a facilidade de uso e a segurança de tipos do Prisma são vantagens evidentes.
Como criar uma relação um para muitos?
Por exemplo: um User possui vários Post
• No modelo Post, adicione o campo userId e @relation(fields: [userId], references: [id])
• No modelo User, adicione o campo posts Post[]
Qual é a diferença entre include e select?
• É usado em consultas relacionadas e retorna os dados relacionados
• Aumenta o volume de dados consultados
select:
• É usado para escolher campos e retorna apenas os campos especificados
• Reduz o volume de dados consultados
Os dois podem ser usados juntos: { include: { posts: true }, select: { id: true, name: true } }
Como evitar o problema de consultas N+1?
O Prisma oferece suporte a transações?
Como implantar o Prisma na Vercel?
1) Defina a variável de ambiente DATABASE_URL no Vercel Dashboard
2) Adicione prisma generate ao comando build do package.json
3) Use prisma migrate deploy para aplicar as migrações (não use migrate dev)
Verifique se a conexão com o banco de dados de produção está funcionando.
26 min de leitura · Publicado em: 20 dez 2025 · Atualizado em: 4 set 2026
Guia completo Next.js
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Guia para escolher banco de dados no Next.js: comparação completa entre PostgreSQL, MySQL, MongoDB e serviços em nuvem
Não sabe qual banco de dados escolher para seu projeto Next.js? Este guia compara PostgreSQL, MySQL e MongoDB, analisa Vercel Postgres, Supabase, PlanetScale e MongoDB Atlas e apresenta 3 perguntas para você decidir com rapidez e evitar armadilhas.
Parte 22 de 51
Próximo
Guia de gerenciamento de estado no Next.js: Zustand vs Jotai na prática
Redux é pesado demais e Context tem problemas de desempenho? Este artigo compara Zustand e Jotai no Next.js e apresenta um guia claro de escolha e boas práticas para o App Router.
Parte 24 de 51



Comentários
Entre com GitHub para comentar