Menu
CoddyTech

Ordinamento

Fa parte della sezione API REST del percorso Node.js di Coddy. Lezione 16 di 25.

Un client vuole prima le attività più recenti oppure l’elenco dalla A alla Z. Una convenzione comune è ?sort=field, con un - davanti per invertire l’ordine:

// 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);

Prima verifica che field corrisponda a uno dei campi consentiti e rispondi con 400 per qualsiasi altro, perché un campo che i libri non hanno non fornisce un ordinamento utile. toSorted restituisce una copia ordinata. sort riordina l’array stesso e, se nessun filtro ha creato una copia, quell’array è l’archivio: ogni richiesta successiva vede il nuovo ordine. Questo esempio sottrae i valori, operazione che funziona solo con i numeri. Per confrontare due stringhe, usa a.localeCompare(b): come la sottrazione, restituisce un numero negativo, 0 o un numero positivo. Le date ISO sono stringhe che vengono ordinate in ordine cronologico.

challenge icon

Sfida

Facile

Sostituisci l'ordinamento basato solo sul titolo che hai scritto in Ordinamento del testo con la gestione del campo e della direzione riportata di seguito.

I client vogliono prima le attività più recenti oppure l'elenco dalla A alla Z. Lascia che l'elenco si ordini con ?sort=.

  1. In routes/tasks.js, nella route dell'elenco, leggi sort da req.query. Se non è presente, l'elenco mantiene l'ordine in cui è memorizzato.
  2. ?sort=title ordina per titolo dalla A alla Z. ?sort=createdAt ordina per data di creazione, dalla più vecchia alla più recente. Un - davanti inverte l'ordine: ?sort=-title ordina dalla Z alla A, mentre ?sort=-createdAt mostra prima le attività più recenti.
  3. Entrambi i campi sono testo. createdAt è una data ISO come "2026-03-03T10:30:00.000Z", che si ordina correttamente come testo. Quindi confronta con localeCompare, non sottraendo i valori.
  4. Qualsiasi altro campo, come ?sort=color, restituisce lo stato 400:
    { "error": { "code": "invalid_query", "message": "sort must be title or createdAt, with - in front to reverse" } }
  5. Ordina dopo aver applicato i filtri, così ?done=false&sort=-title restituisce le attività aperte dalla Z alla A.
  6. Ordina una copia con toSorted. sort riordina l'array memorizzato stesso e tutte le richieste successive vedrebbero il nuovo ordine.

Prova: GET /api/tasks?sort=title restituisce le attività 2, 3, 1 ("Book...", "Send...", "Write..."). GET /api/tasks?sort=-createdAt restituisce 3, 2, 1. Poi GET /api/tasks senza parametri restituisce di nuovo 1, 2, 3. GET /api/tasks?sort=color restituisce 400 con il codice invalid_query.

Provalo tu

import { Router } from 'express';
import { z } from 'zod';
import { sendError, sendInvalid } from '../errors.js';
import { nextId, tasks } from '../store.js';

const router = Router();

// Un'attività completa, come viene inviata da PUT. .strict() rifiuta i campi che non elenca.
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 se un'altra attività ha già questo titolo, indipendentemente dalle maiuscole e minuscole.
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;

Tutte le lezioni di API REST

Esercitati da solo: Playground Express