Express.js
Query parameters, filtros y límites
Diseño de filtros, orden, búsqueda y paginación como lenguaje público limitado y seguro de una API.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
Express.js
Diseño de filtros, orden, búsqueda y paginación como lenguaje público limitado y seguro de una API.
Una URL como:
GET /orders?status=pending&limit=20&sort=-createdAtno es solo un conjunto de strings. Define filtros, orden, paginación y opciones que afectan coste, seguridad y estabilidad del contrato.
Express entrega request.query según el query parser configurado. El resultado sigue siendo input no confiable y puede contener strings, arrays, objetos o valores repetidos.
Los clientes necesitan variar una colección sin crear una ruta por cada combinación. Query params permiten expresar:
Sin un contrato, el backend puede terminar exponiendo directamente la estructura del ORM o de la base de datos.
query string
↓ parser
estructura no confiable
↓ schema y normalización
consulta pública validada
↓ traducción por whitelist
criterios internos
↓ repository/query builderconst listOrdersQuerySchema = z.object({
status: z.enum(['pending', 'confirmed', 'cancelled']).optional(),
limit: z.coerce.number().int().min(1).max(100).default(20),
cursor: z.string().max(200).optional(),
sort: z.enum(['createdAt', '-createdAt']).default('-createdAt'),
search: z.string().trim().min(1).max(100).optional(),
}).strict();El schema:
La URL siempre transporta texto. limit=20 llega como string. La coerción puede ser útil, pero debe evitar ambigüedad:
limit=
limit=abc
limit=20.5
limit=-1No conviertas silenciosamente valores inválidos a defaults; eso oculta errores del cliente.
Estas URLs pueden representar lo mismo según convención:
?status=pending&status=confirmed
?status[]=pending&status[]=confirmed
?status=pending,confirmedElige una forma, documéntala y normalízala. No dependas accidentalmente del parser por defecto, especialmente al migrar entre Express 4 y 5.
Los placeholders SQL no representan nombres de columnas:
const sortOptions = {
createdAt: { column: 'created_at', direction: 'asc' },
'-createdAt': { column: 'created_at', direction: 'desc' },
} as const;Nunca interpoles directamente:
`ORDER BY ${request.query.sort}`Aunque un ORM parametrice valores, nombres de columnas, relaciones incluidas y operadores dinámicos pueden generar injection o consultas muy costosas.
El contrato público no tiene que copiar el esquema interno:
status=activepuede traducirse a varias condiciones internas. Esta capa de traducción permite evolucionar tablas sin romper consumidores.
Los defaults deben ser:
Un default de 20 resultados puede ser apropiado; “sin limit cuando no se especifica” suele ser riesgoso.
Controla:
inUn input pequeño puede producir una query enorme.
path
→ identidad o jerarquía: /orders/:id
query
→ variación opcional de lectura: ?status=pending
body
→ representación o comando: { items: [...] }No es una regla absoluta, pero ayuda a mantener contratos previsibles.
Simple y permite saltar páginas, pero se degrada con offsets profundos y puede producir inconsistencias bajo cambios concurrentes.
Usa una posición estable basada en columnas ordenadas:
?after=opaqueCursor&limit=20El cursor debe ser opaco, validado y asociado al mismo orden y filtros.
Un parámetro search necesita definir:
No ejecutes regex o full scans arbitrarios por cada request.
const query = listOrdersQuerySchema.parse(request.query);
const result = await listOrders.execute({
actor: response.locals.actor,
filters: {
status: query.status,
search: query.search,
},
pagination: {
limit: query.limit,
cursor: query.cursor,
},
sort: sortOptions[query.sort],
});La aplicación recibe un contrato limpio; el repository construye SQL parametrizado y tenant-aware.
Dos URLs equivalentes pueden fragmentar cache:
?status=pending&limit=20
?limit=20&status=pendingUna API pública puede definir orden canónico, omitir defaults de links generados o usar headers de cache correctamente. No hace falta redirigir toda variación, pero sí comprender el efecto.
Rechazarlo ayuda a detectar errores. Ignorarlo puede hacer que el cliente crea que filtró cuando no ocurrió.
status= no equivale necesariamente a ausencia. Decide y valida.
Firma o valida estructura; responde 400 sin filtrar detalles internos.
La paginación por cursor necesita un tie-breaker, por ejemplo created_at DESC, id DESC.
Puede consumir CPU e I/O aunque devuelva pocas filas.
req.query directamente a where.include.ORDER BY.Prueba:
Un lenguaje de filtros muy potente reduce endpoints, pero aumenta superficie de seguridad, coste y documentación. Para APIs públicas suele ser mejor un conjunto limitado de operaciones que exponer un mini-ORM por URL.
req.query?Request body, Content-Type y parsing explica cómo los bytes se transforman antes de que exista un objeto validable.