Migrar de Prisma a Drizzle: Una retrospectiva de seis meses
Seis meses después de trasladar una stack de PostgreSQL y TypeScript de Prisma a Drizzle ORM: pruebas reales, qué mejoró de inmediato y las compensaciones que resultaron problemáticas.
Fuentes: I replaced Prisma with Drizzle. Six months later, here’s what I gained and what I missed.
Hace unos ocho meses, una duda persistente sobre Prisma finalmente se concretó en algo que merecía ser abordado. No era que hubiera algo rotundo, sino que la fricción se había ido acumulando lentamente con el tiempo hasta volverse imposible de ignorar. La configuración incluía varias aplicaciones pequeñas, todas ejecutadas en PostgreSQL y escritas en TypeScript, detrás de unos pocos servicios API. Nada inusual o de vanguardia. Prisma hacía lo que se suponía que debía hacer. Aun así, seguían apareciendo algunos problemas recurrentes.
El archivo @prisma/client generado era enorme. Al ejecutarse en funciones serverless, los arranques en frío se demoraban notablemente porque primero había que cargar el cliente. El paso de prisma generate en la pipeline de CI comenzó a parecer un costo que se pagaba con cada fusión. Y cada vez que se necesitaba una consulta en bruto para algo más complejo de lo que podía expresar el ORM, el tipo de retorno era unknown[], lo que eliminaba por completo la seguridad tipológica y obligaba a realizar conversiones manuales en todas partes.
Finalmente, se decidió pasar a Drizzle ORM, y la migración finalizó hace aproximadamente seis meses. A continuación, se presenta una descripción sincera de cómo transcurrió todo.
Por qué consideré primero Drizzle
El problema del tamaño del paquete fue el primer factor real que nos llevó a tomar en serio a Drizzle. En el momento de la migración, solo el cliente Prisma añadía casi 14 MB al paquete de despliegue de Lambda, siendo el binario del motor de consultas Rust responsable de varios de esos megabytes por sí solo. Los puntos de acceso que no se utilizaban con frecuencia presentaban tiempos de inicio en frío de 2 a 3 segundos, una cifra que se destaca inmediatamente en un panel de control de latencia.
En comparación, Drizzle ocupa alrededor de 7.4 KB una vez minificado y comprimido, sin ninguna dependencia en tiempo de ejecución. No hay binario compilado, ni proceso del motor separado, ni paso de serialización entre el código TypeScript y la conexión a la base de datos.
La segunda fuente de fricción fue el paso de generación de código. Cualquier cambio en el esquema requería ejecutar prisma generate antes de que TypeScript pudiera reconocer la nueva estructura. En la práctica, esto llevaba a uno de dos resultados negativos: o bien se ejecutaba manualmente y, ocasionalmente, se olvidaba guardar el cliente regenerado, o bien se integraba en cada ejecución de CI, lo que añadía entre 15 y 20 segundos adicionales por compilación. Ninguna de las opciones era satisfactoria.
Con Drizzle, el propio esquema es simplemente TypeScript. Al editar el archivo del esquema, los tipos se actualizan de inmediato; no hay paso de generación intermedio, y el compilador detecta el cambio al instante.
El tercer factor motivador fue la forma en que se manejaba SQL puro. Prisma lo trata exclusivamente como una solución de último recurso: al llamar a $queryRaw se obtiene unknown[], lo cual requiere ya sea un cast manual o un esquema Zod para poder usarlo de manera segura. Por otro lado, la etiqueta de plantilla sql de Drizzle permite declarar directamente la estructura, algo como sql<{ id: number; total: number }>, y se obtiene exactamente ese tipo, sin necesidad de casting ni de una capa adicional de validación.
Así fue la migración
Para ilustrarlo, consideremos tres tablas: users, products y orders. Con Prisma, estaban definidas en un único archivo schema.prisma:
model User {
id String @id @default(uuid())
email String @unique
name String
orders Order[]
createdAt DateTime @default(now())
}
model Product {
id String @id @default(uuid())
name String
priceInCents Int
orders OrderItem[]
}
model Order {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id])
items OrderItem[]
createdAt DateTime @default(now())
}
model OrderItem {
id String @id @default(uuid())
orderId String
order Order @relation(fields: [orderId], references: [id])
productId String
product Product @relation(fields: [productId], references: [id])
quantity Int
}
Con Drizzle, el mismo esquema se convierte en un archivo de TypeScript:
import { pgTable, uuid, text, integer, timestamp } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';\
export const users = pgTable('users', {
id: uuid('id').defaultRandom().primaryKey(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const products = pgTable('products', {
id: uuid('id').defaultRandom().primaryKey(),
name: text('name').notNull(),
priceInCents: integer('price_in_cents').notNull(),
});
export const orders = pgTable('orders', {
id: uuid('id').defaultRandom().primaryKey(),
userId: uuid('user_id').notNull().references(() => users.id),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const orderItems = pgTable('order_items', {
id: uuid('id').defaultRandom().primaryKey(),
orderId: uuid('order_id').notNull().references(() => orders.id),
productId: uuid('product_id').notNull().references(() => products.id),
quantity: integer('quantity').notNull(),
});
export const usersRelations = relations(users, ({ many }) => ({
orders: many(orders),
}));
export const ordersRelations = relations(orders, ({ one, many }) => ({
user: one(users, { fields: [orders.userId], references: [users.id] }),
items: many(orderItems),
}));
export const orderItemsRelations = relations(orderItems, ({ one }) => ({
order: one(orders, { fields: [orderItems.orderId], references: [orders.id] }),
product: one(products, { fields: [orderItems.productId], references: [products.id] }),
}));
Sí, tarda un poco más en ejecutarse. Pero se trata de TypeScript de principio a fin. Tu editor entiende cada campo del esquema, el autocompletado funciona directamente con él, y puedes importar partes del esquema directamente en la lógica de tu aplicación.
Un archivo drizzle.config.ts reúne todo esto:
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'postgresql',
schema: './src/schema.ts',
out: './drizzle/migrations',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
strict: true,
});
Cuidado: siempre establece strict: true. Si lo omites, Drizzle podría interpretar un cambio de nombre de columna como “eliminar la columna antigua y crear una nueva”, lo que destruye silenciosamente los datos hasta que examines el SQL generado para ver qué sucedió. Al activar strict: true, Drizzle se detiene y solicita confirmación cada vez que hay un cambio ambiguo.
Con el esquema listo, generar una migración se hace con un solo comando:
bunx drizzle-kit generate
Eso genera un archivo SQL dentro de ./drizzle/migrations. Lo inspeccionas, confirmas que hace lo esperado y luego lo aplicas:
bunx drizzle-kit migrate
Ese archivo SQL se registra en el control de versiones justo al lado del esquema. Todo el historial de migraciones está en SQL claro y legible, en lugar de un formato de registro que solo tiene sentido mediante las herramientas propias de Prisma.
Qué mejoró de inmediato
Bultos más pequeños, arranques en frío más rápidos. Esto fue evidente de inmediato y fácil de medir. El tamaño de los bultos de Lambda disminuyó en más del 90 por ciento. Las funciones que anteriormente necesitaban más de dos segundos para arrancar en frío ahora lo hacen en menos de 400 ms. Los trabajos en segundo plano no obtuvieron beneficio real de esto, pero nuestras funciones API se aceleraron notablemente.
SQL en bruto escrito a mano. Esto resultó útil literalmente el segundo día de la migración. Teníamos una consulta de informes que unía cuatro tablas con operaciones de agregación que el generador de consultas de Prisma simplemente no podía expresar de manera limpia. La solución alternativa de Prisma para este tipo de consulta era la siguiente:
// Prisma - returns unknown[], no type safety
const revenue = await prisma.$queryRaw`
SELECT p.id, p.name, SUM(oi.quantity * p.price_in_cents) as total_revenue
FROM products p
JOIN order_items oi ON p.id = oi.product_id
GROUP BY p.id, p.name
ORDER BY total_revenue DESC
`;
Con Drizzle, la consulta equivalente devuelve un tipo real asociado:
import { sql } from 'drizzle-orm';
interface ProductRevenue {
id: string;
name: string;
totalRevenue: number;
}
const revenue = await db.execute(sql<ProductRevenue>`
SELECT p.id, p.name, SUM(oi.quantity * p.price_in_cents) AS total_revenue
FROM products p
JOIN order_items oi ON p.id = oi.product_id
GROUP BY p.id, p.name
ORDER BY total_revenue DESC
`);
// revenue.rows typed as ProductRevenue[] at compile-time; validate at runtime if needed
Aún se escribe la interfaz a mano en lugar de que se infiera automáticamente, pero eso representa un costo menor en comparación con tener que insertar castings unknown por toda la base de código.
Drizzle Studio. Al ejecutar bunx drizzle-kit studio se inicia un navegador de bases de datos local. Es menos pulido que Prisma Studio, pero es rápido, funcional y no requiere una cuenta de Prisma ni conexión a la nube. Para el trabajo local diario, esa independencia tiene su valor.
Qué requirió ajustes
Las relaciones necesitan una configuración más explícita. En Prisma, obtener un pedido junto con su usuario y los artículos asociados era tan sencillo como anidar un include:
// Prisma - nested include
const order = await prisma.order.findUnique({
where: { id: orderId },
include: {
user: true,
items: {
include: { product: true }
}
}
});
La API de consultas relacionales de Drizzle permite obtener un resultado similar:
import { db } from './db';
import { orders, users, orderItems, products } from './schema';
const order = await db.query.orders.findFirst({
where: (orders, { eq }) => eq(orders.id, orderId),
with: {
user: true,
items: {
with: { product: true }
}
}
});
La forma final de las dos consultas es similar. El problema es que la sintaxis with solo funciona una vez que se han definido las llamadas a relations() en el esquema, las mismas que se describieron anteriormente. Cuando cambiamos por primera vez, aún no habíamos terminado de configurarlas y pasamos toda una tarde intentando entender por qué with no devolvía nada. Una vez que las relaciones están declaradas correctamente, todo funciona como se espera; pero ese paso inicial de configuración lo maneja Prisma implícitamente a partir únicamente del esquema, sin necesidad de configurar nada adicional.
La semillaización ahora es una tarea manual. Prisma incluye una convención de semillaización integrada: basta con colocar un archivo seed.ts en su lugar, ejecutar prisma db seed, y listo. Drizzle no tiene un comando equivalente integrado. En su lugar, se escribe un script normal de TypeScript y se ejecuta manualmente:
import { db } from './db';
import { users, products } from './schema';
async function seed() {
await db.insert(users).values([
{ email: 'ana.silva@acme.com', name: 'Ana Silva' },
{ email: 'carlos.reyes@acme.com', name: 'Carlos Reyes' },
]);
await db.insert(products).values([
{ name: 'Starter Plan', priceInCents: 2900 },
{ name: 'Pro Plan', priceInCents: 7900 },
{ name: 'Enterprise Plan', priceInCents: 19900 },
]);
console.log('Seed complete');
process.exit(0);
}
seed().catch(console.error);
Y lo configuras en package.json:
{
"scripts": {
"db:seed": "bun run src/seed.ts"
}
}
Nada de esto es difícil, y se podría argumentar que es más transparente que la convención implícita de Prisma. Pero cualquier herramienta o paso de CI existente que llamara directamente a prisma db seed necesitará ser reescrito para llamar a tu propio script en su lugar.
Comparación directa de los dos estudios. Drizzle Studio cumple con la función. Prisma Studio simplemente tiene un aspecto mejor y ofrece más: controles de filtro integrados, paginación y un editor de tablas más pulido. En comparación, Drizzle Studio parece bastante básico. Después de seis meses usando Drizzle, hemos terminado dependiendo menos de ambas herramientas de lo esperado, ya que la mayor parte de la exploración de datos reales se realiza a través de un cliente de base de datos dedicado, independientemente del ORM que se utilice.
El único caso en el que Prisma sigue ganando
Escribimientos anidados. Cuando necesitas insertar un pedido junto con sus ítems en un solo paso, Prisma lo gestiona de manera eficiente:
// Prisma - nested create in one call
const order = await prisma.order.create({
data: {
userId: user.id,
items: {
create: [
{ productId: starterPlanId, quantity: 1 },
{ productId: proPlanId, quantity: 2 },
]
}
},
include: { items: true }
});
Drizzle no cuenta con un equivalente para las creaciones anidadas. En su lugar, debes escribir tú mismo la lógica dentro de una transacción explícita:
import { db } from './db';
import { orders, orderItems } from './schema';
const newOrder = await db.transaction(async (tx) => {
const [order] = await tx
.insert(orders)
.values({ userId: user.id })
.returning();
await tx.insert(orderItems).values([
{ orderId: order.id, productId: starterPlanId, quantity: 1 },
{ orderId: order.id, productId: proPlanId, quantity: 2 },
]);
return order;
});
Eso implica escribir y mantener mucho más código. Si tu aplicación depende en gran medida de este tipo de patrón, el código adicional se acumula rápidamente. La versión de Prisma permanece más corta y comunica la intención de forma más directa. Si las mutaciones anidadas aparecen con frecuencia en tu modelo de dominio, optar por Prisma es una elección completamente razonable.
Decidir si hacer el cambio
Considere migrar a Drizzle si se aplica alguna de estas situaciones: está implementando su aplicación en plataformas serverless o edge donde el tamaño del paquete y la latencia de inicio tienen un impacto directo en los costos o en la experiencia del usuario; escribe con frecuencia consultas SQL analíticas complejas y desea acceder a ellas directamente; su equipo domina lo suficiente el SQL como para que una capa de abstracción no aporte mucho valor; o desea que las migraciones se expresen como archivos SQL simples que cualquier ingeniero, independientemente de su familiaridad con ORM, pueda leer y revisar.
Manténgase con Prisma si: está trabajando en un prototipo o en una herramienta interna donde la rapidez de entrega es más importante que reducir milisegundos en el tiempo de ejecución; su aplicación realiza muchas operaciones de escritura y lectura con relaciones anidadas profundas; parte de su equipo no se siente cómodo con SQL puro y se beneficia del estilo de modelado declarativo de Prisma; o ya ha migrado a Prisma 7, donde el problema histórico del tamaño del paquete es mucho menos grave.
También es importante el tiempo de ejecución en este caso. Prisma 7, lanzado a finales de 2025, eliminó por completo el motor Rust. Su tamaño actual es de alrededor de 1,6 MB; sigue siendo más de 200 veces mayor que el tamaño de Drizzle, pero está lejos de los 14 MB del binario original que llevó a la gente a buscar alternativas. Si el tamaño del paquete era la única razón por la que considerabas hacer el cambio, vale la pena verificar si esa razón sigue siendo válida teniendo en cuenta cuánto ha disminuido el tamaño de Prisma.
Para el equipo responsable de esta migración, la respuesta seguía siendo sí. Tipos mejorados para las consultas directas y la ausencia de un paso de generación de código justificaban por sí solos el cambio, independientemente de cualquier ahorro en tamaño. Al mirar hacia atrás después de seis meses, la decisión sigue siendo acertada. No obstante, no es la respuesta universalmente correcta; cualquiera que afirme que un ORM es objetivamente superior para todos los casos de uso no está siendo honesto contigo.
Siguientes pasos si deseas explorarlo por tu cuenta
Si desea probar Drizzle con una base de datos que ya posee, sin comprometerse a una reescritura completa, la herramienta de introspección del Drizzle Kit permite que drizzle-kit pull lea su esquema existente y genere automáticamente las definiciones correspondientes en TypeScript. Esto le proporciona una base funcional en lugar de tener que escribir manualmente cada definición de tabla desde cero.
La documentación oficial de Drizzle ORM explica en detalle el generador de consultas relacionales, el operador de plantillas sql y el conjunto completo de comandos de migración. También vale la pena leer la página de comparación de Prisma con Drizzle, disponible en este enlace; ofrece una visión equilibrada de los compromisos desde su punto de vista, con mayor sinceridad que la mayoría de las comparaciones entre proveedores.