Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Drizzle ORM com PostgreSQL

Atualizado em: 20 de agosto de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

Drizzle ORM com PostgreSQL combina uma API tipada para TypeScript com consultas próximas do SQL. A biblioteca permite definir tabelas no código, criar migrations e executar operações usando o driver do PostgreSQL. Ela é útil para equipes que desejam ajuda do compilador sem esconder completamente joins, índices, transações e decisões do banco.

Tipagem não substitui validação em runtime, modelagem ou conhecimento de SQL. Uma consulta pode estar correta para o TypeScript e ainda ser lenta, bloquear linhas ou retornar dados que a aplicação não deveria expor. Neste guia, você verá como organizar schema, conexão, migrations, queries e testes. Para fundamentos, consulte PostgreSQL e Node.js.

Instalação

npm init -y
npm install drizzle-orm pg
npm install --save-dev drizzle-kit @types/pg typescript

Use uma versão suportada do Node.js e mantenha o lockfile versionado. A URL de conexão deve vir do ambiente ou de um gerenciador de segredos.

Definindo o schema

import {
  pgTable,
  uuid,
  varchar,
  integer,
  timestamp,
  index,
  uniqueIndex
} from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: uuid('id').defaultRandom().primaryKey(),
  email: varchar('email', { length: 320 }).notNull(),
  name: varchar('name', { length: 120 }).notNull(),
  createdAt: timestamp('created_at', {
    withTimezone: true
  }).defaultNow().notNull()
}, table => [
  uniqueIndex('users_email_unique').on(table.email)
]);

export const orders = pgTable('orders', {
  id: uuid('id').defaultRandom().primaryKey(),
  userId: uuid('user_id')
    .notNull()
    .references(() => users.id, { onDelete: 'restrict' }),
  totalCents: integer('total_cents').notNull(),
  status: varchar('status', { length: 30 }).notNull(),
  createdAt: timestamp('created_at', {
    withTimezone: true
  }).defaultNow().notNull()
}, table => [
  index('orders_user_created_idx').on(
    table.userId,
    table.createdAt
  )
]);

Defina restrições no banco, não apenas no código. NOT NULL, unique, foreign keys e checks impedem estados inválidos mesmo quando outro serviço ou script escreve diretamente.

Conexão e pool

import { Pool } from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';
import * as schema from './schema.js';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
  connectionTimeoutMillis: 3000,
  idleTimeoutMillis: 30000,
  statement_timeout: 5000,
  application_name: 'orders-api'
});

export const db = drizzle(pool, { schema });

O pool existe por processo. Se usar vários workers, multiplique o limite pela quantidade de instâncias. O artigo sobre pool PostgreSQL no Node.js explica como evitar excesso de conexões.

Inserindo dados

const [user] = await db
  .insert(users)
  .values({
    email: input.email.toLowerCase(),
    name: input.name
  })
  .returning({
    id: users.id,
    email: users.email,
    name: users.name
  });

Retorne somente os campos necessários. Trate violações de unique como conflito de negócio, sem expor detalhes internos da constraint ao cliente.

Consultas com filtros

import { and, desc, eq, lt } from 'drizzle-orm';

const page = await db
  .select({
    id: orders.id,
    totalCents: orders.totalCents,
    status: orders.status,
    createdAt: orders.createdAt
  })
  .from(orders)
  .where(and(
    eq(orders.userId, userId),
    cursor ? lt(orders.createdAt, cursor) : undefined
  ))
  .orderBy(desc(orders.createdAt))
  .limit(50);

Prefira paginação por cursor para tabelas grandes. OFFSET precisa percorrer e descartar linhas à medida que a página cresce. A ordenação deve ser estável; adicione id como desempate quando timestamps podem se repetir.

Joins

const result = await db
  .select({
    orderId: orders.id,
    totalCents: orders.totalCents,
    customerName: users.name,
    customerEmail: users.email
  })
  .from(orders)
  .innerJoin(users, eq(orders.userId, users.id))
  .where(eq(orders.id, orderId));

Selecione campos explicitamente. Isso evita transferir colunas sensíveis e torna o contrato da consulta visível. Use EXPLAIN ANALYZE em consultas importantes e crie índices baseados no padrão real de filtros e ordenação.

Transações

await db.transaction(async tx => {
  const [order] = await tx
    .insert(orders)
    .values({
      userId,
      totalCents,
      status: 'pending'
    })
    .returning({ id: orders.id });

  await tx.insert(outboxEvents).values({
    aggregateId: order.id,
    type: 'order.created',
    payload: { orderId: order.id, totalCents }
  });
});

Transações devem ser curtas. Não faça chamadas HTTP ou espere filas enquanto mantém locks. O padrão outbox grava o evento na mesma transação e publica depois, evitando inconsistência entre banco e mensageria.

Migrations

Use drizzle-kit para gerar migrations revisáveis. Nunca use push automático no banco de produção sem entender o SQL.

npx drizzle-kit generate
npx drizzle-kit migrate

Leia cada migration, teste com uma cópia representativa e planeje rollback. Alterações destrutivas devem ser divididas em etapas compatíveis: adicionar, preencher, mudar leitores, mudar escritores e remover somente depois.

Configuração do Drizzle Kit

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  dialect: 'postgresql',
  schema: './src/db/schema.ts',
  out: './drizzle',
  dbCredentials: {
    url: process.env.DATABASE_URL
  },
  strict: true,
  verbose: true
});

Não execute essa configuração em ambientes sem a variável obrigatória. O pipeline de migration deve ter credenciais separadas das credenciais da aplicação, com permissões apropriadas.

Validação de entrada

Os tipos do Drizzle representam o código compilado, mas dados HTTP continuam não confiáveis. Valide comprimento, formato, faixas numéricas e campos permitidos. Veja Zod no TypeScript.

Não passe objetos inteiros recebidos do cliente diretamente para values() ou set(). Mapeie os campos permitidos para impedir mass assignment.

Atualizações seguras

const [updated] = await db
  .update(orders)
  .set({ status: 'paid' })
  .where(and(
    eq(orders.id, orderId),
    eq(orders.status, 'pending')
  ))
  .returning({ id: orders.id });

if (!updated) {
  throw new ConflictError('Order cannot be paid');
}

Incluir o estado esperado no WHERE evita sobrescrever mudanças concorrentes. Para modelos mais complexos, use versão ou timestamp de atualização como controle otimista.

SQL customizado

O Drizzle permite fragmentos SQL para recursos específicos. Use parâmetros, nunca concatenação de entrada.

import { sql } from 'drizzle-orm';

const stats = await db.execute(sql`
  select status, count(*)::int as total
  from orders
  where created_at >= ${startDate}
  group by status
`);

SQL explícito não é uma falha do ORM. Em relatórios e consultas avançadas, ele pode ser a opção mais clara, desde que seja tipado, testado e medido.

Tratamento de erros

Mapeie códigos do PostgreSQL para erros de aplicação. Violação de unique pode virar 409; foreign key inválida pode virar 400 ou 409; timeout pode virar 503. Não envie query, stack ou detalhes da conexão ao usuário.

Use códigos internos e requestId. Registre duração, operação e contexto mínimo, removendo valores sensíveis.

Segurança

  • use usuário de banco com privilégios mínimos;
  • ative TLS quando a rede não é confiável;
  • parametrize SQL customizado;
  • não registre senhas nem URLs completas com credenciais;
  • aplique limites e timeouts;
  • valide campos antes das queries;
  • mantenha migrations sob revisão;
  • faça backup e teste restauração.

Observabilidade

Meça tempo de aquisição de conexão, duração de consultas, timeouts, erros por código e uso do pool. Slow queries devem ser investigadas com plano de execução. Não use texto completo da query com valores como label de métrica.

O guia sobre performance de APIs Node.js ajuda a conectar métricas do banco à latência HTTP.

Testes

Use um banco isolado para integração, aplique migrations e limpe dados entre testes. Teste constraints, transações, concorrência e paginação. Não substitua todos os testes por mocks, pois boa parte do comportamento está no PostgreSQL.

import test from 'node:test';
import assert from 'node:assert/strict';

test('não cria e-mail duplicado', async () => {
  await createUser({ email: 'a@example.com', name: 'A' });

  await assert.rejects(
    () => createUser({ email: 'a@example.com', name: 'B' }),
    error => error.code === 'USER_EMAIL_CONFLICT'
  );
});

Consulte também testes unitários com Jest.

Erros comuns

  • Confiar apenas nos tipos: entrada externa ainda precisa de validação;
  • Ignorar índices: queries tipadas continuam podendo ser lentas;
  • Migrations automáticas: mudanças destrutivas chegam sem revisão;
  • Transações longas: locks e filas aumentam;
  • Pool por requisição: conexões são abertas sem controle;
  • Select de todas as colunas: dados sensíveis podem vazar.

Checklist

  • schema possui constraints reais;
  • pool e timeouts foram definidos;
  • queries selecionam campos explícitos;
  • entradas são validadas;
  • migrations são revisadas;
  • transações são curtas;
  • consultas críticas possuem índices;
  • integração usa banco real de teste.

Referências oficiais

Conclusão

Drizzle ORM com PostgreSQL oferece uma camada tipada sem afastar o desenvolvedor do SQL. Schemas, queries e migrations ficam próximos do código, facilitando revisão e refatoração.

O resultado confiável depende de constraints no banco, validação em runtime, pools dimensionados, migrations graduais e consultas medidas. Quando esses elementos são tratados juntos, o ORM melhora produtividade sem esconder as responsabilidades de um banco relacional em produção.

Os 10 Melhores Cursos de Programação de 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita