Paginación
Paginación
Sección titulada «Paginación»Veloce-TS incluye un conjunto completo de helpers para paginación por offset (página/límite) y por cursor.
Tabla de Contenidos
Sección titulada «Tabla de Contenidos»- Paginación por Offset
- Paginación por Cursor
- Cursor Multi-Campo
- Metadatos de Paginación
- Mejores Prácticas
Paginación por Offset
Sección titulada «Paginación por Offset»La paginación por offset usa los parámetros page y limit para navegar por páginas de resultados.
Inicio Rápido con paginate()
Sección titulada «Inicio Rápido con paginate()»El helper standalone paginate() construye una respuesta { data, meta } en una sola llamada:
import { Controller, Get, Query, paginate } from 'veloce-ts';import { z } from 'zod';
const PaginacionSchema = z.object({ page: z.string().transform(Number).default('1'), limit: z.string().transform(Number).default('10'),});
@Controller('/productos')class ProductoController { @Get('/') async listar(@Query(PaginacionSchema) query: z.infer<typeof PaginacionSchema>) { const { page, limit } = query; const offset = (page - 1) * limit;
const [data, total] = await Promise.all([ db.select().from(productos).limit(limit).offset(offset), db.select({ count: sql<number>`count(*)` }).from(productos), ]);
return paginate(data, total[0].count, page, limit); }}Estructura de respuesta:
{ "data": [...], "meta": { "total": 150, "page": 2, "limit": 10, "totalPages": 15, "hasNext": true, "hasPrev": true, "from": 11, "to": 20 }}Usando PaginationHelper
Sección titulada «Usando PaginationHelper»Para más control, usa la clase PaginationHelper:
import { PaginationHelper } from 'veloce-ts';
@Get('/')async listar(@Query() query: any) { // Parsea page/limit del query con valores por defecto y límite máximo const { page, limit } = PaginationHelper.parsePaginationQuery( query, 10, // defaultLimit 100 // maxLimit );
const offset = (page - 1) * limit; const data = await db.select().from(productos).limit(limit).offset(offset); const total = await db.$count(productos);
return PaginationHelper.paginate(data, total, page, limit);}Paginación por Cursor
Sección titulada «Paginación por Cursor»La paginación por cursor es más eficiente para grandes conjuntos de datos y feeds en tiempo real. En lugar de un número de página, se usa un cursor opaco apuntando a un registro específico.
Inicio Rápido
Sección titulada «Inicio Rápido»import { Controller, Get, Query, createCursorPaginatedResult, parseCursorQuery } from 'veloce-ts';import { gt } from 'drizzle-orm';
@Controller('/publicaciones')class PublicacionController { @Get('/') async listar(@Query() query: any) { // Parsea cursor y limit del query de forma segura const { cursor, limit } = parseCursorQuery(query, 20, 100);
let filas; if (cursor) { const lastId = parseInt(atob(cursor)); filas = await db .select() .from(publicaciones) .where(gt(publicaciones.id, lastId)) .limit(limit + 1); // obtener uno extra para detectar hasNext } else { filas = await db .select() .from(publicaciones) .limit(limit + 1); }
return createCursorPaginatedResult(filas, limit, 'id'); }}Estructura de respuesta:
{ "data": [...], "meta": { "count": 20, "hasNext": true, "hasPrev": false, "nextCursor": "eyJpZCI6MjB9", "prevCursor": null }}Navegar con Cursores
Sección titulada «Navegar con Cursores»# Primera páginaGET /publicaciones
# Siguiente página (usa nextCursor de la respuesta anterior)GET /publicaciones?cursor=eyJpZCI6MjB9
# Página anteriorGET /publicaciones?cursor=eyJpZCI6MX0=&direction=prevAPI de createCursorPaginatedResult
Sección titulada «API de createCursorPaginatedResult»createCursorPaginatedResult( data: T[], limit: number, cursorField: keyof T, hadPrevCursor?: boolean // pasar true al navegar hacia adelante con cursor): CursorPaginatedResult<T>El parámetro hadPrevCursor (añadido en v0.4.0) establece correctamente hasPrev: true cuando no estás en la primera página:
const { cursor } = parseCursorQuery(query);
const resultado = createCursorPaginatedResult( filas, limit, 'id', !!cursor // hasPrev = true cuando se proporcionó un cursor);Cursor Multi-Campo
Sección titulada «Cursor Multi-Campo»Cuando ordenas por campos no únicos (p.ej. createdAt), necesitas un cursor compuesto usando múltiples campos para un ordenamiento estable.
Crear un Cursor Multi-Campo
Sección titulada «Crear un Cursor Multi-Campo»import { createMultiCursor, decodeMultiCursor } from 'veloce-ts';
@Get('/')async listar(@Query() query: any) { const { cursor, limit } = parseCursorQuery(query, 20);
let where; if (cursor) { // Decodifica cursor compuesto: { createdAt, id } const decoded = decodeMultiCursor(cursor); where = or( lt(publicaciones.createdAt, decoded.createdAt), and( eq(publicaciones.createdAt, decoded.createdAt), lt(publicaciones.id, decoded.id) ) ); }
const filas = await db .select() .from(publicaciones) .where(where) .orderBy(desc(publicaciones.createdAt), desc(publicaciones.id)) .limit(limit + 1);
const ultimoItem = filas[filas.length - 1]; const nextCursor = ultimoItem ? createMultiCursor(ultimoItem, ['createdAt', 'id']) : null;
return { data: filas.slice(0, limit), meta: { hasNext: filas.length > limit, nextCursor, count: Math.min(filas.length, limit), }, };}Metadatos de Paginación
Sección titulada «Metadatos de Paginación»Estructura de PaginationMeta (v0.4.0)
Sección titulada «Estructura de PaginationMeta (v0.4.0)»interface PaginationMeta { total: number; // total de elementos page: number; // página actual (base 1) limit: number; // elementos por página totalPages: number; // total de páginas hasNext: boolean; // true si hay página siguiente hasPrev: boolean; // true si hay página anterior from: number; // índice del primer elemento en esta página (base 1, p.ej. 11) to: number; // índice del último elemento en esta página (p.ej. 20)}Estructura de CursorPaginatedResult
Sección titulada «Estructura de CursorPaginatedResult»interface CursorPaginatedResult<T> { data: T[]; meta: { count: number; // elementos reales devueltos en esta página hasNext: boolean; hasPrev: boolean; nextCursor: string | null; prevCursor: string | null; };}Mejores Prácticas
Sección titulada «Mejores Prácticas»1. Siempre establece un límite máximo
Sección titulada «1. Siempre establece un límite máximo»Evita que los clientes soliciten miles de registros en una sola llamada:
const { page, limit } = PaginationHelper.parsePaginationQuery(query, 10, 100);// limit se limita a 100 aunque el cliente envíe limit=99992. Usa paginación por cursor para grandes conjuntos de datos
Sección titulada «2. Usa paginación por cursor para grandes conjuntos de datos»La paginación por offset se vuelve lenta con offsets grandes (OFFSET 50000). Usa cursor para feeds, timelines o tablas con millones de filas.
3. Ordena siempre por columnas indexadas para cursor
Sección titulada «3. Ordena siempre por columnas indexadas para cursor»La paginación por cursor solo funciona correctamente cuando se ordena por un campo indexado y estable. Usa id (clave primaria) o compuesto (createdAt, id).
4. Devuelve metadatos de paginación en todos los endpoints de lista
Sección titulada «4. Devuelve metadatos de paginación en todos los endpoints de lista»// ✓ Bueno - los clientes pueden construir UI de paginaciónreturn paginate(data, total, page, limit);
// ✗ Malo - los clientes no saben si hay más páginasreturn data;5. Documenta tus parámetros de paginación
Sección titulada «5. Documenta tus parámetros de paginación»@Get('/')@Summary('Listar productos con paginación')@Description('Soporta paginación por offset (?page=1&limit=10) y por cursor (?cursor=xxx&limit=10)')async listar(@Query() query: any) { ... }Próximos Pasos
Sección titulada «Próximos Pasos»- Aprende sobre Integración ORM para conectar tu base de datos
- Explora Caché para cachear resultados paginados
- Lee la Guía de Decoradores para patrones con
@Query