Guia completo
Prisma ORM 7: guia completo com PostgreSQL
Entenda schema, Prisma Client, driver adapter, CRUD, relações e migrations no Prisma ORM 7.9.1 com Node 24 e PostgreSQL 18, do modelo ao SQL real.
Prisma ORM 7 é uma camada entre sua aplicação TypeScript e o banco de dados. Você
descreve modelos num schema.prisma, gera um Prisma Client adaptado a esses
modelos e usa métodos como create, findMany e update; no PostgreSQL, o
resultado continua sendo tabela, constraint, índice, transação e SQL.
Ao terminar este guia, você entenderá as peças do Prisma 7.9.1, verá um catálogo
de livros sair do schema e chegar ao PostgreSQL 18.6 e terá uma ordem de estudo
para instalação, modelos, CRUD, relações e migrations. O foco é a arquitetura
atual: ESM, prisma.config.ts, driver adapter obrigatório e Client gerado num
diretório explícito.
Se tabela, chave primária e JOIN ainda são nomes novos, comece pelo
guia de PostgreSQL. O ORM reduz código repetitivo, mas
não remove as decisões do banco. Saber o que existe por baixo é o que permite
usar a ferramenta sem transformar autocomplete em adivinhação.
O tradutor que conhece o catálogo
Imagine uma livraria que recebe pedidos em português, mas o estoque trabalha com fichas e códigos próprios. Um atendente traduz “quero livros publicados acima de cinquenta reais” para a consulta entendida pelo estoque e devolve a resposta num formato previsível. Ele também conhece o catálogo: se alguém pedir uma coluna que não existe, consegue avisar antes de enviar o pedido.
No mapa técnico, sua aplicação é a pessoa no balcão, Prisma Client é o atendente,
o Query Compiler transforma a consulta e o driver pg conversa com PostgreSQL.
O schema.prisma descreve o catálogo que gerou esse atendente. O nome ORM vem
de object-relational mapping: uma camada que relaciona objetos e operações da
aplicação às estruturas relacionais do banco.
O limite da analogia é onde muita gente se confunde. Prisma não é o estoque e não conhece dados que nunca chegaram à execução. O banco ainda garante chave única e chave estrangeira, escolhe plano, bloqueia concorrência e persiste os bytes. Um modelo bem tipado também não prova que um desconto está correto. O ORM organiza a conversa; não substitui SQL, validação ou regra de negócio.
As cinco peças do Prisma 7
Um projeto atual tem cinco partes que cooperam. Separá-las evita copiar um tutorial do Prisma 6 e receber erros aparentemente sem relação.
- Prisma Schema: modelos, campos, relações, datasource e generator.
- Prisma Config: caminho do schema, migrations e URL usada pela CLI.
- Prisma CLI: valida, gera Client e cria ou aplica migrations.
- Prisma Client: código gerado a partir dos modelos, importado pela aplicação.
- Driver adapter: ponte de runtime entre Client e o driver JavaScript do banco.
No Prisma 7, a URL deixou o bloco datasource e vai para prisma.config.ts. O
generator moderno é prisma-client, pede output explícito e gera código no
seu projeto. Para PostgreSQL, a aplicação instala @prisma/adapter-pg e entrega
o adapter ao construtor do Client. O pacote precisa trabalhar como ESM, com
"type": "module" no package.json.
npm install @prisma/client@7.9.1 @prisma/adapter-pg@7.9.1 pg dotenv
npm install --save-dev prisma@7.9.1 typescript tsx @types/node @types/pgEssas mudanças não são detalhe cosmético. O exemplo antigo abaixo não é a base
deste cluster: importar de @prisma/client e chamar new PrismaClient() sem
adapter já não representa o fluxo exigido pelo Prisma 7.
Do modelo à consulta existem duas viagens
A primeira viagem acontece no desenvolvimento. Você altera o schema, cria uma
migration, aplica SQL no banco e gera o Client. A segunda acontece durante a
execução: seu código chama o Client, o Query Compiler prepara a operação e o
adapter envia a consulta pelo driver pg.
Pense nas setas desta forma:
schema.prisma -> prisma migrate dev -> PostgreSQL
|
+------> prisma generate ----> src/generated/prisma
aplicação -> Prisma Client -> @prisma/adapter-pg -> pg -> PostgreSQLNo Prisma 7, migrate dev não executa prisma generate automaticamente. As
duas viagens usam o mesmo schema, mas cumprem tarefas diferentes. Migration
muda a estrutura persistida; generate muda a API tipada disponível para o
TypeScript. Esquecer a segunda etapa deixa o editor enxergando um modelo antigo.
Uma configuração mínima e atual
O schema começa declarando onde o Client será gerado e qual banco será usado:
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
moduleFormat = "esm"
}
datasource db {
provider = "postgresql"
}output é relativo ao arquivo schema.prisma. Se ele mora em prisma/, o
caminho ../src/generated/prisma aponta para dentro de src. O gerador cria
arquivos TypeScript que entram no versionamento ou no processo de build conforme
a política do projeto. O ponto obrigatório é importar desse caminho, não contar
com geração escondida dentro de node_modules.
A configuração da CLI fica em prisma.config.ts:
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: { path: "prisma/migrations" },
datasource: {
url: env("DATABASE_URL"),
},
});dotenv/config carrega o arquivo .env; env interrompe a CLI se a variável
obrigatória não existir. Essa URL serve a comandos como validate e migrate. Na
aplicação, o adapter recebe a conexão de runtime. Em banco serverless pode haver
uma URL direta para migration e outra preparada para o tráfego da aplicação;
não misture as duas por conveniência.
O primeiro modelo vira tabela e API
O catálogo usa Autor e Livro. O modelo abaixo mistura decisões da aplicação com decisões reais do PostgreSQL:
enum StatusLivro {
RASCUNHO
PUBLICADO
ESGOTADO
}
model Livro {
id Int @id @default(autoincrement())
titulo String
slug String @unique
preco Decimal @db.Decimal(10, 2)
estoque Int @default(0)
status StatusLivro @default(RASCUNHO)
autorId Int @map("autor_id")
@@index([autorId])
@@map("livros")
}model Livro gera a base para prisma.livro. @id identifica cada registro;
@unique cria uma regra de unicidade; @default define valores; o tipo nativo
Decimal(10, 2) evita usar ponto flutuante para preço. @map e @@map mantêm
nomes agradáveis no TypeScript e nomes snake_case ou plurais no banco.
Ainda falta a relação com Autor, tratada numa lição própria. Por enquanto,
perceba a dupla função: o schema orienta o SQL de migration e também a API gerada.
Se você acrescentar isbn, precisa migrar o banco e gerar novamente o Client.
Client gerado não é um pacote genérico
Depois de npx prisma generate, a saída real do teste foi:
npx prisma generatePrisma schema loaded from prisma/schema.prisma.
✔ Generated Prisma Client (7.9.1) to ./src/generated/prisma in 18ms
O Client conhece exatamente os modelos, campos, enums e relações daquele schema.
É por isso que o editor completa prisma.livro.findMany e rejeita
prisma.livro.findTudo. Não existe uma API universal baixada pronta: o gerador
produz uma versão alinhada ao seu domínio.
No runtime, configure o adapter uma vez e exporte uma instância compartilhada:
import "dotenv/config";
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "./generated/prisma/client.js";
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error("DATABASE_URL não foi definida");
}
const adapter = new PrismaPg({ connectionString });
export const prisma = new PrismaClient({ adapter });O arquivo termina em .ts, mas o import usa .js porque o projeto segue a
resolução ESM do NodeNext. Ferramentas como tsx resolvem o fonte durante o
desenvolvimento; o JavaScript compilado mantém uma extensão válida para o Node.
Em servidor, reaproveite a instância em vez de criar um pool a cada requisição.
O mapa do cluster acompanha uma aplicação real
As cinco lições seguem a ordem em que as dependências aparecem no projeto. A trilha não começa em métodos de consulta porque ainda não existe Client nem tabela para consultarem.
| Etapa | Resultado verificável | Lição |
|---|---|---|
| 1 | PostgreSQL responde e Client é gerado com adapter | Instalar Prisma 7 com PostgreSQL |
| 2 | Modelos válidos geram tabelas, constraints e tipos | Schema e modelos no Prisma |
| 3 | Create, read, update e delete alteram dados reais | CRUD com Prisma Client |
| 4 | Autor e Livro são consultados e gravados juntos | Relações no Prisma |
| 5 | Mudanças de estrutura ganham SQL versionado | Migrations com Prisma |
Use a sequência da trilha de banco de dados para enxergar SQL e Prisma lado a
lado. O conhecimento não compete: primeiro você entende o
que SELECT, INSERT, chave estrangeira e índice fazem; depois observa como o
Client expressa essas operações.
Quando quiser reunir configuração, schema, migration e CRUD numa entrega única, siga o tutorial API Node com TypeScript, Express, Prisma e Docker. Ele mostra o Prisma como uma camada do sistema, sem esconder o SQL nem o banco.
CRUD continua sendo uma mudança no banco
Com a instância pronta, criar uma autora com dois livros relacionados usa um nested write. A operação é transacional: se um livro violar uma regra, a autora também não fica gravada pela metade.
const autora = await prisma.autor.create({
data: {
nome: "Conceição Evaristo",
email: "conceicao@example.com",
livros: {
create: [
{ titulo: "Olhos d'água", slug: "olhos-dagua", preco: "49.90" },
{ titulo: "Becos da memória", slug: "becos-da-memoria", preco: "59.90" },
],
},
},
include: {
livros: { orderBy: { titulo: "asc" } },
},
});
console.log({
autora: autora.nome,
livros: autora.livros.map((livro) => livro.titulo),
});O Prisma não retorna relações por padrão. include pediu os livros junto da
autora; select seria a escolha para trazer campos específicos. No banco, as
gravações viram INSERTs coordenados por transação e ligados pela chave
estrangeira.
Uma leitura intencional pede somente o que a tela usa:
const livros = await prisma.livro.findMany({
where: { status: "PUBLICADO", preco: { gte: "50.00" } },
orderBy: { titulo: "asc" },
select: {
titulo: true,
preco: true,
autor: { select: { nome: true } },
},
});where, orderBy e select são objetos tipados. O editor conhece quais
operadores pertencem a Decimal e quais campos existem em Autor. A consulta ainda
precisa de índice e desenho adequado para crescer; tipos evitam nomes inválidos,
não garantem plano rápido.
Relações têm campo de navegação e chave real
Num relacionamento um-para-muitos, Autor possui livros Livro[]. Livro possui
o campo de navegação autor Autor e o escalar autorId Int que vira coluna:
model Autor {
id Int @id @default(autoincrement())
nome String
email String @unique
livros Livro[]
@@map("autores")
}
model Livro {
id Int @id @default(autoincrement())
titulo String
autorId Int @map("autor_id")
autor Autor @relation(fields: [autorId], references: [id], onDelete: Restrict)
@@index([autorId])
@@map("livros")
}livros e autor ajudam o Client a navegar, mas não viram duas colunas
misteriosas. A coluna persistida é autor_id, protegida por foreign key. O
onDelete: Restrict impede apagar uma autora que ainda é referenciada. O índice
em autorId é explícito porque consultas e joins por essa coluna serão comuns.
No Prisma 7.9.1 com @prisma/adapter-pg, tentar apagar o registro pai produziu
o erro controlado abaixo:
O detalhe do adapter preservou a mensagem do PostgreSQL: a tabela autores
viola a regra RESTRICT da foreign key livros_autor_id_fkey. A correção depende
do domínio: remover ou transferir livros antes, permitir autor nulo, ou escolher
uma ação referencial diferente de forma consciente.
Migrations são o histórico executável da estrutura
Alterar o schema não muda o banco por telepatia. prisma migrate dev compara o
estado desejado, cria SQL dentro de prisma/migrations e aplica a migration no
ambiente de desenvolvimento. O arquivo vai para o Git e precisa ser revisado
como código.
npx prisma migrate dev --name inicial
npx prisma generateThe following migration(s) have been created and applied from new schema changes:
prisma/migrations/ └─ 20260823013306_inicial/ └─ migration.sql
Your database is now in sync with your schema.
O nome da pasta começa com timestamp e muda em cada execução. O conteúdo SQL é a
prova do que ocorrerá: CREATE TABLE, ALTER TABLE, índices e foreign keys.
Não edite uma migration já aplicada para “arrumar o passado”; crie uma mudança
seguinte. Em produção, use prisma migrate deploy, nunca migrate dev.
Um erro de unicidade mostra onde termina o tipo
O schema declarou email String @unique. TypeScript sabe que email é texto, mas
não consegue saber quais valores já existem no banco. Duas requisições podem
passar pela validação e tentar o mesmo email. A constraint do PostgreSQL decide,
e Prisma transforma o retorno num erro conhecido:
import { Prisma } from "./generated/prisma/client.js";
try {
await prisma.autor.create({
data: { nome: "Outra autora", email: "conceicao@example.com" },
});
} catch (erro) {
if (erro instanceof Prisma.PrismaClientKnownRequestError) {
console.error({ name: erro.name, code: erro.code });
}
}P2002 significa violação de constraint única. Uma API costuma traduzir isso
para conflito, sem devolver stack interna ao cliente. Fazer primeiro um
findUnique pode melhorar a mensagem, mas não substitui a constraint: outra
requisição pode gravar entre a leitura e a escrita.
Tabela de decisão que uso antes de adotar Prisma
Prisma é uma escolha de arquitetura, não um selo obrigatório em toda API Node. Minha decisão começa pela frequência das consultas comuns, necessidade de controle SQL e capacidade da equipe de diagnosticar o banco.
| Situação | Escolha inicial | Motivo e limite |
|---|---|---|
| CRUD e relações comuns em TypeScript | Prisma Client | API gerada reduz nomes inválidos e mantém filtros, retornos e relações tipados. |
| Projeto novo com schema controlado pela aplicação | Prisma Migrate | O schema declarativo e o SQL versionado formam um fluxo único de evolução. |
| Banco legado já existente | prisma db pull antes de migrar |
Introspecção parte da estrutura real; não deixe uma migration inicial tentar recriar tabelas existentes. |
| Relatório SQL complexo ou recurso específico do PostgreSQL | SQL tipado ou raw query localizada | Não force uma consulta ilegível pela API; mantenha parâmetros seguros e revisão do SQL. |
| Equipe sem base de SQL | Estudar SQL junto, não esconder | O ORM não explica índice ausente, lock, cardinalidade ou plano lento. |
| Função serverless | Adapter e ciclo de conexão revisados | Pool, limites da plataforma e URL apropriada importam mais que a sintaxe do Client. |
| Migration de alto risco | SQL inspecionado e rollout em etapas | Campo obrigatório, índice pesado e alteração de tipo precisam considerar dados e locks. |
| Versão RC numa aplicação estável | Permanecer na GA | Prisma 8 ainda é RC nesta revisão; teste separado não é o mesmo que atualização de produção. |
O sinal verde não é “não quero escrever SQL”. É “quero uma API tipada para a maior parte do acesso, e aceito continuar responsável pelo banco”. Quando a equipe não consegue explicar qual tabela e constraint estão por trás de uma query, a abstração já passou do ponto saudável.
Como localizar a camada que realmente falhou
Uma mensagem com a palavra Prisma não significa que o ORM causou o problema.
Antes de mudar código, descubra em qual etapa a falha nasceu. Se prisma validate acusa P1012, o schema está incoerente e nenhuma query chegou ao banco.
Se prisma generate termina, mas o TypeScript não encontra um campo novo, o
import pode apontar para outro output ou o editor pode estar usando Client
antigo. Se a aplicação não abre conexão, confira URL, adapter, rede e processo
PostgreSQL antes de revisar o model.
Erros conhecidos como P2002 e P2025 atravessam o Client porque o banco ou a
operação encontrou um estado esperado: unicidade violada ou registro necessário
ausente. Já o P2039 observado com adapter-pg trouxe dentro de meta a causa do
driver e o código do PostgreSQL. Nesse caso, o número Prisma é a primeira pista,
não toda a investigação. Preserve o erro original nos logs internos e devolva
ao consumidor apenas uma resposta de domínio, sem caminho local nem credencial.
Minha sequência de diagnóstico é curta. Primeiro confirmo versão e config com
npx prisma --version e npx prisma validate. Depois verifico se migration e
banco concordam com npx prisma migrate status. Em seguida reduzo a consulta
para um script que cria uma instância, executa uma operação e desconecta. Só
depois olho plano, índice, pool e concorrência. Essa ordem separa problema de
ferramenta, estrutura, conexão e dado sem trocar cinco coisas ao mesmo tempo.
Em desenvolvimento, logs de query podem mostrar duração e operação, mas não
devem virar vazamento de dados pessoais. Se uma listagem fica lenta, observe o
SQL e rode EXPLAIN (ANALYZE, BUFFERS) num ambiente seguro. select menor reduz
tráfego; ele não compensa um filtro sem índice. Da mesma forma, include torna a
intenção conveniente, mas relações profundas e listas grandes ainda precisam de
paginação e análise de cardinalidade.
Há uma fronteira final: performance de conexão. O driver pg mantém pool e o
adapter usa essa camada. Criar Client dentro de cada handler pode multiplicar
pools; guardar uma instância global sem entender o ambiente serverless também
pode exceder o limite do provedor. A política correta depende do ciclo de vida
da plataforma, mas a pergunta é sempre observável: quantas conexões existem,
quanto tempo uma query espera e quem encerra o processo.
Esse modo de depurar é parte da decisão autoral deste guia. Prisma é mais útil
quando melhora a comunicação entre aplicação e banco sem esconder as medições.
Se a única reação a um erro é adicionar as any, aumentar timeout ou recriar a
base, volte uma camada e produza uma prova menor.
O que Prisma resolve e o que continua com você
Prisma resolve geração de uma API alinhada ao schema, composição de consultas, nested writes, histórico de migration e uma camada de erros conhecidos. Ele também deixa mudanças visíveis no editor: retirar um campo do schema faz usos antigos falharem na checagem depois de gerar o Client.
Continuam com você a validação da entrada HTTP, autorização, regras de negócio,
índices, limites de conexão, observabilidade, backup, restauração e rollout.
data: req.body não fica seguro só porque Prisma rejeita campos que não conhece.
Valide formatos e permita somente propriedades autorizadas antes da query.
Também continue olhando SQL. Ative logs de query de modo controlado em
desenvolvimento, use EXPLAIN para investigação e acompanhe consultas lentas no
PostgreSQL. O artigo de SQL com SELECT, INSERT, UPDATE e DELETE
mostra as operações que o CRUD expressa por baixo.
Missão: prove as duas camadas
Suba um PostgreSQL descartável, configure Prisma 7.9.1 com adapter e modele Autor
e Livro. Crie a migration inicial, abra o SQL e identifique CREATE TABLE,
índice único e foreign key. Depois gere o Client e escreva uma consulta que liste
título, preço e nome da autora.
O critério de sucesso tem seis provas: prisma validate confirma o schema;
prisma migrate status diz que o banco está atualizado; prisma generate mostra
o caminho explícito; o TypeScript rejeita um campo inexistente; a consulta
devolve dados do PostgreSQL; e um email duplicado produz P2002 sem gravar uma
segunda autora.
Guarde o projeto e faça commits separados para schema, migration e aplicação. Esse histórico deixa visível a ideia central: Prisma tem uma camada de desenvolvimento que gera contratos e uma camada de runtime que conversa com o banco. Quando você consegue apontar onde cada decisão vive, siga para a instalação completa do Prisma com PostgreSQL e reproduza o ambiente do início.
Trilha
Banco de dados
SQL, PostgreSQL, MongoDB e Prisma: modelar, consultar e conectar o banco na API que você escreveu.
Perguntas frequentes
Prisma substitui o PostgreSQL?
Preciso saber SQL para usar Prisma?
Prisma 7 ainda usa prisma-client-js?
Prisma 8 já deve ser usado como versão estável?
Migrate dev pode rodar em produção?
O código deste guia foi executado em Node 24.16.0, Prisma ORM 7.9.1, @prisma/adapter-pg 7.9.1, PostgreSQL 18.6 em postgres:18-alpine e Docker 29.5.3, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- npm — versões do Prisma — npmjs.com
- Prisma ORM — documentação oficial — prisma.io
- Prisma Client — geração e output — prisma.io
- Prisma ORM — database drivers e adapters — prisma.io
- Prisma Migrate — visão geral — prisma.io