Menu
CoddyTech

Seitennummerierung

Teil des Abschnitts REST-APIs der Node.js-Journey von Coddy. Lektion 17 von 25.

Eine Liste mit zehntausend Aufgaben ist zu lang für eine einzige Antwort. Sende sie seitenweise: Der Client fragt nach ?page=2&limit=20 und erhält diesen Ausschnitt sowie die Angaben, die er braucht, um „Seite 2 von 5“ anzuzeigen:

{ "items": [ ...books 21 to 40... ], "page": 2, "total": 95, "pages": 5 }
const start = (page - 1) * limit;    // Seite 1 beginnt bei 0
const items = list.slice(start, start + limit);
const pages = Math.ceil(list.length / limit);

Ermittle total nach den Filtern, da der Wert die Liste beschreibt, nach der der Client gefragt hat. Wenn die Abfragezeichenfolge kein page enthält, verwende 1 und für limit 10, bevor du die Werte umwandelst: Number(undefined) ist NaN. Weise dann alles zurück, was keine ganze Zahl im zulässigen Bereich ist, und gib den Statuscode 400 zurück. Ein Höchstwert für limit verhindert, dass ein Client alles auf einmal anfordert.

challenge icon

Aufgabe

Einfach

Eine Liste mit zehntausend Aufgaben ist zu lang für eine einzige Antwort. Sende die Liste in Seiten, damit ein Client beispielsweise „Seite 1 von 2“ anzeigen kann.

  1. Ändere in routes/tasks.js, was die Listenroute zurückgibt: ein Objekt statt eines Arrays. Für GET /api/tasks?limit=2 mit den anfänglichen Aufgaben gibt sie Folgendes zurück:
    {
      "items": [
        { "id": 1, "title": "Write the project brief", "done": true, "createdAt": "2026-03-02T09:00:00.000Z" },
        { "id": 2, "title": "Book the meeting room", "done": false, "createdAt": "2026-03-03T10:30:00.000Z" }
      ],
      "page": 1,
      "total": 3,
      "pages": 2
    }
    items enthält die Aufgaben dieser Seite, page ist die Seitennummer, total gibt an, wie viele Aufgaben den Filtern entsprechen, und pages gibt die Anzahl der Seiten an.
  2. Lies page und limit aus der Abfragezeichenfolge aus:
    • page ist eine ganze Zahl ab 1. Wenn der Wert fehlt, verwende 1.
    • limit ist eine ganze Zahl von 1 bis 50. Wenn der Wert fehlt, verwende 10.

    Verwende den Standardwert, bevor du den Wert konvertierst, denn Number(undefined) ist NaN. Jeder andere Wert (page=0, page=abc, page=1.5, limit=0, limit=51) führt zu Status 400. Bei einem ungültigen Limit lautet die Meldung limit must be a whole number from 1 to 50:

    { "error": { "code": "invalid_query", "message": "page must be a whole number from 1" } }
  3. Führe die Seiteneinteilung zuletzt durch: Filtere und sortiere zuerst, und wähle dann die Seite aus. total zählt die Aufgaben, die nach dem Filtern übrig bleiben. Daher ergibt ?done=false&limit=1 "total": 2 und "pages": 2.

Dadurch ändert sich die Form der Antwort, also muss sich auch ein Client ändern, der ein Array erwartet. Die Lektion zur Versionierung zeigt, wie du damit umgehst.

Probiere es aus: GET /api/tasks?limit=2 gibt die Aufgaben 1 und 2 mit "page": 1, "total": 3 und "pages": 2 zurück. GET /api/tasks?page=2&limit=2 gibt nur Aufgabe 3 zurück. Ein einfaches GET /api/tasks gibt alle 3 Aufgaben mit "page": 1 und "pages": 1 zurück. GET /api/tasks?page=0 gibt 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 von 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();
const SORT_FIELDS = ['title', 'createdAt'];

// 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, sort } = 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 (sort !== undefined) {
        const field = String(sort).startsWith('-') ? String(sort).slice(1) : String(sort);
        if (!SORT_FIELDS.includes(field)) {
            return sendError(
                res,
                400,
                'invalid_query',
                'sort must be title or createdAt, with - in front to reverse',
            );
        }
        const direction = String(sort).startsWith('-') ? -1 : 1;
        list = list.toSorted((a, b) => a[field].localeCompare(b[field]) * direction);
    }
    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;
quiz iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in REST-APIs

Übe selbstständig: Express-Playground