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
