Menu
CoddyTech

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.

challenge icon

Aufgabe

Einfach

Ersetze 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.

  1. Lies in routes/tasks.js in der Listenroute sort aus req.query. Ohne diesen Parameter behält die Liste ihre gespeicherte Reihenfolge bei.
  2. ?sort=title sortiert die Titel von A bis Z. ?sort=createdAt sortiert nach Erstellungszeit, älteste zuerst. Ein - am Anfang kehrt die Reihenfolge um: ?sort=-title ist Z bis A und ?sort=-createdAt zeigt die neuesten zuerst.
  3. Beide Felder sind Text. createdAt ist ein ISO-Datum wie "2026-03-03T10:30:00.000Z", das sich als Text korrekt sortieren lässt. Vergleiche daher mit localeCompare, statt zu subtrahieren.
  4. 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" } }
  5. Sortiere nach den Filtern, damit ?done=false&sort=-title die offenen Aufgaben von Z bis A zurückgibt.
  6. Sortiere eine Kopie mit toSorted. sort ordnet 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

Übe selbstständig: Express-Playground