Ordenamiento
Parte de la sección API REST del Journey de Node.js de Coddy. Lección 16 de 25.
Un cliente quiere las tareas más recientes primero, o la lista de la A a la Z. Una convención habitual es ?sort=field, con un - delante para invertir el orden:
// GET /api/books?sort=-year
const field = sort.startsWith('-') ? sort.slice(1) : sort; // 'year'
const direction = sort.startsWith('-') ? -1 : 1;
list = list.toSorted((a, b) => (a[field] - b[field]) * direction);Comprueba primero que field esté entre los campos que permites y responde con 400 para cualquier otro, ya que un campo que los libros no tienen no proporciona un orden útil. toSorted devuelve una copia ordenada. sort reordena la propia matriz y, si ningún filtro creó una copia, esa matriz es el almacén: todas las solicitudes posteriores verán el nuevo orden. Este ejemplo resta, lo que solo funciona con números. Para comparar dos cadenas, usa a.localeCompare(b): al igual que la resta, devuelve un número negativo, 0 o un número positivo. Las fechas ISO son cadenas que se ordenan cronológicamente.
Desafío
FácilSustituye la ordenación solo por título que escribiste en Sorting Text por el manejo de campo y dirección que aparece a continuación.
Los clientes quieren ver primero las tareas más recientes o la lista de la A a la Z. Deja que la lista se ordene con ?sort=.
- En
routes/tasks.js, en la ruta de la lista, leesortdereq.query. Si no está, la lista conserva el orden en que está almacenada. ?sort=titleordena por título de la A a la Z.?sort=createdAtordena por fecha de creación, de la más antigua a la más reciente. Un-delante invierte el orden:?sort=-titleva de la Z a la A, y?sort=-createdAtmuestra primero las más recientes.- Ambos campos son texto.
createdAtes una fecha ISO como"2026-03-03T10:30:00.000Z", que se ordena correctamente como texto. Así que compara conlocaleCompare, no mediante una resta. - Cualquier otro campo, como
?sort=color, devuelve el estado 400:{ "error": { "code": "invalid_query", "message": "sort must be title or createdAt, with - in front to reverse" } } - Ordena después de aplicar los filtros, para que
?done=false&sort=-titledevuelva las tareas pendientes de la Z a la A. - Ordena una copia con
toSorted.sortcambia el orden de la matriz almacenada, y todas las solicitudes posteriores verían el nuevo orden.
Pruébalo: GET /api/tasks?sort=title devuelve las tareas 2, 3, 1 ("Book...", "Send...", "Write..."). GET /api/tasks?sort=-createdAt devuelve 3, 2, 1. Después, GET /api/tasks sin parámetros vuelve a devolver 1, 2, 3. GET /api/tasks?sort=color devuelve 400 con el código invalid_query.
Pruébalo tú mismo
import { Router } from 'express';
import { z } from 'zod';
import { sendError, sendInvalid } from '../errors.js';
import { nextId, tasks } from '../store.js';
const router = Router();
// Una tarea completa, tal como la envía PUT. .strict() rechaza los campos que no incluye en su lista.
const Task = z
.object({
title: z.string().trim().min(1).max(100),
done: z.boolean(),
})
.strict();
const NewTask = Task.partial({ done: true });
const TaskChanges = Task.partial();
// True si otra tarea ya tiene este título, sin importar si está en mayúsculas o minúsculas.
const titleTaken = (title, exceptId) =>
tasks.some((t) => t.id !== exceptId && t.title.toLowerCase() === title.toLowerCase());
router.get('/', (req, res) => {
const { done, q } = req.query;
let list = tasks;
if (done !== undefined) {
if (done !== 'true' && done !== 'false') {
return sendError(res, 400, 'invalid_query', 'done must be true or false');
}
list = list.filter((t) => t.done === (done === 'true'));
}
if (q !== undefined) {
const words = String(q).toLowerCase();
list = list.filter((t) => t.title.toLowerCase().includes(words));
}
if (req.query.sort === 'title') {
list = list.toSorted((a, b) => a.title.localeCompare(b.title));
}
res.json(list);
});
router.get('/:id', (req, res) => {
const task = tasks.find((t) => t.id === Number(req.params.id));
if (!task) {
return sendError(res, 404, 'not_found', 'Task not found');
}
res.json(task);
});
router.post('/', (req, res) => {
const result = NewTask.safeParse(req.body);
if (!result.success) {
return sendInvalid(res, result.error);
}
const { title, done = false } = result.data;
if (titleTaken(title)) {
return sendError(res, 409, 'conflict', 'A task with this title already exists');
}
const task = { id: nextId(), title, done, createdAt: new Date().toISOString() };
tasks.push(task);
res.status(201).location(`/api/tasks/${task.id}`).json(task);
});
router.patch('/:id', (req, res) => {
const task = tasks.find((t) => t.id === Number(req.params.id));
if (!task) {
return sendError(res, 404, 'not_found', 'Task not found');
}
const result = TaskChanges.safeParse(req.body);
if (!result.success) {
return sendInvalid(res, result.error);
}
if (result.data.title !== undefined && titleTaken(result.data.title, task.id)) {
return sendError(res, 409, 'conflict', 'A task with this title already exists');
}
Object.assign(task, result.data);
res.json(task);
});
router.put('/:id', (req, res) => {
const index = tasks.findIndex((t) => t.id === Number(req.params.id));
if (index === -1) {
return sendError(res, 404, 'not_found', 'Task not found');
}
const result = Task.safeParse(req.body);
if (!result.success) {
return sendInvalid(res, result.error);
}
const { id, createdAt } = tasks[index];
if (titleTaken(result.data.title, id)) {
return sendError(res, 409, 'conflict', 'A task with this title already exists');
}
tasks[index] = { id, ...result.data, createdAt };
res.json(tasks[index]);
});
router.delete('/:id', (req, res) => {
const index = tasks.findIndex((t) => t.id === Number(req.params.id));
if (index === -1) {
return sendError(res, 404, 'not_found', 'Task not found');
}
tasks.splice(index, 1);
res.status(204).end();
});
export default router;
Todas las lecciones de API REST
1Recursos y verbos
Recursos y URLCrear con POST4Navegadores, versiones y configuración
Qué es un origenPermitir otro origenCORSUna página que llama a la APIPractica por tu cuenta: Playground de Express