Inicio / Artículos

SQL sin procesar, Prisma o Drizzle: elegir una capa de base de datos para seguridad de tipos y control

En qué se diferencian SQL puro, Prisma y Drizzle en términos de control, seguridad de tipos y experiencia del desarrollador en PostgreSQL, y una forma práctica de elegir uno para una aplicación en TypeScript.

Editor: REACTAPP.TOP15 min de lectura3403 palabras

Fuentes: Prisma vs Drizzle vs Raw SQL: What Every Backend Developer Should Understand Before Choosing an ORM

La mayor parte del trabajo en el backend comienza con obtener una API que responda. Se configuran rutas, se añade uno o dos controladores, se integra la autenticación y algunos middleware, y por un tiempo eso es todo. Luego surge una pregunta más difícil: ¿cómo debe comunicarse realmente la aplicación con su base de datos? De repente aparecen muchos términos (SQL puro, controladores de conexión, ORM, generadores de consultas, Prisma, Drizzle) y opiniones contundentes por todas partes. Algunos ingenieros no tocarían un ORM. Otros dicen que Prisma les ahorró semanas de trabajo. Un tercer grupo elige Drizzle porque mantiene el SQL visible en lugar de ocultarlo. Las secciones siguientes desarrollan el tema desde los principios básicos, de modo que elegir entre SQL puro, Prisma y Drizzle para una arquitectura basada en TypeScript y PostgreSQL se convierte en una decisión técnica relacionada con el control y la seguridad de tipos, y no con modas.

Cómo una solicitud llega a la base de datos

Antes de que el nombre de cualquier herramienta tenga algún significado, es útil seguir cómo una solicitud recorre un backend típico. Alguien visita su sitio y hace clic en un control como “mostrar mi perfil”. Esa acción comienza en el navegador y avanza a través de su API, hacia la capa de base de datos, y finalmente hasta la propia base de datos, antes de que una respuesta realice el viaje de regreso.

Browser
   |
   vExpress API   |
   vDatabase Layer   |
   vPostgreSQL Database   |
   vResponse back to the user

La capa de base de datos en el medio es donde se concentra la confusión. Tu aplicación necesita algún mecanismo para comunicarse con la base de datos, y existen tres métodos comunes: escribir SQL en bruto a mano, utilizar un ORM como Prisma o usar un constructor de consultas como Drizzle. Cada una de las secciones siguientes aborda uno de estos métodos por turnos.

Escribir SQL a mano

La ruta más directa es también la más antigua: escribes el SQL tú mismo. Imagina una tabla sencilla de usuarios con las columnas habituales.

users
id
name
email
password

Obtener un único usuario es entonces algo sumamente sencillo.

SELECT *
FROM users
WHERE id = 1;

Dado que la base de datos utiliza SQL como su lenguaje nativo, no se realiza ninguna traducción al ingresar o salir datos. En Node.js con PostgreSQL, el paquete pg es la forma habitual de enviar consultas directamente; se importa un pool de conexiones a partir de él.

import { Pool } from "pg";

Luego se configura el pool a partir de una variable de entorno y se ejecuta una consulta parametrizada contra él, leyendo las filas resultantes.

const pool = new Pool({
  connectionString: process.env.DATABASE_URL
});const result = await pool.query(
  "SELECT * FROM users WHERE id = $1",
  [1]
);console.log(result.rows);

Tu código envía SQL, PostgreSQL lo ejecuta y las filas regresan directamente, sin que nada se interponga en el proceso. Al escribir el SQL tú mismo, tienes un control total. Los informes complejos, por ejemplo, se expresan de forma directa, como al vincular a los usuarios con sus pedidos y agruparlos para contar los pedidos de cada usuario.

SELECT
users.name,
COUNT(orders.id)
FROM users
JOIN orders
ON users.id = orders.user_id
GROUP BY users.name;

Para consultas analíticas complejas como esa, el SQL escrito a mano es realmente difícil de superar. Escribirlo también te obliga a comprender las uniones, índices, planes de ejecución y el rendimiento en general, y ese conocimiento sigue siendo valioso sin importar qué herramienta adoptes más adelante. Además, no hay una capa adicional entre tu código y el motor.

Los inconvenientes aparecen cuando un proyecto pasa de tener unas pocas consultas. Imagina una base de código con cientos de ellas dispersas por todas partes, incluyendo muchas que se parecen a esta búsqueda por correo electrónico.

const result = await pool.query(
`
SELECT *
FROM users
WHERE email=$1
`,
[email]
);

Supongamos ahora que alguien cambia el nombre de una columna, convirtiendo email en email_address. Cualquier consulta que utilice esa columna dejará de funcionar sin que se note. La base de datos registra el cambio de inmediato, pero tu código en TypeScript no se da cuenta hasta que algo falla en tiempo de ejecución. Existe una versión más sutil de este mismo riesgo. Digamos que obtienes la primera fila.

const user = result.rows[0];

Luego lees una propiedad de él, con un error de escritura.

console.log(user.emial);

El error de escritura emial no genera ninguna queja por parte de JavaScript. Solo lo descubres cuando esa línea se ejecuta y provoca un problema ante el usuario. Esta misma categoría de problemas es la razón por la que existen los ORMs.

Qué es realmente un ORM

ORM es la abreviatura de Object Relational Mapping, lo cual suena más impresionante de lo que es en realidad. Simplemente significa que existe un puente entre tu lenguaje de programación y tu base de datos. En lugar de escribir una consulta de esta manera a mano,

SELECT *
FROM users
WHERE id=1;

Escribes algo más parecido a una llamada a método en un objeto.

user.findUnique({
 id: 1
})

El ORM toma eso y genera el SQL real en segundo plano, de modo que tu código pasa por el ORM hasta llegar a una consulta generada y, posteriormente, a la base de datos.

Your TypeScript code
        |
        vORM        |
        vSQL query        |
        vDatabase

Los equipos recurren a los ORMs porque las aplicaciones modernas buscan algo más que simplemente una forma de enviar consultas. Desean seguridad en los tipos, migraciones reales, un manejo de esquemas que no dependa de recordar cada tabla, y mucho menos código genérico repetitivo. En el mundo actual de TypeScript, los nombres que más se mencionan son Prisma, Drizzle, TypeORM y Sequelize. La atención aquí se centra en SQL puro, Prisma y Drizzle, ya que estos tres abarcan lo que realmente consideran la mayoría de los backends modernos basados en TypeScript.

Conociendo Prisma

Prisma se encuentra entre los ORM más discutidos en el ecosistema de TypeScript y suele ser el primer nombre que se menciona en cualquier conversación sobre servidores backend modernos de Node.js. En esencia, permite que una aplicación en TypeScript se comunique con una base de datos mediante TypeScript convencional, en lugar de tener SQL por todas partes, y es compatible con PostgreSQL, MySQL, SQLite, SQL Server y MongoDB. Usted escribe en TypeScript, y Prisma lo convierte en consultas.

TypeScript code
        |
        vPrisma Client        |
        vSQL queries        |
        vDatabase

Una configuración típica de Express, Prisma y PostgreSQL está organizada de manera clara: las solicitudes entran a través de la API, pasan por una capa de servicios y llegan a la base de datos mediante Prisma Client.

Client
  |
  vExpress API  |
  vService Layer  |
  vPrisma Client  |
  vPostgreSQL Database

Prisma Client es el componente que realiza el trabajo real: el verdadero puente entre tu código de backend y la base de datos que lo sustenta. Todo comienza con un único archivo, schema.prisma, que describe toda la estructura de la base de datos en un solo lugar. Un modelo de usuario, por ejemplo, declara cuatro columnas con tipo definido.

model User {
  id       Int    @id @default(autoincrement())
  name     String
  email    String @unique
  password String
}

Ese bloque indica a Prisma que cree una tabla User con esas columnas, y Prisma entiende perfectamente lo que eso implica. Mientras que en SQL puro se necesitaría una instrucción explícita para crear la misma tabla,

CREATE TABLE users (
 id SERIAL PRIMARY KEY,
 name TEXT,
 email TEXT UNIQUE,
 password TEXT
);

Prisma solo necesita el bloque de esquema anterior y se encarga de traducirlo en cambios reales en la base de datos a través de una migración. Una migración es, en esencia, un historial registrado de cómo ha cambiado la base de datos con el tiempo. Supongamos que su tabla de usuarios comienza siendo mínima.

User
id
name
email

Meses después, deseas agregar un número de teléfono. En lugar de modificar la tabla a mano, ejecutas un único comando.

npx prisma migrate dev --name add_phone_number

Prisma genera un archivo de migración para registrar ese cambio, de modo que la carpeta pasa de contener una migración de configuración inicial a otra que agrega el número de teléfono; además, todo el historial se mantiene versionado en lugar de quedar en la memoria de alguien. Una vez definido el esquema, Prisma genera un cliente que se utiliza en el código, por ejemplo para obtener un usuario por su ID.

const user = await prisma.user.findUnique({
  where:{
    id:1
  }
});

Detrás de escena, donde se convierte silenciosamente en SQL común.

SELECT *
FROM users
WHERE id=1;

Nunca fue necesario escribir el SQL por uno mismo. Las operaciones cotidianas también se corresponden de manera intuitiva: un inserto se convierte en una llamada a create, obtener todo se convierte en una llamada a find-many, actualizar un registro se convierte en una llamada a update con una cláusula where, y eliminar se convierte en una llamada a delete. Donde Prisma gana realmente su reputación es en cuanto a la seguridad de tipos. Si su esquema define un User con un id, un nombre y un correo electrónico, entonces las consultas ofrecen autocompletado real para cada campo.

const user = await prisma.user.findMany();
user[0].email;

Escribe ese código como user[0].emali y Prisma, junto con TypeScript, lo detectan antes de que la aplicación se ejecute, lo que ahorra una cantidad sorprendente de tiempo en la depuración con el paso del tiempo. Por supuesto, las aplicaciones reales rara vez tienen una sola tabla. Supongamos que un usuario posee muchos artículos; entonces existen tablas separadas para los usuarios y para los artículos, relacionadas entre sí mediante un id de usuario.

User
id
namePostid
title
userId

Prisma expresa esa relación directamente en el esquema, tanto en el modelo de usuario como en otros elementos.

model User {
 id Int @id @default(autoincrement())

y el modelo de publicación, que hace referencia a su usuario a través de una relación.

 name String posts Post[]
}model Post { id Int @id @default(autoincrement()) title String userId Int user User @relation(
 fields:[userId],
 references:[id]
 )}

Obtener a un usuario junto con sus publicaciones se convierte entonces en una única llamada legible que incluye los registros relacionados.

const user = await prisma.user.findUnique({
 where:{
  id:1
 },
 include:{
  posts:true
 }
});

Eso devuelve un objeto de usuario con un array anidado de publicaciones, exactamente la estructura que se desea en el código de la aplicación. Las fortalezas de Prisma radican en lo natural que resulta la experiencia del desarrollador, en la sólida seguridad tipológica que ofrece en los proyectos de TypeScript, en la organización de las migraciones, en la calidad de su documentación y en la rapidez con la que todo un equipo puede leer el esquema y comprender la estructura de la base de datos.

No está exento de compromisos. La generación de informes verdaderamente complejos puede resultar incómoda en comparación con SQL escrito a mano. Dado que Prisma oculta el SQL de manera tan eficaz, los desarrolladores que solo aprenden Prisma a veces tienen dificultades más adelante cuando necesitan optimizar a nivel de base de datos. Además, técnicamente hay un paso adicional en la ruta de la solicitud, desde la aplicación hasta Prisma, luego hasta SQL y finalmente hasta la base de datos, en lugar de la ruta más corta de aplicación a SQL a base de datos. Para la mayoría de las aplicaciones, ese nivel adicional no representa un problema, pero vale la pena saber que existe. Prisma suele ser la mejor opción cuando se están creando APIs en TypeScript, se desea un desarrollo rápido, se necesitan principalmente operaciones estándar de creación, lectura, actualización y eliminación, y se presta mucha atención a la experiencia del desarrollador; por eso muchas startups y productos SaaS recurren a él.

Dónde encaja Drizzle

Drizzle es otro ORM que ha ganado verdadero impulso en TypeScript, y para comprenderlo primero hay que reconocer que Prisma y Drizzle se basan en filosofías realmente diferentes. Prisma intenta mantener a los desarrolladores alejados de la complejidad de las bases de datos en la medida de lo posible. Drizzle adopta una postura opuesta: mantiene a los desarrolladores cerca de SQL y simplemente añade la seguridad que ofrece TypeScript por encima. Ambos buscan el mismo objetivo, una mejor experiencia al trabajar con las bases de datos, pero por caminos muy distintos. Drizzle es un ORM ligero para TypeScript destinado a PostgreSQL, MySQL y SQLite, enfocado en la seguridad de tipos, el rendimiento, la cercanía a SQL y un diseño arquitectónico sencillo. Tú sigues escribiendo código en TypeScript que pasa a través de Drizzle para convertirse en una consulta y llegar a la base de datos.

TypeScript code
        |
        vDrizzle ORM        |
        vSQL query        |
        vDatabase

Al colocarlos uno al lado del otro, la brecha filosófica se vuelve evidente. La forma de recuperar a todos los usuarios en Prisma es la que se muestra a continuación, y el SQL que genera en el fondo rara vez se tiene en cuenta.

const user = await prisma.user.findMany();

Drizzle solicita algo que se asemeje mucho más al SQL real.

const users = await db
.select()
.from(usersTable);

Prácticamente se puede leer el comportamiento a nivel de base de datos directamente en el código. Los esquemas también difieren. Prisma los almacena en un archivo dedicado schema.prisma, mientras que Drizzle declara el esquema en TypeScript puro, primero importando los constructores de columnas que necesita.

import {
 pgTable,
 serial,
 varchar
} from "drizzle-orm/pg-core";

y declarar la tabla con columnas de tipo definido.

export const users = pgTable("users", { id: serial("id").primaryKey(), name: varchar("name"), email: varchar("email")});

Por lo tanto, la estructura de su base de datos se encuentra directamente dentro del códigobase de TypeScript, y no hay ningún dialecto de esquema adicional que tener en cuenta. Las operaciones diarias se asemejan a consultas SQL vestidas con la estructura de TypeScript: un inserción es una llamada de inserción que lleva valores, leer todo consiste en realizar un select desde la tabla, y el filtrado añade una cláusula where construida con un ayudante de igualdad. La gente elige Drizzle por varias razones. Es realmente ligero y presenta poca abstracción. Dado que se mantiene cercano a SQL, sus habilidades en SQL siguen perfeccionándose en lugar de desvanecerse tras una capa de abstracción. Las consultas que genera suelen ser predecibles, ya que hay menos elementos intermedios entre su intención y el resultado. Además, fue diseñado teniendo en cuenta entornos modernos de ejecución, incluidos los entornos edge y serverless.

Por otro lado, si nunca has aprendido realmente SQL, Drizzle puede resultar más confuso al principio que Prisma, que generalmente es una entrada más sencilla para los principiantes. Utilizar bien Drizzle realmente requiere comprender las uniones, las relaciones, los índices y cómo se comportan las consultas, sin ocultar ningún aspecto de ello. Aunque su ecosistema, documentación y comunidad son sólidos, siguen siendo más pequeños que el entorno más maduro de Prisma. La forma más clara de resumir la diferencia es esta: con Prisma tratas la base de datos como una colección de objetos y el énfasis está en la experiencia del desarrollador, mientras que con Drizzle tratas la base de datos más bien como el propio SQL y el énfasis se desplaza hacia el rendimiento y el control.

Los tres enfoques uno al lado del otro

Al considerar los tres, sus arquitecturas se presentan como un espectro que muestra cuántas capas hay entre la aplicación y la base de datos.

Raw SQL:      Application → SQL Query → Database
Prisma:       Application → Prisma Client → SQL → Database
Drizzle:      Application → Drizzle → SQL → Database

Considere una operación sencilla: encontrar a un usuario por correo electrónico en una tabla básica de usuarios. En SQL puro, se envía directamente una consulta parametrizada.

const result = await pool.query(
`
SELECT *
FROM users
WHERE email=$1
`,
["rahul@test.com"]
);

En Prisma se realiza una búsqueda única,

const user =
await prisma.user.findUnique({

pasar el correo electrónico en una cláusula where.

where:{
 <email:"rahul@test.com>"
}});

Y en Drizzle se crea un select restringido por una igualdad en la columna de correo electrónico.

const user =
await db
.select()
.from(users)
.where(
 eq(
 users.email,
 "rahul@test.com"
 )
);

Al final, los tres devuelven los mismos datos. Lo que difiere es el grado de control que se desea, el nivel de abstracción con el que uno se siente cómodo y cuánto SQL ya se conoce. Un error común entre los principiantes es centrarse en determinar cuál es el más rápido, cuando en los proyectos reales la velocidad rara vez es el factor decisivo. Las preguntas más relevantes son con qué herramienta ya se siente cómodo el equipo, cuán complejo es realmente el proyecto, cuán sólidos son los conocimientos de base de datos de cada uno y cuán complicadas podrían volverse las consultas. Una aplicación SaaS sencilla suele funcionar bien con Prisma por sí solo, mientras que en las aplicaciones con gran uso de bases de datos es donde Drizzle, o SQL puro en ciertas partes, comienza a ser realmente útil.

En resumen, en términos sencillos: la curva de aprendizaje es más difícil con SQL puro, más fácil con Prisma y moderada con Drizzle. El control sobre SQL es mayor con SQL puro, alto con Drizzle y más bajo con Prisma. La seguridad de tipos es débil con SQL puro, pero excelente tanto con Prisma como con Drizzle. El rendimiento bruto es excelente con SQL puro y Drizzle, y bueno con Prisma. La experiencia del desarrollador es mejor con Prisma, buena con Drizzle y mediocre con SQL puro. Las consultas complejas favorecen a SQL puro y Drizzle, mientras que Prisma se sitúa en un nivel intermedio. En cuanto a la facilidad de uso para principiantes, Prisma lidera, Drizzle está en el medio y SQL puro queda atrás.

Entonces, ¿qué deberías elegir realmente?

¿Debería un ORM ser siempre la opción por defecto? Honestamente, no. Al igual que cualquier otra herramienta, un ORM es adecuado para algunas tareas y no para otras, y nada sirve para todos los proyectos. El SQL puro tiene su lugar cuando se necesita el máximo control, cuando la aplicación depende en gran medida de la base de datos, cuando las consultas se vuelven complejas o cuando el rendimiento simplemente no puede verse afectado. Las plataformas de análisis, los sistemas de informes y las aplicaciones financieras son ejemplos adecuados, al igual que cualquier sistema que maneje volúmenes significativos de datos. Un informe que responda a preguntas como cuánto ha comprado cada cliente en cinco años, junto con su valor promedio de pedido, es naturalmente más fácil de elaborar directamente en SQL, seleccionando los agregados necesarios.

SELECT
customers.name,
COUNT(orders.id),
AVG(orders.amount)

y uniendo y agrupando para crearlos.

FROM customersJOIN ordersON customers.id = orders.customer_idGROUP BY customers.name;

Para ese tipo de tareas, realmente nada puede reemplazar lo que SQL puede expresar. El problema es que depender de SQL escrito a mano para todo en una gran base de código se vuelve rápidamente inviable, dejando cientos de archivos de consultas dispersos y una seguridad en el tipo que uno mismo debe supervisar. Una visión ordenada por prioridades de el rendimiento de la API de Node.js muestra cuán a menudo es la base de datos, y no el framework, donde se encuentran las verdaderas ganancias y pérdidas.

Prisma suele ser la mejor opción cuando se está desarrollando un backend en TypeScript, realizando principalmente operaciones estándar de creación, lectura, actualización y eliminación, dando prioridad a la velocidad de desarrollo y coordinando a varios desarrolladores en una misma base de código. Esto se aplica a productos SaaS, paneles de administración, APIs de tiendas en línea y sistemas de gestión de contenido, donde las tareas diarias consisten en crear usuarios, actualizar perfiles, realizar pedidos o obtener datos del panel de control. Prisma realmente aumenta la productividad al transformar una consulta básica en una llamada limpia y legible que cualquier colega puede comprender al instante. Drizzle se impone cuando se trabaja con TypeScript, ya se conoce SQL de manera razonable, se presta atención al rendimiento y al control, y se busca algo ligero en lugar de pesado. Atrae a desarrolladores que desean la seguridad que ofrece un ORM sin sentirse demasiado alejados de la base de datos.

En la práctica, muchos proyectos reales no se adhieren a un único enfoque. Una startup podría razonablemente comenzar con un camino directo desde Express, pasando por Prisma, hasta PostgreSQL.

Express → Prisma → PostgreSQL

Es un primer paso realmente sensato. Más adelante, cuando el producto crece y surgen necesidades de análisis o generación de informes más complejas, los equipos suelen añadir consultas SQL en bruto en ciertas partes del código en lugar de eliminar Prisma, lo que resulta en una aplicación que accede a PostgreSQL tanto a través de Prisma como de consultas escritas a mano.

Application
├── Prisma
└── Raw SQL Queries
        ↓
   PostgreSQL

Utilizar un mismo enfoque en todas partes no es una regla que deba seguirse, y la mayoría de los repositorios de código reales terminan combinándolos. Las startups suelen recurrir a TypeScript, Prisma y PostgreSQL únicamente por velocidad. Los proyectos más recientes en TypeScript optan cada vez más por Drizzle y su estilo ligero y compatible con SQL. Además, los sistemas más grandes combinan con frecuencia un ORM con SQL puro y optimización especializada de bases de datos, simplemente porque las diferentes partes de un sistema grande enfrentan problemas distintos.

Errores que vale la pena evitar

Algunos errores se repiten con frecuencia. El primero es tratar al ORM como si fuera la base de datos. Prisma no es una base de datos, al igual que Drizzle; son herramientas para acceder a una, ya sea PostgreSQL, MySQL o MongoDB en su interior. El segundo es omitir por completo SQL y recurrir directamente a Prisma. Al principio parece eficiente, pero suele causar problemas cuando las consultas lentas dejan de tener explicación, las uniones resultan innecesariamente complejas y el ajuste de rendimiento se vuelve realmente difícil por la falta de una base sólida bajo la abstracción. Un ORM nunca fue diseñado para reemplazar a SQL; funciona sobre él. El tercer error es priorizar por encima de todo las cifras de rendimiento bruto. La productividad del desarrollador es igualmente importante en el mundo real, y reducir cinco milisegundos de una consulta mientras se ralentiza el desarrollo en tres meses rara vez es una decisión inteligente.

Un camino de aprendizaje que rinde frutos

Partiendo de cero hoy, un orden sensato es comenzar con los fundamentos de SQL, incluyendo tablas, claves primarias, claves foráneas, uniones, índices y transacciones, utilizando algo como PostgreSQL. Luego, pase tiempo conectándose directamente con un controlador como el paquete pg de Node, para que entienda cómo una aplicación se comunica con una base de datos sin nada oculto en el medio. A partir de ahí, pase a Prisma y familiarícese con los esquemas, las migraciones, las relaciones y su seguridad tipológica. Finalmente, pruebe Drizzle adecuadamente para que sienta cómo es un constructor de consultas basado en SQL, junto con los compromisos de rendimiento que conlleva.

Donde lo deja eso

El propósito de un ORM nunca fue reemplazar a SQL. Existe para mejorar la experiencia del desarrollador, y nada es más importante que eso. Un desarrollador de backend verdaderamente competente no se define por conocer bien Prisma o Drizzle. Es alguien que comprende cómo funciona realmente una base de datos, puede escribir SQL cuando es necesario, sabe qué es lo que un ORM simplifica en su lugar y, lo que es igualmente importante, sabe cuándo tiene sentido utilizar un ORM y cuándo es más inteligente recurrir a SQL escrito a mano. Una vez que se logra esa claridad, elegir entre estas herramientas deja de ser una suposición. Se empieza a decidir según las necesidades reales del proyecto, y no según lo que esté de moda ese mes.

Leer más

  • Reducir la carga de Prisma y PostgreSQL antes de pagar por una base de datos más grande: Quince técnicas prácticas, desde EXPLAIN ANALYZE e índices compuestos hasta soluciones para el problema N+1, contadores, pooling y vacuum, para disminuir las operaciones de la base de datos Prisma.