Saltar al contenido

Lección 15 de 17 · Lectura y práctica

Desarrollar Endpoints con Queries Optimizadas

Tipo de lección: VideoConcepto claveLas queries optimizadas en Prisma son consultas a la base de datos que maximizan el rendimiento y minimizan el consumo de recursos. Piensa en ellas como la diferencia entre pedir un café en un.

Del concepto a la práctica

Taller de esta lección

Un endpoint debe validar parámetros antes de consultar y construir una respuesta limitada al contrato elegido. Para GET /rooms/:id/reservations, valida el identificador, filtra por sala y devuelve fecha, estado y nombre del usuario, no el modelo completo.

Ejemplo resuelto

js
async function listarReservas(req,res) {
const roomId = Number(req.params.id);
if (!Number.isInteger(roomId) || roomId <= 0) return res.status(400).json({ error: "roomId inválido" });
const data = await db.reservation.findMany({
  where: { roomId, status: "CONFIRMED" },
  select: { startsAt: true, endsAt: true, user: { select: { name: true } } },
  orderBy: [{ startsAt: "asc" }, { id: "asc" }], take: 20
});

return res.json(data);
}

Valida cadena de dígitos y Number.isSafeInteger(roomId)>0; Number admite otras representaciones que quizá no quieras aceptar. Antes de consultar comprueba identidad y permiso sobre la sala, ya que parametrizar no autoriza datos. Este endpoint no muestra status pese a la descripción: añádelo al select si lo necesita el contrato. Fixture pararoom2: reservas10/11/12 CONFIRMED a10/11/12h y13 PENDING a13h el2026-01-02, cada una dura1h con usuario existente; salida3 confirmadas. Una sala válida sin reservas devuelve[]; una sala inexistente requiere la política404 explícita.

Pruébalo

  1. Valida ruta y parámetros de página.

  2. Define filtro y campos de salida.

  3. Añade orden y límite.

  4. Prueba entrada inválida y válida.

Comprueba tu resultado

/rooms/2/reservations devuelve como máximo 20 reservas confirmadas; /rooms/x/reservations responde 400 sin consulta.

Error frecuente: interpolar texto sin validar. Corrígelo convirtiendo y comprobando el entero.

Ejercicio: con 3 confirmadas y 1 pendiente para la sala 2, el resultado esperado contiene 3 filas.

Tablas de clientes y pedidos: INNER JOIN conserva coincidencias y LEFT JOIN conserva también a Luis sin pedidos.
CursaloMapa de práctica
Relacionar tablas sin perder filas

Una clave conecta cada pedido con su cliente. INNER JOIN devuelve las coincidencias y puede producir varias filas por cliente. LEFT JOIN conserva también los clientes sin pedido y representa los campos ausentes con NULL.

Ver la infografía en tamaño completo ↗

Tipo de lección: Video

Concepto clave

Las queries optimizadas en Prisma son consultas a la base de datos que maximizan el rendimiento y minimizan el consumo de recursos. Piensa en ellas como la diferencia entre pedir un café en un bar: una query no optimizada es como pedir "un café" y esperar que el barista adivine todo (tipo, tamaño, extras), mientras que una optimizada es dar instrucciones precisas como "café americano mediano, sin azúcar". En producción, cada milisegundo cuenta y cada conexión a la base de datos tiene un costo.

La optimización se enfoca en tres pilares: selectividad (traer solo los datos necesarios), relaciones eficientes (evitar el problema N+1) y uso de índices (acelerar búsquedas). Una query mal diseñada puede funcionar en desarrollo con 100 registros, pero colapsar en producción con millones. La clave es pensar como un arquitecto de bases de datos, no solo como un desarrollador de API.

Cómo funciona en la práctica

Imagina que desarrollas una API para un e-commerce. Necesitas un endpoint que devuelva los últimos 10 pedidos con sus productos y el cliente. El enfoque naive seria: 1) Obtener los pedidos, 2) Para cada pedido, hacer una query para sus productos, 3) Para cada pedido, otra query para el cliente. Esto genera 1 + 10 + 10 = 21 queries (problema N+1).

El enfoque optimizado usa include con selectividad y paginacion:

  1. Definir exactamente que campos necesitas de cada modelo
  2. Usar include para traer relaciones en una sola query
  3. Aplicar take y skip para paginacion
  4. Ordenar por un campo indexado como createdAt

Esto reduce las 21 queries a solo 1, con un impacto dramatico en el rendimiento.

Codigo en accion

Primero, veamos el antes (query no optimizada):

// ENDPOINT NO OPTIMIZADO - PROBLEMA N+1
app.get('/pedidos/lentos', async (req, res) => {
  const pedidos = await prisma.pedido.findMany({
    take: 10,
    orderBy: { createdAt: 'desc' }
  });
  
  // PROBLEMA: queries adicionales por cada pedido
  const pedidosConDetalles = await Promise.all(
    pedidos.map(async (pedido) => {
      const productos = await prisma.producto.findMany({
        where: { pedidoId: pedido.id }
      });
      const cliente = await prisma.cliente.findUnique({
        where: { id: pedido.clienteId }
      });
      return { ...pedido, productos, cliente };
    })
  );
  
  res.json(pedidosConDetalles);
});

Ahora el despues (query optimizada):

// ENDPOINT OPTIMIZADO - 1 QUERY
app.get('/pedidos/optimizados', async (req, res) => {
  const pedidos = await prisma.pedido.findMany({
    take: 10,
    skip: 0, // Para paginacion
    orderBy: { createdAt: 'desc' }, // Campo indexado
    select: {
      id: true,
      total: true,
      createdAt: true,
      // Solo campos necesarios
      cliente: {
        select: {
          id: true,
          nombre: true,
          email: true // No traer toda la info del cliente
        }
      },
      productos: {
        select: {
          id: true,
          nombre: true,
          precio: true,
          cantidad: true
        },
        where: { activo: true } // Filtro adicional
      }
    },
    where: {
      estado: 'COMPLETADO' // Filtro principal
    }
  });
  
  res.json(pedidos);
});

Errores comunes

  • Traer todos los campos: Usar select: true en lugar de select especifico. Solucion: Listar explicitamente los campos necesarios.
  • Olvidar los índices: Ordenar o filtrar por campos no indexados. Solucion: Verificar índices en el schema y agregarlos donde sea necesario.
  • N+1 en relaciones anidadas: Incluir relaciones pero no sus sub-relaciones. Solucion: Usar include anidado con selectividad.
  • Paginacion incompleta: Usar solo take sin skip para paginas siguientes. Solucion: Implementar logica completa de paginacion.
  • Filtros ineficientes: Usar OR con muchos condiciones o LIKE sin limites. Solucion: Revisar planes de ejecucion y simplificar condiciones.

Checklist de dominio

  1. ¿Uso select en lugar de include cuando solo necesito campos, no relaciones completas?
  2. ¿Verifico que los campos en orderBy y where principales tengan índices?
  3. ¿Evito el problema N+1 usando include para traer relaciones en una sola query?
  4. ¿Implemento paginacion con take y skip para limites de datos?
  5. ¿Uso where para filtrar antes de traer datos, no después en JavaScript?
  6. ¿Reviso el plan de ejecucion de queries complejas con prisma.$queryRaw y EXPLAIN?
  7. ¿Considero usar transacciones para operaciones multiples que deben ser atomicas?

Optimizar endpoint de usuarios con pedidos recientes

En este ejercicio, optimizaras un endpoint existente que tiene problemas de rendimiento. Trabajaras con un schema de Prisma para una aplicacion de pedidos.

  1. Contexto: Tienes un endpoint GET /usuarios/:id/pedidos-recientes que devuelve los ultimos 5 pedidos de un usuario, con los productos de cada pedido. Actualmente hace 1 + 5 + (5 * N) queries (donde N es productos por pedido).
  2. Schema relevante:
model Usuario {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  nombre    String
  pedidos   Pedido[]
}

model Pedido {
  id         Int       @id @default(autoincrement())
  createdAt  DateTime  @default(now())
  usuarioId  Int
  usuario    Usuario   @relation(fields: [usuarioId], references: [id])
  productos  ProductoPedido[]
}

model ProductoPedido {
  id        Int     @id @default(autoincrement())
  pedidoId  Int
  productoId Int
  cantidad  Int
  pedido    Pedido   @relation(fields: [pedidoId], references: [id])
  producto  Producto @relation(fields: [productoId], references: [id])
}

model Producto {
  id     Int     @id @default(autoincrement())
  nombre String
  precio Float
  productoPedidos ProductoPedido[]
}
  1. Codigo actual no optimizado:
app.get('/usuarios/:id/pedidos-recientes', async (req, res) => {
  const usuarioId = parseInt(req.params.id);
  
  // 1. Obtener usuario
  const usuario = await prisma.usuario.findUnique({
    where: { id: usuarioId }
  });
  
  // 2. Obtener ultimos 5 pedidos
  const pedidos = await prisma.pedido.findMany({
    where: { usuarioId: usuarioId },
    take: 5,
    orderBy: { createdAt: 'desc' }
  });
  
  // 3. Para cada pedido, obtener productos (PROBLEMA N+1)
  const pedidosConProductos = await Promise.all(
    pedidos.map(async (pedido) => {
      const productosPedido = await prisma.productoPedido.findMany({
        where: { pedidoId: pedido.id },
        include: { producto: true }
      });
      return {
        ...pedido,
        productos: productosPedido.map(pp => ({
          ...pp.producto,
          cantidad: pp.cantidad
        }))
      };
    })
  );
  
  res.json({
    usuario: { id: usuario.id, nombre: usuario.nombre },
    pedidos: pedidosConProductos
  });
});
  1. Tu tarea: Refactoriza este endpoint para que use maximo 2 queries en total. Aplica:
    • Selectividad: Trae solo los campos necesarios
    • Include anidado: Para relaciones usuario-pedidos-productos
    • Paginacion: Manten take: 5
    • Filtros: Asegurate de que createdAt este indexado
  2. Entrega: Proporciona el codigo completo del endpoint optimizado. Incluye comentarios explicando cada decision de optimizacion.
Pistas
  • Recuerda que puedes usar include anidado: usuario → pedidos → productosPedido → producto
  • Considera usar select dentro de include para limitar los campos de producto
  • Piensa si realmente necesitas todos los campos de ProductoPedido o solo algunos

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.

Sigue con Prisma ORM Profesional: Schemas, Migraciones y Queries Optimizadas para Producción

Esta lección forma parte de un curso de Cursalo. Revisa su programa y elige entre la compra individual o el acceso a toda la escuela.

Si ya tenías acceso a este curso, entra con tu cuenta: tus derechos y tu progreso se conservan. No necesitas comprarlo de nuevo.

01 / ELEGIR UNA RUTA

El próximo paso

Aprende IA mientras construyes algo que sí necesitas.

Empieza por el curso inicial o explora el catálogo para encontrar una práctica alineada con tu trabajo.