Alternar tema

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

Easton editorial illustration: cache waterfall instrument

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 include em 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 + 1 conexões.
  • Otimização de consultas: ele consulta somente os campos necessários, e select pode 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:

  1. Cria o arquivo prisma/schema.prisma, que contém a configuração principal.
  2. Cria o arquivo .env com a variável de ambiente DATABASE_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:

  1. globalForPrisma adiciona um tipo ao globalThis, para que o TypeScript consiga reconhecê-lo.
  2. export const prisma = globalForPrisma.prisma || new PrismaClient() é a linha principal: se globalThis.prisma já tiver um valor, ele será usado; caso contrário, uma nova instância será criada.
  3. if (process.env.NODE_ENV !== 'production') garante que a instância seja armazenada no globalThis apenas 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 → users e Post → 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 User possui posts Post[], indicando que um usuário tem vários Post.
  • O modelo Post possui author User e authorId Int, indicando que um Post pertence a um User.

A linha @relation(fields: [authorId], references: [id]) define a chave estrangeira:

  • fields: [authorId] representa o campo authorId do modelo atual, Post.
  • references: [id] indica a relação com o campo id do modelo User.

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:

  1. Detecta as alterações no Schema.
  2. Gera o arquivo SQL de migração.
  3. Aplica a migração ao banco de dados.
  4. 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:

  1. Consulte o histórico das migrações:
npx prisma migrate status
  1. 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 select para reduzir os campos, include para evitar N+1 e configure o pool de conexões de forma adequada.
  • Gerenciamento de migrações: use migrate dev em desenvolvimento e migrate deploy em 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 generate antes 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. 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. 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. 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. 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. 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. 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'?
Esse erro é causado pelo vazamento de conexões durante o hot reload do Next.js.

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?
Prisma:
• 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?
Adicione o campo de chave estrangeira ao lado 'muitos' e marque-o com @relation.

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?
include:
• É 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?
Use include para consultar os dados relacionados de uma só vez, em vez de fazer isso dentro de um loop externo. Por exemplo, prisma.user.findMany({ include: { posts: true } }) consulta de uma vez todos os usuários e seus posts, em vez de executar uma consulta de posts para cada usuário.
O Prisma oferece suporte a transações?
Sim. Use prisma.$transaction([...]) para executar várias operações de modo que todas sejam concluídas com sucesso ou todas falhem. Por exemplo: await prisma.$transaction([prisma.user.create(...), prisma.post.create(...)]).
Como implantar o Prisma na Vercel?
Etapas:
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

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog