“`html
Prisma ORM • Arquitetura • Boas decisões no dia a dia
Dominando a arquitetura de Prisma ORM
Se você quer consistência, performance previsível e uma base que aguenta crescimento, a arquitetura do Prisma importa
mais do que “qual modelo escrever no schema.prisma”. Vou organizar as decisões certas para você aplicar
em projetos reais — da camada de acesso a dados até o controle de transações e erros.
Centralize o cliente e desacople a regra de negócio do banco
O erro mais comum é chamar o Prisma diretamente em controllers/rotas, misturando regra de negócio com detalhes de persistência.
O resultado é um código difícil de testar, com queries duplicadas e efeitos colaterais escondidos.
Minha abordagem
- PrismaClient único (inicialização controlada)
- Camada de repositório/DAO com operações de banco
- Serviços com lógica de negócio e orquestração
- Transações quando necessário, sem “vazamento” para a UI
Benefícios práticos
- Queries reutilizáveis e padronizadas
- Menos refactor quando o schema evolui
- Testes mais fáceis (mockar a camada de repositório)
- Controle melhor de performance (selects, includes, ordenação)
Em resumo: o Prisma vira um “motor de persistência”, não o centro do seu domínio.
Use constraints do banco e decisões explícitas no schema
A arquitetura não é só código. É também como você modela relações, chaves e índices.
O Prisma permite que você expresse isso de forma declarativa no schema.prisma, mas a escolha do desenho
determina o quanto você vai sofrer depois.
-
Relacione com clareza: defina
@relatione identifique cardinalidade (1:1, 1:N, N:N).
Se a relação é obrigatória, reflita isso com campos não-nulos quando aplicável. -
Imponha unicidade com índices: quando existir “uma única regra” (ex.: email por usuário),
não deixe isso só para validação no app — use@uniqueou índices equivalentes. -
Evite cascatas acidentais: cascata pode simplificar ou arruinar dados, dependendo do caso.
Prefira decisões explícitas paraonDelete/onUpdate. -
Considere colunas de auditoria:
createdAt,updatedAt,
responsáveis, etc. Isso impacta queries e debugging.
Quando você alinha as invariantes do domínio com constraints do banco, a consistência melhora e os “bugs fantasma”
diminuem.
Quando usar $transaction (e quando não)
Transações não são um “curinga”. Elas são uma ferramenta para garantir atomicidade quando múltiplas operações precisam
obedecer invariantes.
Eu uso transação quando existe dependência real entre as etapas.
Use transações quando…
- Você precisa garantir que tudo acontece ou nada acontece
- Há atualização em múltiplas tabelas com dependência (ex.: criar pedido + itens)
- Existe validação baseada em estado atual (ex.: saldo, limite, estoque)
Evite transações desnecessárias quando…
- As operações são independentes
- A transação aumenta lock time e concorrência (latência sob carga)
- Você não precisa de atomicidade (ex.: logs assíncronos)
Um ponto importante: dentro de uma transação, todas as operações devem usar o mesmo “contexto” para permanecerem consistentes.
Trate erros com intenção e controle a forma da query
Com Prisma, a performance costuma ser boa — mas você precisa controlar o formato do acesso:
select vs include, paginação, ordenação, joins implícitos e tamanho de payload.
-
Prefira
selectquando você só precisa de alguns campos.
includepuxa relações e pode virar carga desnecessária. -
Paginação consistente: use paginação com
orderBydeterminístico e critérios estáveis
(evite paginação “solta” em listas que mudam). - Time de respostas sob controle: reduza N+1 e evite “loops com queries por item”.
-
Erros com contexto: capture falhas previsíveis e converta para mensagens semânticas
(ex.: violação de unicidade, registro não encontrado, tentativa de estado inválido).
Arquitetura boa também é: responder de forma consistente, com status e mensagens corretas,
sem expor detalhes do banco ao consumidor da API.
Exemplo prático: repositório + transação com criação atômica
A ideia aqui é simples: a rota chama um serviço, o serviço orquestra o caso de uso e o repositório encapsula
as operações no banco. A transação fica no lugar certo — na orquestração do serviço.
// prismaClient.ts
import { PrismaClient } from "@prisma/client";
declare global {
// evite múltiplas instâncias em dev
// eslint-disable-next-line no-var
var __prisma: PrismaClient | undefined;
}
export const prisma =
global.__prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") {
global.__prisma = prisma;
}
// repositories/orderRepository.ts
import { prisma } from "../prismaClient";
export const orderRepository = {
async createOrderWithItems(args: {
userId: string;
items: Array<{ productId: string; quantity: number }>;
totalCents: number;
}) {
return prisma.order.create({
data: {
userId: args.userId,
totalCents: args.totalCents,
items: {
create: args.items.map((i) => ({
productId: i.productId,
quantity: i.quantity,
})),
},
},
});
},
async adjustStockForItems(args: {
items: Array<{ productId: string; quantity: number }>;
}) {
// Exemplo: ajuste simples; em produção, vale modelar estratégia
// de concorrência e integridade conforme seu domínio.
for (const item of args.items) {
await prisma.product.update({
where: { id: item.productId },
data: { stock: { decrement: item.quantity } },
});
}
},
};
// services/createOrderService.ts
import { prisma } from "../prismaClient";
import { orderRepository } from "../repositories/orderRepository";
export async function createOrderService(input: {
userId: string;
items: Array<{ productId: string; quantity: number }>;
totalCents: number;
}) {
// Transação: criação do pedido + consistência de estoque
return prisma.$transaction(async (tx) => {
// Repare: aqui você pode usar tx em vez de prisma para garantir
// que tudo roda no mesmo contexto transacional.
// Para manter o exemplo enxuto, vou mostrar a estrutura do fluxo:
// 1) Ajustar estoque
for (const item of input.items) {
await tx.product.update({
where: { id: item.productId },
data: { stock: { decrement: item.quantity } },
});
}
// 2) Criar pedido e itens
const order = await tx.order.create({
data: {
userId: input.userId,
totalCents: input.totalCents,
items: {
create: input.items.map((i) => ({
productId: i.productId,
quantity: i.quantity,
})),
},
},
include: {
items: true,
},
});
return order;
});
}
Observação: se você tem muitos itens, evite loops com updates em escala sem estratégia.
Nesses casos, você pode otimizar com operações em lote (conforme seu banco e modelo).
Próximo passo
Se você gostou do foco em arquitetura (camadas, transações, previsibilidade), então vai curtir os próximos posts:
continue construindo um backend sólido, com decisões consistentes e manutenção fácil.
“`
Sou Apaixonado pela programação e estou trilhando o caminho de ter cada diz mais conhecimento e trazer toda minha experiência vinda do Design para a programação resultando em layouts incríveis e idéias inovadoras! Conecte-se Comigo!