Drizzle ORM con TypeScript: schema tipado, queries con inferencia y migraciones

Drizzle ORM es la alternativa más cercana a SQL dentro del ecosistema TypeScript: el schema es código TypeScript puro, los tipos se infieren automáticamente de las definiciones de tablas y las queries se construyen con una API fluent que mantiene el tipado a lo largo de toda la cadena.

Definir el schema

// src/db/schema.ts
import { pgTable, serial, text, integer, timestamp, boolean } from 'drizzle-orm/pg-core';

export const usuarios = pgTable('usuarios', {
  id: serial('id').primaryKey(),
  nombre: text('nombre').notNull(),
  email: text('email').notNull().unique(),
  edad: integer('edad'),
  activo: boolean('activo').notNull().default(true),
  creadoEn: timestamp('creado_en').defaultNow().notNull(),
});

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  titulo: text('titulo').notNull(),
  cuerpo: text('cuerpo').notNull(),
  autorId: integer('autor_id').notNull().references(() => usuarios.id),
  publicadoEn: timestamp('publicado_en'),
});

Tipos inferidos con $inferSelect e $inferInsert

import { usuarios, posts } from './schema';

// Tipo completo de una fila seleccionada:
type Usuario = typeof usuarios.$inferSelect;
// { id: number; nombre: string; email: string; edad: number | null; activo: boolean; creadoEn: Date }

// Tipo para insertar (id y creadoEn son opcionales porque tienen defaults):
type NuevoUsuario = typeof usuarios.$inferInsert;
// { nombre: string; email: string; edad?: number | null; activo?: boolean; creadoEn?: Date }

type Post = typeof posts.$inferSelect;
type NuevoPost = typeof posts.$inferInsert;

Queries CRUD encadenadas

import { drizzle } from 'drizzle-orm/node-postgres';
import { eq, and, like, gte, desc } from 'drizzle-orm';
import { Pool } from 'pg';
import { usuarios } from './schema';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzle(pool, { schema: { usuarios } });

// SELECT con filtros:
const listaActivos = await db
  .select()
  .from(usuarios)
  .where(and(eq(usuarios.activo, true), gte(usuarios.edad, 18)))
  .orderBy(desc(usuarios.creadoEn))
  .limit(10);
// listaActivos: Usuario[]

// INSERT:
const [nuevoUsuario] = await db
  .insert(usuarios)
  .values({ nombre: 'Carlos', email: '[email protected]' })
  .returning();
// nuevoUsuario: Usuario

// UPDATE:
const [actualizado] = await db
  .update(usuarios)
  .set({ nombre: 'Carlos García' })
  .where(eq(usuarios.id, nuevoUsuario.id))
  .returning();

// DELETE:
await db.delete(usuarios).where(eq(usuarios.id, 999));

Joins tipados

import { posts } from './schema';

const resultado = await db
  .select({
    postId: posts.id,
    titulo: posts.titulo,
    autor: usuarios.nombre,
  })
  .from(posts)
  .innerJoin(usuarios, eq(posts.autorId, usuarios.id))
  .where(eq(usuarios.activo, true));

// resultado: { postId: number; titulo: string; autor: string }[]
// TypeScript conoce exactamente los campos y sus tipos

Migraciones con drizzle-kit

// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './src/db/schema.ts',
  out: './drizzle',          // carpeta donde se guardan las migraciones SQL
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

// package.json
// "scripts": {
//   "db:generate": "drizzle-kit generate",
//   "db:push": "drizzle-kit push",
//   "db:migrate": "drizzle-kit migrate",
//   "db:studio": "drizzle-kit studio"
// }

drizzle-kit generate compara el schema actual con el anterior y genera el SQL de migración. drizzle-kit push aplica los cambios directamente a la base de datos (útil en desarrollo). drizzle-kit migrate aplica las migraciones pendientes en orden.

Consultas reutilizables y queries preparadas

import { sql, placeholder } from 'drizzle-orm';

// Query preparada con placeholder tipado:
const usuarioPorEmail = db
  .select()
  .from(usuarios)
  .where(eq(usuarios.email, placeholder('email')))
  .prepare('usuario_por_email');

const resultado = await usuarioPorEmail.execute({ email: '[email protected]' });
// resultado: Usuario[]

Imagen: Pexels / Tima Miroshnichenko

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP