Pagination in APIs umsetzen
Pagination liefert große Datenmengen in handlichen Häppchen aus. Du lernst Offset- und Cursor-basierte Pagination kennen und siehst, wie du sie in Node.js sauber umsetzt.
Stell dir vor, deine API soll 50.000 Produkte zurückgeben. Würdest du alle auf einmal senden, wäre die Antwort riesig, langsam und für den Client kaum zu verarbeiten. Die Lösung heißt Pagination: Du lieferst die Daten in handlichen Seiten aus. In diesem Beitrag zeige ich dir die gängigen Verfahren und wie du sie umsetzt.
Warum Pagination?
Pagination teilt eine große Ergebnismenge in Seiten auf. Das bringt gleich mehrere Vorteile:
- Die Antworten bleiben klein und schnell.
- Deine Datenbank wird entlastet, weil nicht alles auf einmal geladen wird.
- Der Client kann Daten nach und nach anzeigen, etwa beim Scrollen.
Es gibt zwei verbreitete Ansätze: Offset-basierte und Cursor-basierte Pagination. Schauen wir uns beide an.
Offset-basierte Pagination
Beim Offset-Verfahren gibst du an, wie viele Einträge übersprungen (offset) und wie viele zurückgegeben werden (limit). In SQL sieht das so aus:
-- Seite 3 bei 20 Eintraegen pro Seite: 40 ueberspringen
SELECT * FROM produkte
ORDER BY id
LIMIT 20 OFFSET 40;In einer Express-Route berechnest du den Offset aus der Seitennummer:
app.get("/produkte", async (req, res) => {
const seite = Number(req.query.seite) || 1;
const proSeite = Number(req.query.proSeite) || 20;
const offset = (seite - 1) * proSeite;
const produkte = await db.produkte.findMany({
skip: offset,
take: proSeite,
orderBy: { id: "asc" }
});
res.json({ data: produkte, seite, proSeite });
});Das ist einfach und intuitiv. Der Nachteil: Bei sehr großen Offsets wird die Datenbank langsam, weil sie viele Zeilen überspringen muss.
Metadaten mitliefern
Damit der Client weiß, wie viele Seiten es gibt, schickst du nützliche Metadaten mit. Dafür brauchst du die Gesamtzahl der Einträge:
const gesamt = await db.produkte.count();
const seitenGesamt = Math.ceil(gesamt / proSeite);
res.json({
data: produkte,
meta: {
seite,
proSeite,
gesamt,
seitenGesamt
}
});Mit diesen Angaben kann der Client eine Seitennavigation aufbauen und weiß, ob es eine nächste Seite gibt.
Cursor-basierte Pagination
Bei sehr großen Datenmengen ist die Cursor-Pagination effizienter. Statt einer Seitennummer merkst du dir einen Zeiger auf den letzten gesehenen Eintrag, meist dessen ID:
app.get("/feed", async (req, res) => {
const nachId = Number(req.query.nachId) || 0;
const limit = 20;
// Nur Eintraege nach der zuletzt gesehenen ID
const eintraege = await db.posts.findMany({
where: { id: { gt: nachId } },
take: limit,
orderBy: { id: "asc" }
});
const naechsterCursor =
eintraege.length > 0 ? eintraege[eintraege.length - 1].id : null;
res.json({ data: eintraege, naechsterCursor });
});Der Client schickt beim nächsten Aufruf einfach den naechsterCursor als nachId mit. Das bleibt auch bei Millionen Einträgen schnell.
Offset oder Cursor: Was wann?
Beide Verfahren haben ihre Stärken. Eine Faustregel hilft bei der Entscheidung:
- Offset: gut, wenn Nutzer zu beliebigen Seiten springen sollen (z. B. eine klassische Seitennavigation).
- Cursor: ideal für endloses Scrollen und sehr große oder häufig wachsende Datenmengen.
Cursor-Pagination hat zusätzlich den Vorteil, dass sich Ergebnisse nicht verschieben, wenn während des Blätterns neue Einträge hinzukommen.
Fazit
Pagination ist Pflicht, sobald deine API größere Listen ausliefert. Mit Offset-basierter Pagination bekommst du schnell eine klassische Seitennavigation, mit Cursor-basierter Pagination skalierst du auch riesige Datenmengen mühelos. Liefere immer hilfreiche Metadaten mit, damit der Client weiß, wie es weitergeht. Überlege dir je nach Anwendungsfall, welches Verfahren besser passt.