martes, 21 de octubre de 2025

Paginación para APIs con Eloquent y query builder

 ¿Cómo se pagina el resultado de una API?

La paginación es un mecanismo fundamental para manejar grandes conjuntos de datos devueltos por una API, mejorando el rendimiento y la experiencia del usuario. En lugar de enviar 1 millón de registros de golpe (lo cual colapsaría la aplicación), el servidor envía el resultado en trozos o "páginas".

Existen principalmente tres estrategias para implementar la paginación con una API. La elección depende de cómo está diseñado el backend.


1. Paginación Basada en Desplazamiento (Offset/Limit)

Esta es la forma más común y la más fácil de implementar. Se basa en indicar al servidor cuántos elementos saltar (offset) y cuántos devolver (limit).

¿Cómo Funciona?

El cliente (tu aplicación React) envía dos parámetros a la API:

  1. limit (Límite/Tamaño de Página): Cuántos ítems quieres por página (ej: 25).

  2. offset (Desplazamiento) o page (Página):

    • Si usas offset: Cuántos ítems ignorar (ej: para la página 3 con un límite de 25, el offset es $2 \times 25 = 50$).

    • Si usas page: El número de página solicitado (ej: 3).

Ejemplo de Solicitud

PáginaParámetros (usando offset)Parámetros (usando page)
Página 1/api/productos?limit=25&offset=0/api/productos?page=1&limit=25
Página 2/api/productos?limit=25&offset=25/api/productos?page=2&limit=25

✅ Ventajas:

  • Fácil Navegación: Permite saltar directamente a cualquier página (ej: ir de la página 1 a la 10).

  • Contadores Sencillos: Es fácil mostrar "Página 3 de 10".

❌ Desventajas:

  • Rendimiento en Tablas Grandes: A medida que el offset crece (ej: offset=100000), la base de datos se ralentiza porque tiene que escanear y descartar muchos registros primero.

  • Inconsistencia: Si un usuario elimina un registro en la página 2 mientras tú estás en la página 3, tu página 3 mostrará un registro duplicado o perderá un registro.


2. Paginación Basada en el Cursor (Keyset/Token)

Esta es la forma más moderna y eficiente para grandes volúmenes de datos. En lugar de usar números de página, usa un valor de un campo específico (el "cursor") para indicar dónde continuar la búsqueda.

¿Cómo Funciona?

  1. El cliente solicita la primera página.

  2. El servidor devuelve los datos más un token o un valor clave que identifica el último registro de esa página (el "cursor").

  3. Para obtener la siguiente página, el cliente envía este cursor al servidor (ej: after_id=150).

  4. El servidor busca todos los registros que vienen después del registro con ID 150.

Ejemplo de Solicitud

AcciónParámetrosNota
Página 1/api/productos?limit=50Devuelve el last_id = 150
Página 2/api/productos?limit=50&after_id=150El cursor le dice al servidor dónde empezar.

✅ Ventajas:

  • Rendimiento Superior: El servidor utiliza índices de la base de datos directamente, haciendo que las consultas sean súper rápidas, independientemente del tamaño total de la tabla.

  • Datos Consistentes: El flujo de datos es más estable porque los registros eliminados o añadidos no afectan al puntero (cursor).

❌ Desventajas:

  • Sin Saltos Directos: Solo puedes avanzar o retroceder de forma secuencial. No puedes saltar directamente a la página 10.

  • Sin Contadores Fáciles: Es difícil saber el número total de páginas (Página X de Y).


3. Paginación Infinita (Infinite Scroll)

Esta es una técnica de presentación híbrida que combina Cursor Paging con la experiencia del usuario. En lugar de botones "Página Siguiente", el contenido se carga automáticamente cuando el usuario se desplaza hasta el final de la página.

¿Cómo Funciona?

Se utiliza la paginación basada en Cursor (o a veces Offset) en el backend, pero el frontend usa un hook de React como useInView (o librerías como react-query o swr con su modo de paginación infinita) para detectar cuándo el usuario ve el último elemento de la lista y, en ese momento, dispara la solicitud para la siguiente página.

✅ Ventajas:

  • Experiencia Fluida: Muy popular en aplicaciones sociales (como Instagram o Twitter) ya que no requiere clics.

  • Simple para el Usuario: El usuario solo se enfoca en el contenido.

❌ Desventajas:

  • Mala Navegación: El usuario no puede "marcar" dónde se detuvo y es difícil llegar al footer de la página.

  • Sobrecarga del DOM: Si el usuario se desplaza mucho, la aplicación puede volverse lenta ya que el DOM crece sin límites (se necesita virtualización).


¿Y en la capa de acceso a datos (Eloquent) qué hago?

¡Esa es una pregunta excelente! Una vez que decides la estrategia de paginación (Offset o Cursor), necesitas saber cómo implementarla en el servidor, y Eloquent ORM de Laravel hace este proceso notablemente sencillo.

Eloquent tiene métodos integrados para manejar ambas estrategias de paginación.


1. Paginación Basada en Desplazamiento (Offset/Limit)

Eloquent tiene dos métodos principales para esto, y el más usado ya está optimizado para generar los contadores y la información de la paginación.

A. Método Integrado: paginate() (Recomendado)

El método paginate() es la forma estándar de Laravel. No solo aplica el OFFSET y LIMIT necesarios a la consulta SQL, sino que también construye automáticamente el objeto JSON con metadatos como el total de ítems, el número de la última página, y los enlaces a la página siguiente/anterior.

PHP
// Ejemplo: Obtener 15 productos por página
public function index()
{
    // El '15' define el 'limit' (o tamaño de la página)
    $productos = Producto::where('activo', true)
        ->orderBy('nombre', 'asc')
        ->paginate(15);

    // Retorna un objeto JSON con metadatos:
    /*
    {
        "current_page": 1,
        "data": [...], // Los 15 productos
        "from": 1,
        "last_page": 7, // Calculado automáticamente
        "per_page": 15,
        "total": 100,
        ... y enlaces de navegación
    }
    */
    return response()->json($productos);
}

B. Método Manual: take() y skip()

Si tu frontend te envía explícitamente el limit y el offset, puedes mapearlos directamente a los métodos take() y skip().

PHP
// $limit = 25; // Viene de la request
// $offset = 50; // Viene de la request (para la página 3)

$productos = Producto::where('activo', true)
    ->take($limit) // take() es el LIMIT de SQL
    ->skip($offset) // skip() es el OFFSET de SQL
    ->get();

return response()->json($productos);
// Nota: Aquí pierdes los metadatos automáticos como 'total' y 'last_page'.

2. Paginación Basada en Cursor (Keyset/Token)

Este es el enfoque moderno de Laravel para grandes bases de datos. Evita completamente el problema de rendimiento del OFFSET en grandes tablas.

A. Método Integrado: cursorPaginate() (El Mejor para Rendimiento)

El método cursorPaginate() funciona de forma similar a paginate(), pero en lugar de usar un número de página, Laravel genera un token cifrado que apunta al último registro que se debe usar como cursor.

PHP
// Ejemplo: Obtener 20 pedidos por cursor
public function index()
{
    // Es crucial ordenar por la columna que usarás como cursor (ej: 'id')
    $pedidos = Pedido::orderBy('id', 'desc')
        ->cursorPaginate(20);

    /*
    La respuesta JSON contiene:
    "next_cursor": "eyJpZCI6MzgsIl9wb2ludHMiOiJjdXJzb3JQYWdpbmF0..."
    */
    return response()->json($pedidos);
}

💡 ¿Cómo lo usas en el Frontend?

  1. La primera solicitud no tiene cursor.

  2. Tu frontend recibe la respuesta, extrae el valor de next_cursor (el token).

  3. Para la siguiente página, haces la solicitud al servidor, pasando el cursor en la URL (p. ej., /api/pedidos?cursor=eyJ...). Laravel se encarga automáticamente de descifrar el token y continuar la consulta SQL desde el punto exacto.

Resumen de la Conveniencia con Eloquent

La mayor conveniencia es el método paginate(X) para la paginación Offset (ya que genera todos los metadatos de la paginación que la UI necesita, como el número total de páginas) y cursorPaginate(X) si la prioridad absoluta es el rendimiento en bases de datos masivas.


En el método con paginate de Eloquent ¿Cómo lleva Eloquent el tracking de la página actual?

Esa es la parte mágica y más útil del método paginate() de Eloquent: el framework se encarga automáticamente de obtener y rastrear la página actual al inspeccionar la solicitud HTTP entrante (Illuminate\Http\Request).

Aquí te explico exactamente cómo Laravel logra el tracking de la página actual y cómo funciona el proceso:


1. El Parámetro Estándar de la URL (page)

Por defecto, el método paginate() de Eloquent espera recibir el número de página actual a través de un parámetro de consulta llamado page en la URL.

Ejemplo de Flujo:

  1. El usuario hace clic en el enlace "Página 3" en tu frontend.

  2. El frontend genera una solicitud: /api/productos?page=3&limit=15.

  3. Laravel recibe la solicitud.

2. Acceso al Request y Cálculo del Offset

Cuando llamas a Producto::paginate(15) dentro de tu controller, Eloquent hace lo siguiente internamente:

A. Inspecciona el Request

Laravel utiliza el Request global para buscar el valor del parámetro page.

PHP
// Lo que hace Eloquent internamente:
$pagina_actual = request()->query('page', 1); // Si 'page' no existe, asume que es la página 1.

B. Calcula el OFFSET

Una vez que Eloquent sabe que estás en la página 3 y el tamaño de la página (limit) es 15, calcula el OFFSET (los registros que debe saltar) usando la siguiente fórmula:


En el ejemplo de la página 3 con límite 15:

$$\text{OFFSET} = (3 - 1) \times 15 = 30$$

Esto resulta en la siguiente consulta SQL ejecutada por Eloquent:
SQL
SELECT * FROM productos WHERE activo = true ORDER BY nombre ASC LIMIT 15 OFFSET 30

C. Almacena la Información en el Objeto Paginator

El método paginate() no solo ejecuta la consulta; crea una instancia de la clase LengthAwarePaginator. Este objeto es el que lleva el tracking de toda la información:

  • current_page: 3 (El valor tomado del request).

  • per_page: 15 (El valor que le pasaste a paginate(15)).

  • last_page: 7 (Calculado mediante una segunda consulta SQL COUNT(*)).

  • next_page_url: El enlace a la página 4, con el parámetro page=4.


3. Sobrescribir el Parámetro (pageName)

Si tu frontend o alguna convención de API requiere que uses un nombre de parámetro diferente a page (por ejemplo, pagina o p), puedes indicárselo a Eloquent:

PHP
// Usando un nombre de parámetro diferente en la URL:
// URL esperada: /api/productos?pagina=3

$productos = Producto::where('activo', true)
    ->paginate(15, ['*'], 'pagina'); // El tercer argumento es el nombre del parámetro (pageName)

return response()->json($productos);

En resumen, Eloquent utiliza la solicitud HTTP entrante (el request) y el nombre de parámetro estándar page (que puedes personalizar) como su fuente de verdad para determinar dónde está el usuario y así calcular el OFFSET necesario para la consulta SQL.