How would you design pagination for an API endpoint that returns thousands of records?
Return a slice per request using limit and page (offset) or, for large changing data, a cursor of the last item seen. Cap the maximum limit and return total or next-page information in the response.
Explain it simply
A results page showing 10,000 students at once would be slow and useless. Pagination is like turning pages in a book: ask for page 3 and get only that page.
Two common styles:
- Offset (page) based:
GET /students?page=3&limit=20. Easy to build, lets the user jump to any page. The database must skip all earlier rows, so deep pages get slower, and new inserts can shift items between pages. - Cursor based:
GET /students?limit=20&after=10452. The server returns the next 20 rows after a given ID. It stays fast on huge tables and is stable while data changes, but cannot jump to page 50 directly. It suits feeds and infinite scroll.
Example
app.get("/api/students", async (req, res) => {
const limit = Math.min(Number(req.query.limit) || 20, 100); // cap at 100
const page = Math.max(Number(req.query.page) || 1, 1);
const [items, total] = await Promise.all([
Student.find().sort({ _id: 1 }).skip((page - 1) * limit).limit(limit),
Student.countDocuments(),
]);
res.json({
items,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
});
});Always sort by a stable key; without it, rows may repeat or vanish between pages.
What to say in the interview
"I use limit and page for simple admin lists, and cursor-based pagination for large or fast-changing data. I cap the limit, sort by a stable column, and return metadata so the client knows what comes next."
Common mistakes
- Allowing
limit=1000000. - Paginating without any ORDER BY.