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.
Aufgabe
EinfachEine 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.
- Ändere in
routes/tasks.js, was die Listenroute zurückgibt: ein Objekt statt eines Arrays. FürGET /api/tasks?limit=2mit 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 }itemsenthält die Aufgaben dieser Seite,pageist die Seitennummer,totalgibt an, wie viele Aufgaben den Filtern entsprechen, undpagesgibt die Anzahl der Seiten an. - Lies
pageundlimitaus der Abfragezeichenfolge aus:pageist eine ganze Zahl ab 1. Wenn der Wert fehlt, verwende 1.limitist eine ganze Zahl von 1 bis 50. Wenn der Wert fehlt, verwende 10.
Verwende den Standardwert, bevor du den Wert konvertierst, denn
Number(undefined)istNaN. 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 Meldunglimit must be a whole number from 1 to 50:{ "error": { "code": "invalid_query", "message": "page must be a whole number from 1" } } - Führe die Seiteneinteilung zuletzt durch: Filtere und sortiere zuerst, und wähle dann die Seite aus.
totalzählt die Aufgaben, die nach dem Filtern übrig bleiben. Daher ergibt?done=false&limit=1"total": 2und"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;
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
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