Skip to content

Node.js + MongoDB

Pagination APIs

Limit, next cursor, and a stable sort — the API shape that matches cursor pagination.

IntermediateAbout 6 minutes

Module 5 is the query: stable sort, $gt on the last key, _id tiebreaker. The API is { items, next }. next is an opaque token (base64 of { price, id }), not ?page=4000. Cap limit. Do not ask the client to send skip.

app.get("/products", async (req, res) => {
  const limit = Math.min(Number(req.query.limit) || 10, 50);
  const cursor = decodeCursor(req.query.cursor); // null on first page

  const filter = { inStock: true };
  if (cursor) {
    filter.$or = [
      { price: { $gt: cursor.price } },
      { price: cursor.price, _id: { $gt: new ObjectId(cursor.id) } },
    ];
  }

  const docs = await products
    .find(filter)
    .sort({ price: 1, _id: 1 })
    .limit(limit + 1)
    .toArray();

  const hasMore = docs.length > limit;
  const page = hasMore ? docs.slice(0, limit) : docs;
  const last = page[page.length - 1];
  res.json({
    items: page.map(({ _id, name, price }) => ({ id: _id.toString(), name, price })),
    next: hasMore && last
      ? encodeCursor({ price: last.price, id: last._id.toString() })
      : null,
  });
});
GET /products?limit=10&cursor=...

limit + 1 tells you whether there is another page without a count query. Offset (skip) is acceptable for a 20-row admin table; it is not this endpoint. The token is signed or at least not a raw Mongo filter the client edits into $gt: 0. Index { inStock: 1, price: 1, _id: 1 } or this is a pretty slow JSON wrapper.

Interview question

How do you expose cursor pagination from a Node API?

Think about it first.

Practice

The query under a 'next page' of in-stock products.