Como Usar o Prisma ORM com TypeScript e PostgreSQL
← Voltar para Codeshort

Como Usar o Prisma ORM com TypeScript e PostgreSQL

Configure o Prisma do zero com PostgreSQL, evite os erros de migration que quebram CI e escreva queries realmente type-safe.

DC
Dev Code Software
31 de julho de 2026·6 min de leitura

Você já perdeu uma tarde inteira debugando um erro de tipo que só aparecia em runtime, numa query que "devia" estar certa? Se a resposta é sim, você vai gostar do que o Prisma resolve.

O problema que o Prisma resolve

Query builders tradicionais te dão flexibilidade, mas cobram um preço: você escreve SQL como string, e o TypeScript não tem como saber se aquela coluna existe. O erro só aparece quando o código roda — geralmente em produção, geralmente numa sexta-feira.

O Prisma ataca isso de um jeito diferente. Ele lê o schema do banco, gera um client TypeScript inteiro baseado nisso, e qualquer query que você escrever é validada em tempo de compilação. Errou o nome de uma coluna? O editor te avisa antes do build.

Isso não é mágica. É geração de código a partir de introspecção de schema, e funciona bem justamente porque o PostgreSQL é rígido com tipos — o Prisma aproveita essa rigidez a favor do dev.

Instalando e configurando o Prisma

Parte com um projeto Node novo ou existente, tanto faz:

npm install prisma --save-dev
npm install @prisma/client
npx prisma init --datasource-provider postgresql

O prisma init cria uma pasta prisma/ com um schema.prisma e um .env com a variável DATABASE_URL. Ajusta ela pro seu banco:

DATABASE_URL="postgresql://usuario:senha@localhost:5432/meubanco?schema=public"

⚠️ Atenção: nunca versiona o .env com credenciais reais. Parece óbvio, mas já vi repositório público com string de conexão de produção — e o banco levou menos de um dia pra ser varrido por bot.

Com a conexão configurada, o schema.prisma fica assim por padrão:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

Esse arquivo é a fonte de verdade do projeto. Todo o resto — migrations, client gerado, tipos — deriva dele.

Modelando o schema do banco

Aqui é onde a maioria dos tutoriais mostra um exemplo bonito demais. Vamos de algo mais realista: um blog com usuários e posts, com uma relação de verdade.

model User {
  id        String   @id @default(uuid())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id        String   @id @default(uuid())
  title     String
  content   String
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime @default(now())

  @@index([authorId])
}

Repara no @@index([authorId]). É fácil esquecer índices em foreign keys, e o PostgreSQL não cria um automaticamente pra colunas de relação — só pra chave primária e colunas com @unique. Sem esse índice, qualquer query que filtra posts por autor vai fazer um sequential scan conforme a tabela cresce.

Rodando migrations sem drama

Com o schema definido, você gera a migration:

npx prisma migrate dev --name init

Esse comando faz três coisas: cria o arquivo SQL da migration, aplica no banco de desenvolvimento e regenera o Prisma Client. Cada mudança no schema pede um novo migrate dev com um nome descritivo — isso vira o histórico de mudanças do banco, versionado junto com o código.

Em produção, o comando é outro:

npx prisma migrate deploy

Esse não gera nada novo, só aplica as migrations pendentes. É o que você roda no CI/CD, nunca o migrate dev.

💡 Dica: se o CI quebrou com erro de "drift detected", normalmente é porque alguém alterou o banco direto, fora do fluxo de migrations. O Prisma detecta a divergência entre o schema esperado e o estado real do banco — e ele está certo em reclamar.

Fazendo queries type-safe

Com o client gerado, as queries ficam assim:

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function criarPost(authorId: string, title: string, content: string) {
  return prisma.post.create({
    data: {
      title,
      content,
      authorId,
    },
  })
}

❌ O que não fazer — instanciar PrismaClient em todo arquivo:

// em cada rota, cada service, cada arquivo
const prisma = new PrismaClient()

Isso esgota o pool de conexões do PostgreSQL rápido, principalmente em ambientes serverless onde cada invocação pode criar uma instância nova.

✓ O certo é centralizar numa única instância:

// lib/prisma.ts
import { PrismaClient } from '@prisma/client'

const globalForPrisma = global as unknown as { prisma: PrismaClient }

export const prisma =
  globalForPrisma.prisma || new PrismaClient()

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

Esse padrão de singleton evita que o hot-reload do Next.js, por exemplo, crie uma nova conexão a cada mudança de arquivo em dev.

Relações e o problema do N+1

Buscar um post com o autor é direto:

const post = await prisma.post.findUnique({
  where: { id: postId },
  include: { author: true },
})

Até aqui, tranquilo. O problema aparece quando você busca uma lista e, pra cada item, faz outra query separada — o clássico N+1. Isso apareceu num PR meu em 2021, numa listagem de posts que buscava o autor de cada um num loop for. O revisor só comentou: "isso vira 50 queries pra 50 posts?". Era exatamente isso.

❌ Gerando N+1 sem perceber:

const posts = await prisma.post.findMany()
for (const post of posts) {
  const author = await prisma.user.findUnique({ where: { id: post.authorId } })
}

✓ Resolvendo com include numa query só:

const posts = await prisma.post.findMany({
  include: { author: true },
})

O Prisma monta um JOIN por trás — ou, dependendo da estratégia de query engine configurada, faz batching inteligente das buscas relacionadas. De qualquer forma, é uma query em vez de N.

Erros comuns com Prisma e PostgreSQL

Erro comumPor que aconteceComo evitar
Timeout de conexão em produçãoMúltiplas instâncias de PrismaClient esgotando o poolUsar singleton, configurar connection_limit na URL
P2002 (unique constraint) não tratadoFalta de try/catch em torno de create com campo únicoCapturar erro e responder com mensagem clara pro usuário
Migration falha em produção mas passou localDados existentes violam nova constraint (ex: NOT NULL)Testar migration contra uma cópia do banco de produção antes
Query lenta sem índiceFiltro ou relação sem @@index no schemaRodar EXPLAIN ANALYZE nas queries mais usadas

Por que isso importa? Porque em produção você não tem segunda chance com uma migration mal testada — reverter schema de banco é bem mais caro que reverter um deploy de código.

FAQ

Prisma funciona bem com PostgreSQL em serverless (Vercel, Lambda)? Funciona, mas exige atenção ao pool de conexões. Serverless cria e destrói instâncias com frequência, então vale usar um pooler como o PgBouncer ou o Prisma Accelerate pra não estourar o limite de conexões do PostgreSQL.

Prisma substitui SQL puro? Não totalmente. Pra queries complexas — relatórios pesados, agregações específicas — o $queryRaw continua sendo a saída, e o Prisma tipa o retorno se você passar um schema Zod ou uma interface manual.

Vale a pena usar Prisma em projeto pequeno? Sim, principalmente pelo DX: autocomplete, migrations versionadas e menos bug de tipo. O overhead de configuração inicial é baixo comparado ao ganho em manutenção.

Como lidar com transações no Prisma? Use prisma.$transaction, passando um array de operações ou uma função. Ele garante atomicidade — se uma falhar, todas revertem.

Prisma tem suporte a soft delete nativo? Não nativamente. O padrão é adicionar um campo deletedAt no model e filtrar manualmente, ou usar client extensions pra automatizar isso nas queries.

Próximos passos

  • Configura o singleton do PrismaClient antes de qualquer outra coisa, mesmo em projeto pequeno.
  • Adiciona @@index em toda foreign key usada em filtro ou include.
  • Roda npx prisma studio pra inspecionar os dados sem sair do terminal — ajuda muito a debugar relação errada.
  • Testa migrations contra uma cópia real dos dados antes de rodar migrate deploy em produção.
  • Se o projeto crescer, avalia um connection pooler antes que o erro de timeout apareça sozinho.