Menu
CoddyTech

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.

challenge icon

Desafío

Fácil

Sustituye 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=.

  1. En routes/tasks.js, en la ruta de la lista, lee sort de req.query. Si no está, la lista conserva el orden en que está almacenada.
  2. ?sort=title ordena por título de la A a la Z. ?sort=createdAt ordena por fecha de creación, de la más antigua a la más reciente. Un - delante invierte el orden: ?sort=-title va de la Z a la A, y ?sort=-createdAt muestra primero las más recientes.
  3. Ambos campos son texto. createdAt es una fecha ISO como "2026-03-03T10:30:00.000Z", que se ordena correctamente como texto. Así que compara con localeCompare, no mediante una resta.
  4. 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" } }
  5. Ordena después de aplicar los filtros, para que ?done=false&sort=-title devuelva las tareas pendientes de la Z a la A.
  6. Ordena una copia con toSorted. sort cambia 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

Practica por tu cuenta: Playground de Express