Data Validation, Error Handling, and API Design PatternsLesson 3.3
API pagination — cursor-based vs offset-based
offset pagination, cursor pagination, page and limit params, total count, next/prev links, cursor encoding, performance tradeoffs
API Pagination Strategies
Never return unbounded lists from an API. Pagination is required for any endpoint that returns a collection. Two main approaches: offset and cursor.
Offset Pagination
Simple to implement. Takes page and limit query params. The problem: skipping rows gets slower as offset grows, and concurrent inserts cause rows to appear or disappear between pages.
// GET /posts?page=2&limit=20
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const offset = (page - 1) * limit;
const posts = await Post.findAll({ limit, offset });
const total = await Post.count();
res.json({
data: posts,
pagination: {
page, limit, total,
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total
}
});Cursor Pagination
Uses an opaque cursor (usually a base64-encoded ID or timestamp) to mark the last seen record. Stable under concurrent writes and consistent performance regardless of dataset size. Preferred for real-time feeds and large tables.
// GET /posts?cursor=eyJpZCI6NTB9&limit=20
const cursor = req.query.cursor
? JSON.parse(Buffer.from(req.query.cursor, 'base64').toString())
: null;
const where = cursor ? { id: { $gt: cursor.id } } : {};
const posts = await Post.findAll({ where, limit, orderBy: 'id' });
const nextCursor = posts.length === limit
? Buffer.from(JSON.stringify({ id: posts.at(-1).id })).toString('base64')
: null;
res.json({ data: posts, nextCursor });