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



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
// 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

BASH
# 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
100%
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

TS
// 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
💡 Dica: O hot reloading do Next.js cria uma nova instância do PrismaClient toda vez que a página é atualizada. O padrão singleton global reutiliza conexões existentes no ambiente de desenvolvimento, evitando erros de "Too many connections".

▶ Exemplo: Verificando a conexão com o banco de dados

Saída:

TEXT 📖 Somente leitura
Database operation executed successfully.
TS
// 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 })
  }
}
💻 Saída:

JSON
{ "status": "ok", "db": "conectado" }

Saída:

TEXT 📖 Somente leitura
{ "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

100%
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
// 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

BASH
# Iniciar Prisma Studio (GUI no Navegador para Ver/Editar Dados)
npx prisma studio
TEXT 📖 Somente leitura
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
💡 Dica: O Studio suporta edição direta de dados relacionados; quando você edita um User, pode ver seus Posts e Comments.



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

BASH
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

BASH
npx prisma migrate dev --name add_role_enum

SQL Gerado:

SQL
-- 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:

TEXT 📖 Somente leitura
Statement(s) executed successfully.
💡 Dica: 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

TSX
// 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

TS
// 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:

TEXT 📖 Somente leitura
Server action executes and calls revalidatePath() to refresh the page cache.
TSX
// 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:

TEXT 📖 Somente leitura
A form with input fields and submit button.
Visible text: Publicar
TS
// 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

TS
// 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 })
}
TS
// 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

TS
// 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
⚠️ Nota: Em ambientes serverless (Vercel), o número de conexões de banco de dados é limitado. Recomendamos usar Prisma Accelerate (proxy de pool de conexões) ou o pool de conexões do Supabase (?pgbouncer=true).

▶ Exemplo: Configuração do Pool de Conexões do Supabase

ENV
# .env — Pool de Conexões do Supabase
DATABASE_URL="postgresql://postgres:password@db.xxxxx.supabase.co:6543/postgres?pgbouncer=true&connection_limit=5"
TS
// 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:

TEXT 📖 Somente leitura
Database operation executed successfully.


8. Exemplo Completo: Implementação Abrangente de um Sistema de Blog CRUD

TSX
// 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>
  )
}
💡 Dica: Este exemplo demonstra tanto a leitura de dados RSC (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 dev e prisma db push? R: migrate dev gera arquivos SQL de migração rastreáveis (adequado para colaboração em equipe), enquanto db push sincroniza diretamente o schema com o banco de dados (adequado para prototipagem rápida; nenhum histórico é mantido). migrate deploy deve 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. Use prisma.$transaction([...]) ou prisma.$transaction(async (tx) => {...}) quando precisar definir explicitamente uma transação.


📖 Resumo


📝 Exercícios

  1. Exercício Básico (⭐): Inicialize o Prisma em um projeto existente, defina um relacionamento um-para-um entre os modelos User e Profile, execute as migrações, insira um registro e valide no Prisma Studio.

  2. 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.

  3. Desafio (⭐⭐⭐): Use $transaction do 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%