Next.js: Integração com Banco de Dados: Prisma
Última atualização: 2026-08-26
Prisma é o ORM mais popular no ecossistema Node.js — ele permite que você escreva modelos de dados em TypeScript e gera automaticamente clientes de banco de dados type-safe.
1. O Que Você Vai Aprender
- Instalação, Inicialização e Configuração da Estrutura do Projeto para Prisma ORM
- Modelagem de Dados com Schema (Relacionamentos User / Post / Comment)
- Migração de Banco de Dados (
prisma migrate dev) e Visualização com Prisma Studio - Leitura de dados no Server Component; escrita de dados na Server Action
- Operações CRUD em Route Handlers
- Gerenciamento de Pool de Conexões do Prisma e Compatibilidade com Next.js
2. Uma História Real de um Engenheiro Full-Stack
(1) Ponto de Dor: Usar apenas SQL causa uma queda brusca na eficiência de desenvolvimento
Bob é o Líder Técnico da equipe TaskFlow. A equipe usa SQL nativo para trabalhar com PostgreSQL:
"Você precisa escrever 20 linhas de código SQL template para cada endpoint de API. Com
JOIN, é fácil perder campos nas consultas, e você precisa manter manualmente scripts de migração ao alterar a estrutura da tabela. A parte mais frustrante é que os tipos TypeScript não estão sincronizados com os campos do banco de dados — você só percebe que escreveu os nomes das colunas errados em tempo de execução."
| Problema | Tempo Gasto Por Semana | Impacto |
|---|---|---|
| Templates SQL Manuais | 8h | Trabalho Repetitivo |
| Depurar incompatibilidade de tipos | 4h | Erro em tempo de execução |
| Script de migração manual | 3h | Propenso a descuidos |
| Documentação desatualizada | 2h | Novatos demoram para se adaptar |
(2) A Solução do Prisma ORM
Defina um modelo usando Prisma Schema → Gere automaticamente tipos TypeScript → CRUD type-safe.
// prisma/schema.prisma — Modelo de Dados como Documentação
model User {
id String @id @default(cuid())
name String
email String @unique
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
}
(3) Ganhos
| Dimensão | SQL Manual | Prisma |
|---|---|---|
| Volume de código por operação CRUD de API | 25 linhas | 3 linhas |
| Segurança de Tipos | ❌ Definido Manualmente | ✅ Gerado Automaticamente |
| Gerenciamento de Migração | Arquivos SQL Manuais | prisma migrate dev |
| Experiência de Desenvolvimento | Alternando IDEs/Clientes BD | Integrado no Prisma Studio |
| Sincronização de Documentação | Plug-ins | Schema como Documentação |
3. Instalação do Prisma e Inicialização do Projeto
(1) Processo de Instalação
# 1. Instalar Prisma CLI e o cliente
npm install prisma @prisma/client --save-dev
# Ou tudo de uma vez
npx prisma init --datasource-provider postgresql
graph LR
A[prisma init] --> B[Gerar prisma/schema.prisma]
A --> C[Gerar .env DATABASE_URL]
B --> D[Definir o Modelo de Dados]
D --> E[prisma migrate dev]
E --> F[Gerar Prisma Client]
F --> G[Importar e Usar]
style A fill:#cce5ff
style D fill:#d4edda
style F fill:#fff3cd
| Arquivo Gerado | Finalidade |
|---|---|
prisma/schema.prisma |
Definição do Modelo de Dados |
.env |
String de conexão do banco de dados (DATABASE_URL) |
(2) Inicializar o Singleton do Cliente
// lib/prisma.ts — Singleton Global (Evita a criação de múltiplas conexões durante hot reload)
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
▶ Exemplo: Verificando a conexão com o banco de dados
Saída:
Database operation executed successfully.
// app/api/health/route.ts
import { prisma } from '@/lib/prisma'
import { NextResponse } from 'next/server'
export async function GET() {
try {
await prisma.$connect()
return NextResponse.json({ status: 'ok', db: 'conectado' })
} catch (e) {
return NextResponse.json({ status: 'error', message: (e as Error).message }, { status: 500 })
}
}
{ "status": "ok", "db": "conectado" }
Saída:
{ "status": "ok", "db": "conectado" } ← JSON com duas chaves: status ("ok") e db ("conectado")
4. Modelagem de Dados com Schema
(1) Tipos de Relacionamento entre Modelos
graph TB
User -->|Um-para-muitos| Post
User -->|Um-para-muitos| Comment
Post -->|Um-para-muitos| Comment
Post -->|Muitos-para-muitos| Tag
subgraph User
U1[id String @id @default(cuid())]
U2[name String]
U3[email String @unique]
end
subgraph Post
P1[id String @id @default(cuid())]
P2[title String]
P3[content String?]
end
subgraph Comment
C1[id String @id @default(cuid())]
C2[body String]
end
subgraph Tag
T1[id String @id @default(cuid())]
T2[name String @unique]
end
style User fill:#d4edda
style Post fill:#cce5ff
style Comment fill:#f8d7da
style Tag fill:#fff3cd
| Tipo de Relacionamento | Sintaxe Prisma | Implementação no Banco de Dados |
|---|---|---|
| Um-para-um | User Profile |
Chave estrangeira + unique |
| Um-para-muitos | User Post[] |
Chave estrangeira |
| Muitos-para-muitos | Post Tag[] (implícito) |
Tabela intermediária _PostToTag |
| Auto-referência | Category parentCategory |
Chave estrangeira auto-referenciada |
(2) Esquema Completo do TaskFlow
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Role {
ADMIN
EDITOR
VIEWER
}
model User {
id String @id @default(cuid())
name String
email String @unique
role Role @default(VIEWER)
password String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String
tags Tag[]
comments Comment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
}
model Comment {
id String @id @default(cuid())
body String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
postId String
author String
createdAt DateTime @default(now())
@@index([postId])
}
model Tag {
id String @id @default(cuid())
name String @unique
posts Post[]
@@index([name])
}
▶ Exemplo: Operações de Visualização no Prisma Studio
# Iniciar Prisma Studio (GUI no Navegador para Ver/Editar Dados)
npx prisma studio
1. Execute `npx prisma studio`
2. Abra no navegador http://localhost:5555
3. Selecione as tabelas User / Post / Comment / Tag à esquerda
4. Clique em "Add Record" para Adicionar Dados de Teste
5. Clique em "Save Changes" para Persistir
5. Migração de Banco de Dados
(1) Fluxo de Trabalho de Migração
| Etapa | Comando | Função |
|---|---|---|
| Modificar Schema | Editar schema.prisma |
Adicionar ou Remover Modelos/Campos |
| Criar Migração | npx prisma migrate dev --name add_user_role |
Gerar Arquivo SQL de Migração |
| Aplicar Migração | Automático | Atualizar Estrutura do Banco de Dados |
| Resetar Banco de Dados | npx prisma migrate reset |
Limpar Dados + Re-migrar |
| Gerar Cliente | npx prisma generate |
Atualizar Tipos TypeScript |
(2) Estrutura de Arquivos de Migração
prisma/migrations/
├── 20260706000001_init/
│ └── migration.sql # Criando a Tabela Inicial
├── 20260706000002_add_user_role/
│ └── migration.sql # ALTER TABLE Adicionar coluna role
└── migration_lock.toml # Bloqueio do Provider de Banco de Dados
▶ Exemplo: Adicionar Migração de Role
npx prisma migrate dev --name add_role_enum
SQL Gerado:
-- prisma/migrations/20260706000002_add_role_enum/migration.sql
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('ADMIN', 'EDITOR', 'VIEWER');
-- AlterTable
ALTER TABLE "User" ADD COLUMN "role" "Role" NOT NULL DEFAULT 'VIEWER';
Saída:
Statement(s) executed successfully.
prisma migrate dev detecta automaticamente alterações no schema e gera as instruções SQL correspondentes. Não há necessidade de escrever manualmente instruções ALTER TABLE.
6. Operações CRUD na Prática
(1) Server Component Lê Dados
// app/posts/page.tsx — RSC consulta o banco de dados diretamente
import { prisma } from '@/lib/prisma'
export default async function PostsPage() {
const posts = await prisma.post.findMany({
where: { published: true },
include: { author: { select: { name: true } }, tags: true },
orderBy: { createdAt: 'desc' },
take: 20
})
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>Autor: {post.author.name}</p>
<p>{post.tags.map((t) => t.name).join(', ')}</p>
</article>
))}
</div>
)
}
(2) Server Action: Escrever Dados
// app/actions/posts.ts — Server Action CRUD
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(data: { title: string; content?: string; authorId: string }) {
const post = await prisma.post.create({ data })
revalidatePath('/posts')
redirect(`/posts/${post.id}`)
}
export async function deletePost(id: string) {
await prisma.post.delete({ where: { id } })
revalidatePath('/posts')
}
▶ Exemplo: Criando um Artigo Usando um Formulário e uma Server Action
Saída:
Server action executes and calls revalidatePath() to refresh the page cache.
// app/posts/new/page.tsx
import { createPost } from '@/app/actions/posts'
import { auth } from '@/auth'
export default async function NewPostPage() {
const session = await auth()
if (!session?.user) return <p>Por favor, faça login primeiro</p>
return (
<form action={createPost}>
<input name="title" placeholder="Título do Artigo" required />
<textarea name="content" placeholder="Conteúdo do Artigo" rows={10} />
<input type="hidden" name="authorId" value={session.user.id} />
<button type="submit">Publicar</button>
</form>
)
}
Saída:
A form with input fields and submit button.
Visible text: Publicar
// Correção Server Action: Receber FormData
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
const authorId = formData.get('authorId') as string
if (!title || !authorId) throw new Error('Campos obrigatórios estão faltando')
const post = await prisma.post.create({
data: { title, content, authorId }
})
revalidatePath('/posts')
redirect(`/posts/${post.id}`)
}
(3) Route Handler: CRUD Completo
// app/api/posts/route.ts
import { prisma } from '@/lib/prisma'
import { NextResponse } from 'next/server'
export async function GET() {
const posts = await prisma.post.findMany({
include: { author: true, comments: true },
orderBy: { createdAt: 'desc' }
})
return NextResponse.json(posts)
}
export async function POST(req: Request) {
const body = await req.json()
const post = await prisma.post.create({ data: body })
return NextResponse.json(post, { status: 201 })
}
// app/api/posts/[id]/route.ts
import { prisma } from '@/lib/prisma'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const post = await prisma.post.findUnique({
where: { id },
include: { comments: true, tags: true }
})
if (!post) return NextResponse.json({ error: 'não encontrado' }, { status: 404 })
return NextResponse.json(post)
}
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const body = await req.json()
const post = await prisma.post.update({ where: { id }, data: body })
return NextResponse.json(post)
}
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
await prisma.post.delete({ where: { id } })
return NextResponse.json({ deleted: true })
}
7. Gerenciamento de Pool de Conexões
(1) Configuração do Pool de Conexões
| Opção de Configuração | Valor Padrão | Recomendação para Produção | Descrição |
|---|---|---|---|
connection_limit |
10 | 20–50 | Máximo de conexões simultâneas |
pool_timeout |
10s | 30s | Tempo limite de conexão |
idle_timeout |
10s | 30s | Tempo de retenção de conexão ociosa |
(2) Configurar parâmetros do pool de conexões
// lib/prisma.ts — Pool de Conexões de Nível de Produção
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'warn', 'error'] : ['error'],
datasources: {
db: {
url: process.env.DATABASE_URL + '?connection_limit=20&pool_timeout=30&idle_timeout=30'
}
}
})
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
?pgbouncer=true).
▶ Exemplo: Configuração do Pool de Conexões do Supabase
# .env — Pool de Conexões do Supabase
DATABASE_URL="postgresql://postgres:password@db.xxxxx.supabase.co:6543/postgres?pgbouncer=true&connection_limit=5"
// Otimização Serverless: Reutilizar a instância para cada requisição
import { PrismaClient } from '@prisma/client'
let prisma: PrismaClient
export function getPrisma() {
if (!prisma) {
prisma = new PrismaClient({
datasources: { db: { url: process.env.DATABASE_URL } }
})
}
return prisma
}
Saída:
Database operation executed successfully.
8. Exemplo Completo: Implementação Abrangente de um Sistema de Blog CRUD
// app/posts/[id]/page.tsx — Detalhes do Artigo + Funcionalidade de Comentários
import { prisma } from '@/lib/prisma'
import { notFound } from 'next/navigation'
import { auth } from '@/auth'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
async function addComment(formData: FormData) {
'use server'
const session = await auth()
if (!session?.user) throw new Error('Por favor, faça login primeiro')
const body = formData.get('body') as string
const postId = formData.get('postId') as string
if (!body || !postId) throw new Error('Campo ausente')
await prisma.comment.create({
data: { body, postId, author: session.user.name! }
})
revalidatePath(`/posts/${postId}`)
}
export default async function PostDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const post = await prisma.post.findUnique({
where: { id },
include: {
author: { select: { name: true } },
tags: true,
comments: { orderBy: { createdAt: 'desc' } }
}
})
if (!post) notFound()
return (
<div>
<h1>{post.title}</h1>
<p>Autor: {post.author.name}</p>
<div>{post.content}</div>
<p>Tags: {post.tags.map((t) => t.name).join(', ')}</p>
<hr />
<h2>Comentários ({post.comments.length})</h2>
{post.comments.map((c) => (
<div key={c.id}>
<strong>{c.author}</strong>: {c.body}
</div>
))}
<form action={addComment}>
<input type="hidden" name="postId" value={post.id} />
<textarea name="body" placeholder="Escreva um Comentário..." rows={3} required />
<button type="submit">Enviar</button>
</form>
</div>
)
}
prisma.post.findUnique) quanto a escrita via Server Action (addComment) em uma única página, usando uma configuração full-stack Next.js + Prisma.
❓ Perguntas Frequentes
P: Qual devo escolher, Prisma ou Drizzle ORM? R: Prisma é mais amigável para iniciantes (schema declarativo + visualização Studio + migrações automáticas), enquanto Drizzle é mais próximo da sintaxe SQL e oferece desempenho ligeiramente melhor. Este tutorial usa Prisma porque ele tem o maior ecossistema (43k⭐) e documentação abrangente.
P: Por que usar o padrão singleton global para criar um PrismaClient? R: No modo de desenvolvimento do Next.js, o hot reloading frequentemente cria novas instâncias, o que pode causar um aumento explosivo no número de conexões de banco de dados. O singleton global armazena a instância em cache no
globalThis, garantindo que apenas um PrismaClient seja criado.
P: Qual é a diferença entre
prisma migrate deveprisma db push? R:migrate devgera arquivos SQL de migração rastreáveis (adequado para colaboração em equipe), enquantodb pushsincroniza diretamente o schema com o banco de dados (adequado para prototipagem rápida; nenhum histórico é mantido).migrate deploydeve ser usado em ambientes de produção.
P: Consultar o banco de dados diretamente do Server Component causará problemas de desempenho? R: Não. Como o RSC é executado no servidor, consultar o banco de dados diretamente elimina uma viagem de ida e volta HTTP em comparação com passar por uma rota de API. Isso funciona ainda melhor quando combinado com o cache de dados do Next.js (cache automático de
fetch).
P: Como garantir que as operações de banco de dados sejam transacionais? R: Prisma suporta escritas aninhadas (
create: { post: { create: {...} } }), que automaticamente envolvem transações. Useprisma.$transaction([...])ouprisma.$transaction(async (tx) => {...})quando precisar definir explicitamente uma transação.
📖 Resumo
- Prisma ORM usa Schema para definir modelos declarativamente e gera automaticamente um cliente TypeScript type-safe
- Três modelos de relacionamento: um-para-um, um-para-muitos (chave estrangeira) e muitos-para-muitos (tabela intermediária implícita)
- Fluxo de Trabalho de Migração: Modificar Schema →
migrate dev→ Gerar SQL Automaticamente → Atualizar Cliente - Prisma Studio fornece uma GUI baseada em navegador para visualizar e editar dados diretamente
- Operações CRUD podem ser usadas flexivelmente em RSC (leitura), Server Action (escrita) e Route Handler (API)
- O padrão singleton global previne vazamentos de conexão no ambiente de desenvolvimento; parâmetros de pool de conexões devem ser adicionados para ambientes serverless
📝 Exercícios
-
Exercício Básico (⭐): Inicialize o Prisma em um projeto existente, defina um relacionamento um-para-um entre os modelos
UsereProfile, execute as migrações, insira um registro e valide no Prisma Studio. -
Exercício Avançado (⭐⭐): Implemente uma rota de API CRUD completa para o sistema de artigos — GET (listar + paginação), POST (criar), PATCH (atualizar) e DELETE (excluir) — todas usando operações do Prisma.
-
Desafio (⭐⭐⭐): Use
$transactiondo Prisma para implementar uma funcionalidade que permita criar um artigo e associá-lo a tags ao mesmo tempo. Se uma tag não existir, crie a tag primeiro, depois estabeleça um relacionamento muitos-para-muitos entre o artigo e a tag.