Tri
Fait partie de la section API REST du Journey Node.js de Coddy. Leçon 16 sur 25.
Un client veut les tâches les plus récentes en premier, ou la liste de A à Z. Une convention courante consiste à utiliser ?sort=field, avec un - devant pour inverser l’ordre :
// 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);Vérifie d’abord que field fait partie des champs que tu autorises, et réponds 400 pour toute autre valeur, car un champ que les livres n’ont pas ne permet pas d’obtenir un ordre utile. toSorted renvoie une copie triée. sort réorganise le tableau lui-même et, lorsqu’aucun filtre n’a créé de copie, ce tableau est le magasin : toutes les requêtes suivantes voient le nouvel ordre. Cet exemple utilise une soustraction, qui ne fonctionne que pour les nombres. Pour comparer deux chaînes, utilise a.localeCompare(b) : comme la soustraction, cette méthode renvoie un nombre négatif, 0 ou un nombre positif. Les dates ISO sont des chaînes qui se trient dans l’ordre chronologique.
Défi
FacileRemplace le tri par titre uniquement que tu as écrit dans Tri du texte par la gestion du champ et du sens ci-dessous.
Les clients veulent voir les tâches les plus récentes en premier, ou la liste de A à Z. Laisse la liste se trier elle-même avec ?sort=.
- Dans
routes/tasks.js, dans la route de la liste, récupèresortdepuisreq.query. Sans ce paramètre, la liste conserve l’ordre dans lequel elle est stockée. ?sort=titletrie les titres de A à Z.?sort=createdAttrie par date de création, de la plus ancienne à la plus récente. Un-placé devant inverse l’ordre :?sort=-titledonne Z à A, et?sort=-createdAtaffiche les plus récentes en premier.- Les deux champs sont du texte.
createdAtest une date ISO comme"2026-03-03T10:30:00.000Z", qui se trie correctement comme du texte. Il faut donc comparer aveclocaleCompare, et non soustraire. - Tout autre champ, comme
?sort=color, renvoie le statut 400 :{ "error": { "code": "invalid_query", "message": "sort must be title or createdAt, with - in front to reverse" } } - Effectue le tri après les filtres, afin que
?done=false&sort=-titlerenvoie les tâches ouvertes de Z à A. - Trie une copie avec
toSorted.sortréordonne le tableau stocké lui-même, et toutes les requêtes suivantes verraient le nouvel ordre.
Essaie : GET /api/tasks?sort=title renvoie les tâches 2, 3, 1 ("Book...", "Send...", "Write..."). GET /api/tasks?sort=-createdAt renvoie 3, 2, 1. Ensuite, GET /api/tasks sans paramètre renvoie de nouveau 1, 2, 3. GET /api/tasks?sort=color renvoie le statut 400 avec le code invalid_query.
Essayez vous-même
import { Router } from 'express';
import { z } from 'zod';
import { sendError, sendInvalid } from '../errors.js';
import { nextId, tasks } from '../store.js';
const router = Router();
// Une tâche complète, telle que PUT l’envoie. .strict() refuse les champs qu’il ne répertorie pas.
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();
// Vrai lorsqu’une autre tâche porte déjà ce titre, quelle que soit la casse.
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;
Toutes les leçons de API REST
1Ressources et verbes
Ressources et URLCréer avec POST4Navigateurs, versions et configuration
Qu’est-ce qu’une origineAutoriser une autre origineCORSUne page qui appelle l’APIEntraînez-vous par vous-même : Playground Express