O que é o Prisma?
O Prisma é um ORM schema-first para TypeScript e Node.js. Você descreve seus dados apenas uma vez no schema.prisma, executa um gerador e obtém um client cujos métodos e tipos de retorno vêm diretamente desse schema. Não há decorators, nem classes de repositório e nem strings SQL escritas à mão para os casos comuns.
A proposta é que o schema do banco de dados se torne um único arquivo declarativo que humanos leem e ferramentas consomem. A partir dele, o Prisma produz um client tipado que você importa no seu código, migrações SQL que você pode revisar e commitar, e uma interface de studio para navegar pelos dados. O Prisma suporta PostgreSQL, MySQL, MariaDB, SQLite, SQL Server, CockroachDB e MongoDB, e o client gerado traduz cada chamada para SQL ou para o protocolo de rede do MongoDB antes de mapear as linhas de volta para objetos simples.
Se você já utilizou um ORM no estilo ActiveRecord, a mudança de mentalidade é que o Prisma é schema-first e client-generated, em vez de class-first e runtime-reflective. Essa única decisão explica a maioria de seus pontos fortes e a maioria de seus custos.
O schema é a fonte da verdade
Tudo começa no schema.prisma. Ele contém três tipos de blocos: um datasource (onde o banco de dados está), um generator (o que emitir) e um model por tabela.
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(uuid())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}
Algumas convenções valem a pena ser internalizadas. Um campo que termina em ? é nullable, enquanto um tipo de lista como Post[] é uma relação em vez de uma coluna. Atributos começam com @ para campos e @@ para blocos. @id marca a chave primária, @unique cria um índice único, @default(...) fornece um valor, e @map e @@map renomeiam a coluna ou tabela subjacente quando ela não corresponde ao seu modelo.
O modelo acima mapeia para uma tabela User com as colunas id, email, name e createdAt, além de uma relação virtual posts que o Prisma resolve com um join ou uma segunda query.
Gerando e utilizando o client
O client do Prisma é código gerado. Após editar o schema, execute:
pnpm prisma generate
Isso lê o schema.prisma e grava um client em node_modules/.prisma/client, ou em uma pasta de sua escolha utilizando o gerador mais recente prisma-client. Em seguida, você o instancia uma vez e o reutiliza:
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const user = await prisma.user.findUnique({
where: { email: "[email protected]" },
});
Como o client é gerado, tanto o prisma.user quanto a estrutura do user são conhecidos pelo type checker. Renomeie um campo no schema, gere novamente e todo uso desatualizado falhará na compilação. Esse loop de feedback é o principal motivo pelo qual as equipes adotam o Prisma.
Em ambientes serverless ou com hot-reloading, evite criar um novo client por requisição ou por reload. Em vez disso, anexe um ao objeto global:
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;
Lendo dados
O Prisma expõe um método para cada formato de leitura. findUnique busca uma única linha por um campo único, findFirst busca uma linha que corresponda a um filtro arbitrário e findMany retorna uma lista.
const post = await prisma.post.findUnique({ where: { id: 42 } });
const latest = await prisma.post.findFirst({
where: { published: true },
orderBy: { createdAt: "desc" },
});
const posts = await prisma.post.findMany({
where: {
published: true,
title: { contains: "prisma", mode: "insensitive" },
authorId: { in: [userId] },
},
orderBy: { createdAt: "desc" },
take: 20,
skip: 0,
});
Os operadores de filtro são nomeados em vez de simbólicos: equals, not, in, notIn, lt, lte, gt, gte, contains, startsWith, endsWith e mode. Combine-os com arrays AND, OR e NOT:
const posts = await prisma.post.findMany({
where: {
OR: [
{ title: { contains: "orm" } },
{ author: { email: { endsWith: "@example.com" } } },
],
NOT: { published: false },
},
});
take e skip implementam a paginação por offset. Para conjuntos de resultados grandes, prefira a paginação por cursor, que pagina a partir da última linha em vez de contar desde o início:
const page = await prisma.post.findMany({
take: 20,
skip: 1,
cursor: { id: lastSeenId },
orderBy: { id: "asc" },
});
findUnique não aceitará um where que não seja único; use findFirst para esses casos. As variantes findUniqueOrThrow e findFirstOrThrow rejeitam a requisição com um erro em vez de retornar null, o que remove a necessidade de uma verificação condicional quando a linha obrigatoriamente deve existir.
Gravando dados
As operações de criação, atualização e exclusão são igualmente tipadas. create, update, upsert e delete operam em uma única linha, enquanto createMany, updateMany e deleteMany operam em conjuntos.
const user = await prisma.user.create({
data: { email: "[email protected]", name: "Ada" },
});
await prisma.user.update({
where: { id: user.id },
data: { name: "Ada Lovelace" },
});
await prisma.user.upsert({
where: { email: "[email protected]" },
update: { name: "Ada Lovelace" },
create: { email: "[email protected]", name: "Ada" },
});
await prisma.user.delete({ where: { id: user.id } });
Para inserções em massa, createMany emite um único INSERT e é drasticamente mais rápido do que fazer um loop sobre create:
await prisma.post.createMany({
data: [
{ title: "Hello", authorId: user.id },
{ title: "World", authorId: user.id },
],
skipDuplicates: true,
});
A desvantagem é que createMany não consegue gravar relações aninhadas; ele serve apenas para linhas simples. updateMany e deleteMany aceitam os mesmos filtros que findMany, portanto, a ausência de um where realmente afeta todas as linhas.
Relações, include e select
As relações são declaradas em ambos os lados. User.posts é uma lista e Post.author é um valor único, vinculados por @relation(fields: [authorId], references: [id]) no lado proprietário.
Por padrão, o Prisma retorna apenas colunas escalares. Para carregar uma relação, você adiciona include:
const user = await prisma.user.findUnique({
where: { id: userId },
include: {
posts: {
where: { published: true },
orderBy: { createdAt: "desc" },
take: 10,
},
},
});
select é a ferramenta mais específica: ela escolhe exatamente quais campos retornar, tanto para o modelo quanto para relações aninhadas.
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
posts: {
select: { title: true },
where: { published: true },
},
_count: { select: { posts: true } },
},
});
Você não pode combinar select e include no mesmo nível, porque select já responde à pergunta sobre o que retornar. Utilize select quando um endpoint tiver um formato de resposta fixo, e include quando você realmente quiser o registro relacionado completo. Retornar linhas inteiras para depois filtrá-las em JavaScript desperdiça largura de banda e memória, portanto, select evita uma regressão comum.
Escritas aninhadas
Um dos recursos mais interessantes do Prisma é a capacidade de gravar um pai e seus filhos em uma única chamada. As operações aninhadas de create, connect, update e delete são todas executadas dentro de uma transação implícita.
const user = await prisma.user.create({
data: {
email: "[email protected]",
posts: {
create: [
{ title: "First post" },
{ title: "Second post", published: true },
],
},
},
include: { posts: true },
});
Para conectar linhas existentes em vez de criá-las, utiliza-se connect, e para desconectar uma relação, utiliza-se disconnect:
await prisma.post.update({
where: { id: postId },
data: {
author: { connect: { email: "[email protected]" } },
},
});
As escritas aninhadas mantêm os dados relacionados consistentes sem que você precise abrir uma transação manualmente, que é exatamente o tipo de controle que um ORM deve assumir.
Migrations em desenvolvimento e produção
O Prisma Migrate transforma a diferença entre o seu schema e o seu banco de dados em arquivos SQL versionados em prisma/migrations.
pnpm prisma migrate dev --name add_posts
pnpm prisma migrate deploy
pnpm prisma migrate status
migrate dev é um comando de desenvolvimento. Ele compara o schema com o banco de dados, escreve uma nova migration, a aplica e regenera o client. Ele também utiliza um shadow database para detectar drift, portanto, a role de desenvolvimento precisa de permissão para criar e excluir bancos de dados. Se um drift for encontrado, ele poderá sugerir o reset do banco de dados, o que deleta os dados.
migrate deploy é o comando de produção. Ele aplica as migrations pendentes e nada mais: sem geração, sem shadow database e sem resets. Execute-o em seu pipeline de deploy antes que o novo código comece a receber tráfego.
Para protótipos descartáveis, prisma db push ignora completamente o histórico de migrations e força o banco de dados a corresponder ao schema:
pnpm prisma db push
db push é rápido e conveniente, mas não deixa rastro de auditoria e pode excluir colunas ou tabelas para ajustar o schema. Nunca o aponte para produção. Outros comandos úteis são prisma migrate reset para excluir, recriar e re-seedar um banco de dados de desenvolvimento, e prisma migrate diff para mostrar o que seria alterado sem aplicar as mudanças.
Seeding
Os dados de seed devem ficar em prisma/seed.ts. Registre-os em package.json para que o Prisma saiba como executá-los:
{
"prisma": {
"seed": "tsx prisma/seed.ts"
}
}
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
await prisma.user.upsert({
where: { email: "[email protected]" },
update: {},
create: {
email: "[email protected]",
name: "Ada",
posts: { create: [{ title: "Welcome" }] },
},
});
}
main()
.then(() => prisma.$disconnect())
.catch(async (error) => {
console.error(error);
await prisma.$disconnect();
process.exit(1);
});
Execute-os com pnpm prisma db seed. Como o seeding é executado após migrate dev e migrate reset, um banco de dados recém-criado nunca estará vazio, o que torna o onboarding e os testes previsíveis.
Transações
O Prisma possui duas APIs de transação. A forma sequencial recebe um array de operações e as executa em ordem:
const [debit, credit] = await prisma.$transaction([
prisma.account.update({
where: { id: 1 },
data: { balance: { decrement: 5000 } },
}),
prisma.account.update({
where: { id: 2 },
data: { balance: { increment: 5000 } },
}),
]);
A forma interativa fornece um cliente de transação e permite que você tome decisões com base nos resultados:
await prisma.$transaction(async (tx) => {
const sender = await tx.account.findUniqueOrThrow({ where: { id: 1 } });
if (sender.balance < 5000) throw new Error("insufficient_funds");
await tx.account.update({
where: { id: 1 },
data: { balance: { decrement: 5000 } },
});
await tx.account.update({
where: { id: 2 },
data: { balance: { increment: 5000 } },
});
});
Use tx para cada query dentro de uma transação interativa; usar o cliente prisma externo ignora a transação. Você pode ajustar o comportamento com maxWait, que controla quanto tempo esperar por uma conexão, e timeout, que limita a duração máxima da transação, além de definir o nível de isolamento com isolationLevel. Mantenha as transações curtas e nunca mantenha uma aberta durante uma chamada HTTP ou interação do usuário.
SQL puro quando você precisar
O query builder cobre a maioria das leituras, mas consultas de relatórios e recursos específicos do banco de dados às vezes exigem SQL. $queryRaw é um tagged template que parametriza os valores para você:
import { Prisma } from "@prisma/client";
const rows = await prisma.$queryRaw<
{ day: Date; revenue: bigint }[]
>(Prisma.sql`
SELECT date_trunc('day', created_at) AS day,
sum(total_cents) AS revenue
FROM orders
WHERE status = 'paid'
GROUP BY 1
ORDER BY 1 DESC
`);
Use $queryRaw para SELECT e $executeRaw para escritas. Ambos aceitam tagged templates, que evitam SQL injection porque os valores interpolados tornam-se parâmetros vinculados (bound parameters). $queryRawUnsafe e $executeRawUnsafe existem para SQL genuinamente dinâmico e devem ser tratados como perigosos: nunca interpole entradas de usuário neles. Prisma.sql e Prisma.join permitem que você componha fragmentos mantendo a parametrização intacta.
Pooling, serverless e Accelerate
Cada instância do PrismaClient possui um pool de conexões. Em um servidor de longa duração, é exatamente isso que você deseja. Em uma plataforma serverless, cada instância de função cria seu próprio cliente e, consequentemente, seu próprio pool; assim, um pico de tráfego pode esgotar as max_connections do banco de dados em segundos.
A primeira alavanca é a connection string. Limite o pool e o tempo de espera:
DATABASE_URL="postgresql://user:pass@host:5432/shop?connection_limit=5&pool_timeout=10"
Em funções onde cada instância processa uma requisição por vez, connection_limit=1 geralmente é a escolha correta. Atrás do PgBouncer, adicione ?pgbouncer=true para que o Prisma pare de usar prepared statements, que o transaction pooling não consegue preservar. Se você não puder alterar os limites de conexão do banco de dados, o Prisma Accelerate atua na frente como um pooler e cache gerenciados; você envolve o cliente com a extensão dele e roteia as queries através de um endpoint global. O antigo Data Proxy resolvia o mesmo problema e foi substituído pelo Accelerate.
Independentemente da sua escolha, a regra é a mesma: reutilize um único cliente por processo e não permita que o número de instâncias da aplicação multiplique as conexões além do que o banco de dados suporta.
A questão do N+1
Um problema de N+1 ocorre quando você busca uma lista e, em seguida, executa uma consulta por linha para carregar uma relação. O Prisma evita a forma clássica disso porque include e select carregam as relações como parte da mesma chamada: para um findMany com uma relação, ele executa uma consulta para os pais e outra para os filhos, e não uma para cada pai.
Você mesmo reintroduz o problema ao criar um loop:
const users = await prisma.user.findMany();
for (const user of users) {
// One query per user: this is the N+1.
const posts = await prisma.post.findMany({ where: { authorId: user.id } });
}
Em vez disso, carregue a relação na consulta original:
const users = await prisma.user.findMany({
include: { posts: true },
});
Para leituras profundamente aninhadas, a estratégia padrão do Prisma executa uma consulta por nível de relação. Quando uma única consulta com join é materialmente mais rápida, a opção relationLoadStrategy: "join" instrui o Prisma a usar um LEFT JOIN. Meça o desempenho antes de alterar, pois as duas estratégias trocam a quantidade de round trips pela duplicação de colunas do pai.
Boas práticas
- Trate o
schema.prismacomo a fonte da verdade e altere-o através de migrations, nunca manualmente. - Instancie o
PrismaClientapenas uma vez por processo e reutilize-o; proteja o hot reload com um global. - Use
selectpara formatos de resposta fixos eincludeapenas quando precisar de registros completos. - Prefira paginação por cursor em vez de offsets grandes em
skip. - Use
createManypara inserts em massa e escritas aninhadas para linhas relacionadas. - Mantenha transações interativas curtas e sempre use o cliente
txdentro delas. - Utilize
$queryRawcom tagged templates, nunca as variantes Unsafe, quando precisar de SQL. - Configure o
connection_limite, em ambientes serverless, use um pooler como Accelerate ou PgBouncer. - Faça commit das pastas de migration e execute
migrate deployno CI/CD em vez dedb push.
Erros comuns
- Executar
prisma db pushcontra a produção e perder colunas. - Criar um novo
PrismaClientpor requisição e esgotar as conexões. - Esquecer o
prisma generateapós uma alteração de schema e depois se perguntar por que os tipos estão desatualizados. - Buscar linhas completas com
includee filtrar os campos no JavaScript. - Usar o client externo dentro de uma transação interativa, o que sai silenciosamente da transação.
- Ignorar o
wherenoupdateManyoudeleteManye atualizar todas as linhas. - Assumir que
findUniqueaceita qualquer filtro; ele requer um campo único. - Interpolar entrada de usuário em
$queryRawUnsafe. - Manter uma transação aberta durante a espera de uma chamada de API externa.
Próximos passos
O Prisma é uma das formas de interagir com um banco de dados relacional usando TypeScript. Compare-o com o Drizzle, que mantém o SQL visível e dispensa o cliente gerado, e com o TypeORM, a abordagem baseada em classes e decorators que surgiu antes de ambos. Por trás de cada um deles está o próprio banco de dados, portanto, o guia de PostgreSQL oferece um aprofundamento que vale a pena, independentemente do ORM. Se você ainda está escolhendo um runtime para o servidor que hospedará tudo isso, comece pelo Node.js.