Sortieren
Teil des Abschnitts REST-APIs der Node.js-Journey von Coddy. Lektion 16 von 25.
Ein Client möchte die neuesten Aufgaben zuerst oder die Liste von A bis Z. Eine gängige Konvention ist ?sort=field, mit einem - davor, um die Reihenfolge umzukehren:
// 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);Prüfe zuerst, ob field zu den erlaubten Feldern gehört, und antworte bei jedem anderen Feld mit 400, da ein Feld, das Bücher nicht haben, keine sinnvolle Sortierung ermöglicht. toSorted gibt eine sortierte Kopie zurück. sort sortiert das Array selbst um. Wenn kein Filter eine Kopie erstellt hat, ist dieses Array der Speicher: Jede spätere Anfrage sieht die neue Reihenfolge. Dieses Beispiel verwendet Subtraktion, die nur mit Zahlen funktioniert. Um zwei Zeichenfolgen zu vergleichen, verwende a.localeCompare(b): Wie die Subtraktion gibt es eine negative Zahl, 0 oder eine positive Zahl zurück. ISO-Daten sind Zeichenfolgen, die sich in zeitlicher Reihenfolge sortieren lassen.
Aufgabe
EinfachErsetze die reine Sortierung nach Titel, die du in „Text sortieren“ geschrieben hast, durch die folgende Verarbeitung von Feld und Richtung.
Clients möchten die neuesten Aufgaben zuerst oder die Liste von A bis Z sehen. Lass die Liste sich mit ?sort= sortieren.
- Lies in
routes/tasks.jsin der Listenroutesortausreq.query. Ohne diesen Parameter behält die Liste ihre gespeicherte Reihenfolge bei. ?sort=titlesortiert die Titel von A bis Z.?sort=createdAtsortiert nach Erstellungszeit, älteste zuerst. Ein-am Anfang kehrt die Reihenfolge um:?sort=-titleist Z bis A und?sort=-createdAtzeigt die neuesten zuerst.- Beide Felder sind Text.
createdAtist ein ISO-Datum wie"2026-03-03T10:30:00.000Z", das sich als Text korrekt sortieren lässt. Vergleiche daher mitlocaleCompare, statt zu subtrahieren. - Jedes andere Feld, etwa
?sort=color, führt zum Status 400:{ "error": { "code": "invalid_query", "message": "sort must be title or createdAt, with - in front to reverse" } } - Sortiere nach den Filtern, damit
?done=false&sort=-titledie offenen Aufgaben von Z bis A zurückgibt. - Sortiere eine Kopie mit
toSorted.sortordnet das gespeicherte Array selbst um, und jede spätere Anfrage würde dann die neue Reihenfolge sehen.
Probiere es aus: GET /api/tasks?sort=title gibt die Aufgaben 2, 3, 1 zurück ("Book...", "Send...", "Write..."). GET /api/tasks?sort=-createdAt gibt 3, 2, 1 zurück. Danach gibt GET /api/tasks wieder 1, 2, 3 zurück. GET /api/tasks?sort=color gibt den Status 400 mit dem Code invalid_query zurück.
Probier es selbst
import { Router } from 'express';
import { z } from 'zod';
import { sendError, sendInvalid } from '../errors.js';
import { nextId, tasks } from '../store.js';
const router = Router();
// Eine vollständige Aufgabe, wie sie per PUT gesendet wird. .strict() lehnt Felder ab, die nicht aufgeführt sind.
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, wenn eine andere Aufgabe diesen Titel bereits hat, unabhängig von der Groß- und Kleinschreibung.
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;
Alle Lektionen in REST-APIs
1Ressourcen und Verben
Ressourcen und URLsErstellen mit POST4Browser, Versionen und Konfiguration
Was ist ein Origin?Einen anderen Origin zulassenCORSEine Seite, die die API aufruftÜbe selbstständig: Express-Playground