Migrações de Banco de Dados com PostgreSQL: Prisma Migrate vs Drizzle Kit

Conteúdo técnico toda semana
Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.
Sem spam. Cancele a qualquer momento com 1 clique.
Neste artigo
O problema que migração resolve (e o que ela cria)
Alterar a estrutura de um banco PostgreSQL em produção sem um sistema de migrações é como editar código direto no servidor: funciona até o dia que não funciona. Migrações versionam o schema do banco da mesma forma que Git versiona o código. Cada alteração vira um arquivo rastreável, reversível e aplicável em qualquer ambiente.
A questão prática é: qual ferramenta usar? Prisma Migrate e Drizzle Kit resolvem o mesmo problema com filosofias opostas. Prisma gera SQL a partir de um schema declarativo próprio. Drizzle gera SQL a partir de definições TypeScript que espelham a estrutura real do banco. A escolha entre eles afeta como você escreve queries, como faz deploy e como lida com migrações que dão errado.
Se você já trabalha com Prisma no Fastify, este post vai expandir o que você sabe sobre o lado de migrações. Se está avaliando Drizzle, vai sair daqui com critérios concretos para decidir.
Setup: do zero à primeira migração
Prisma Migrate
# Instala Prisma como dependência de desenvolvimento
npm install prisma --save-dev
npm install @prisma/client
# Inicializa o diretório prisma/ com schema.prisma e .env
npx prisma init --datasource-provider postgresqlO arquivo schema.prisma é o ponto central. Toda alteração de schema passa por ele:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// @map renomeia a coluna no banco sem mudar o nome no TypeScript
@@map("users")
}
model Post {
id Int @id @default(autoincrement())
title String @db.VarChar(255)
content String? @db.Text
published Boolean @default(false)
authorId Int @map("author_id")
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now()) @map("created_at")
@@index([authorId])
@@map("posts")
}# Gera o SQL de migração e aplica no banco de desenvolvimento
npx prisma migrate dev --name create_users_and_postsEsse comando faz três coisas: gera um arquivo SQL em prisma/migrations/, aplica no banco e regenera o Prisma Client.
Drizzle Kit
npm install drizzle-orm pg
npm install drizzle-kit --save-dev
npm install @types/pg --save-devNo Drizzle, o schema é TypeScript puro:
// src/db/schema.ts
import { pgTable, serial, varchar, text, boolean, integer, timestamp } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
email: varchar("email", { length: 255 }).notNull().unique(),
name: varchar("name", { length: 255 }),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
export const posts = pgTable("posts", {
id: serial("id").primaryKey(),
title: varchar("title", { length: 255 }).notNull(),
content: text("content"),
published: boolean("published").default(false).notNull(),
// Referência explícita à tabela users
authorId: integer("author_id").notNull().references(() => users.id),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
// Relations são separadas da definição de tabela
// Isso mantém o schema da tabela puro e as queries relacionais opcionais
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle/migrations",
dialect: "postgresql",
dbCredentials: {
// Usa variável de ambiente diretamente, sem .env mágico
url: process.env.DATABASE_URL!,
},
});# Gera o SQL de migração comparando schema atual com estado do banco
npx drizzle-kit generate
# Aplica migrações pendentes
npx drizzle-kit migrateComparação operacional: onde cada um brilha
| Critério | Prisma Migrate | Drizzle Kit |
|---|---|---|
| Linguagem do schema | Prisma Schema Language (DSL própria) | TypeScript puro |
| Geração de SQL | Automática, baseada em diff do schema | Automática, baseada em diff do schema |
| SQL editável antes de aplicar | Sim, arquivo .sql gerado pode ser editado | Sim, arquivo .sql gerado pode ser editado |
| Migração customizada (data migration) | Editar o SQL gerado manualmente | Editar o SQL gerado ou criar migration vazia |
| Rollback nativo | Não tem rollback automático (precisa criar migração reversa) | Não tem rollback automático (mesma abordagem) |
| Introspecção de banco existente | prisma db pull | drizzle-kit pull |
| Type safety nas queries | Prisma Client gerado | Inferência direta do schema TypeScript |
| Overhead de runtime | Prisma Engine (binário Rust, ~15MB) | Zero: traduz para SQL no build, driver nativo |
| Curva de aprendizado | Maior (DSL própria + conceitos Prisma) | Menor para quem conhece SQL |
A diferença de arquitetura de runtime é significativa. O Prisma Client depende de um query engine binário que roda como sidecar do processo Node.js. O Drizzle gera SQL diretamente e usa o driver pg nativo. Em ambientes com restrição de tamanho de bundle (como Edge Functions), o Drizzle tem vantagem clara. O Prisma tem um adapter para edge, mas com limitações.
Workflow de desenvolvimento: adicionando uma coluna
Cenário concreto: adicionar um campo bio na tabela users.
Com Prisma
Edite o schema.prisma:
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
bio String? @db.Text // Nova coluna
posts Post[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("users")
}npx prisma migrate dev --name add_bio_to_usersO arquivo gerado em prisma/migrations/<timestamp>_add_bio_to_users/migration.sql:
-- AlterTable
ALTER TABLE "users" ADD COLUMN "bio" TEXT;Com Drizzle
Edite o schema.ts:
export const users = pgTable("users", {
id: serial("id").primaryKey(),
email: varchar("email", { length: 255 }).notNull().unique(),
name: varchar("name", { length: 255 }),
bio: text("bio"), // Nova coluna
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});npx drizzle-kit generateO resultado é equivalente: um arquivo SQL com ALTER TABLE. A diferença está no que acontece ao redor. O Prisma regenera o client automaticamente. No Drizzle, o tipo já está atualizado porque o schema é TypeScript: a inferência pega a mudança no próximo import.
O que NÃO fazer
Anti-pattern 1: renomear coluna sem cuidado
Tanto Prisma quanto Drizzle interpretam "remover coluna A e criar coluna B" quando você renomeia. Isso destrói dados.
Errado (Prisma):
// ANTES
model User {
name String?
}
// DEPOIS: mudou "name" para "displayName"
model User {
displayName String? @map("display_name")
}O prisma migrate dev vai gerar:
-- PERIGOSO: DROP + ADD, dados perdidos
ALTER TABLE "users" DROP COLUMN "name";
ALTER TABLE "users" ADD COLUMN "display_name" TEXT;Correto: edite o SQL gerado antes de aplicar, ou crie a migração em dois passos.
-- Renomeia sem perder dados
ALTER TABLE "users" RENAME COLUMN "name" TO "display_name";O Drizzle Kit tem o mesmo comportamento. Ambas as ferramentas pedem que você revise o SQL gerado antes de aplicar em produção. Se você não lê o SQL, vai perder dados. Sem exceção.
Para estratégias seguras de alteração de colunas em produção, o post sobre zero-downtime migrations cobre o assunto em profundidade.
Anti-pattern 2: rodar prisma migrate dev em produção
O comando migrate dev é exclusivo para desenvolvimento. Ele reseta o banco se detecta drift (diferença entre o estado esperado e o real). Em produção:
# ERRADO: pode dropar e recriar tabelas
npx prisma migrate dev
# CORRETO: aplica apenas migrações pendentes, falha se houver drift
npx prisma migrate deployNo Drizzle, o drizzle-kit push (que sincroniza schema direto no banco sem gerar arquivo) tem o mesmo risco. Use drizzle-kit migrate em produção, que aplica apenas os arquivos SQL versionados.
Anti-pattern 3: ignorar migrações de dados
Ferramentas de migração de schema não resolvem migração de dados. Se você adiciona uma coluna NOT NULL sem default em uma tabela com registros existentes, a migração falha.
-- FALHA: coluna NOT NULL sem default em tabela com dados
ALTER TABLE "users" ADD COLUMN "role" VARCHAR(50) NOT NULL;
-- CORRETO: adiciona com default, atualiza, remove default se necessário
ALTER TABLE "users" ADD COLUMN "role" VARCHAR(50) NOT NULL DEFAULT 'member';Quando a migração envolve transformação de dados existentes (split de coluna, merge de tabelas), escreva o SQL manualmente dentro do arquivo de migração. Nenhuma das duas ferramentas gera data migrations automaticamente.
Para cenários onde precisa reverter migrações sem perder dados, o post sobre reverter migrations com segurança detalha estratégias práticas.
Deploy em produção: CI/CD pipeline
A migração em produção deve rodar antes do deploy da aplicação, nunca durante o boot. Um exemplo com GitHub Actions:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
# Para Prisma:
- run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
# OU para Drizzle:
# - run: npx drizzle-kit migrate
# env:
# DATABASE_URL: ${{ secrets.DATABASE_URL }}
deploy:
needs: migrate # Só deploya se a migração passou
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# ... passos de deploy da aplicaçãoSeparar a etapa de migração do deploy da aplicação permite que, se a migração falhar, o deploy não aconteça. Se a aplicação falhar no deploy, o banco já está no estado correto para a próxima tentativa. Esse padrão se integra com estratégias de deploy como blue-green, onde o novo schema precisa ser compatível com a versão antiga da aplicação durante a transição.
Quando escolher Prisma, quando escolher Drizzle
A decisão depende de três fatores concretos:
Escolha Prisma Migrate quando:
- O time tem devs com pouca experiência em SQL e se beneficia de uma abstração mais opinativa
- Você já usa Prisma Client e quer manter uma ferramenta só para schema + queries + migrações
- O projeto usa multitenancy com Prisma e você quer manter consistência no tooling
- Precisa de introspecção de banco legado para gerar o schema inicial (
prisma db pullé maduro)
Escolha Drizzle Kit quando:
- O time é confortável com SQL e quer controle fino sobre as queries geradas
- O deploy alvo inclui edge runtimes ou ambientes com restrição de bundle size
- Você quer type safety sem camada de abstração intermediária (sem query engine binário)
- O projeto precisa de queries SQL complexas (CTEs, window functions, subqueries laterais) que o Prisma abstrai com dificuldade
Nenhum dos dois resolve:
- Migrações que exigem coordenação entre múltiplos serviços (use um orquestrador dedicado)
- Schema changes que precisam de processamento assíncrono pesado para migrar dados em background
- Rollback automático de dados (ambos versionam schema, não dados)
Introspecção: partindo de um banco existente
Se você já tem um PostgreSQL rodando e quer adotar uma dessas ferramentas, ambas suportam introspecção:
# Prisma: gera schema.prisma a partir do banco existente
npx prisma db pull
# Drizzle: gera schema TypeScript a partir do banco existente
npx drizzle-kit pullO Prisma gera o schema.prisma completo, incluindo relações inferidas das foreign keys. O Drizzle gera arquivos TypeScript com a definição das tabelas. Em ambos os casos, revise o resultado: nomes de relações inferidos nem sempre fazem sentido semântico, e constraints complexas (check constraints, partial indexes) podem não ser capturadas completamente.
Para projetos que usam Supabase como backend, a introspecção é o caminho para trazer o schema existente do Supabase para dentro do versionamento local.
Checklist de migração segura
- Leia o SQL gerado antes de aplicar. Sempre.
- Teste a migração em um dump do banco de produção, não em banco vazio.
- Migrações que adicionam
NOT NULLsem default falham em tabelas com dados. - Renomear coluna gera DROP + ADD. Edite o SQL para usar
RENAME COLUMN. - Separe migração de schema de migração de dados.
- Em produção, use
migrate deploy(Prisma) oumigrate(Drizzle), nunca os comandos de desenvolvimento. - Rode migrações antes do deploy da aplicação, não durante o boot.
FAQ
Posso usar Prisma Migrate e Drizzle no mesmo projeto?
Tecnicamente sim, mas é uma péssima ideia. Cada ferramenta mantém sua própria tabela de controle de migrações (_prisma_migrations e __drizzle_migrations). Ter duas fontes de verdade para o estado do schema gera conflitos inevitáveis. Escolha uma.
O Prisma Migrate suporta rollback automático?
Não. O Prisma não gera migrações reversas. Se uma migração precisa ser desfeita, você cria uma nova migração que reverte as alterações manualmente. O Drizzle Kit tem a mesma limitação. Para estratégias de rollback seguro, veja o post sobre reverter migrations sem perder dados.
Como lidar com migrações em branches diferentes do Git?
Ambas as ferramentas geram arquivos com timestamp no nome. Se dois devs criam migrações em branches paralelas, os arquivos não conflitam no merge (nomes diferentes), mas o schema final pode ser inconsistente. A prática que funciona: após o merge, rode prisma migrate dev ou drizzle-kit generate para verificar se o estado do schema está correto. Se houver divergência, gere uma migração de reconciliação.
Drizzle Kit funciona com schemas PostgreSQL separados (não o public)?
Sim. No schema TypeScript, use pgSchema para definir schemas customizados:
import { pgSchema, serial, varchar } from "drizzle-orm/pg-core";
// Define um schema PostgreSQL separado do public
const tenantSchema = pgSchema("tenant_001");
export const users = tenantSchema.table("users", {
id: serial("id").primaryKey(),
email: varchar("email", { length: 255 }).notNull(),
});Qual é o impacto de performance do Prisma Engine vs Drizzle em queries?
O overhead do Prisma Engine (o binário Rust que traduz queries) é mensurável mas raramente é gargalo. Em benchmarks sintéticos, Drizzle é mais rápido por não ter a camada intermediária. Na prática, se sua aplicação faz menos de 1000 queries por segundo, a diferença é irrelevante. Se faz mais, o gargalo provavelmente está no PostgreSQL, não no ORM. Otimize índices e queries antes de trocar de ferramenta por performance.
Posição: Drizzle é a escolha mais segura para projetos novos em 2025
Prisma democratizou o acesso a bancos de dados para desenvolvedores TypeScript, e o Prisma Client continua sendo a melhor experiência de autocompletion para quem não quer pensar em SQL. Mas o custo dessa abstração (binário de runtime, DSL própria, lock-in no ecossistema) pesa mais do que o benefício quando o time conhece SQL.
Drizzle Kit produz migrações previsíveis porque o schema é TypeScript que mapeia 1:1 para SQL. Não há tradução de DSL intermediária. Quando algo dá errado, você debugga SQL, não a interpretação que o Prisma fez do seu schema. Para projetos que usam Clean Architecture, o Drizzle se encaixa melhor como detalhe de infraestrutura que não vaza para as camadas superiores.
Dito isso, se seu time já usa Prisma e está produtivo, migrar para Drizzle por purismo técnico é desperdício. A melhor ferramenta de migração é a que todo mundo no time sabe operar com segurança.

Escrito por
Marcos Soares
Fullstack Developer · CEO da Agência Poti
Fullstack Developer e CEO da Agência Poti. Mais de 20 anos construindo arquiteturas cloud-native com React, Next.js e sistemas distribuídos. Parceiro comercial do estúdio iellou design. Fundador do Vivo de Código.
Comentários
Participe da discussão
Seja o primeiro a comentar!
Continue Aprofundando
Guias de integração relacionados
Conteúdo técnico toda semana
Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.
Sem spam. Cancele a qualquer momento com 1 clique.


