Facebook Pixel

Pagination Best Practices for Node.js APIs

Pagination is non-negotiable for list endpoints. Here are best practices.

Pagination Best Practices for Node.js APIs

Pagination is non-negotiable for list endpoints. Returning everything is slow, uses memory, and breaks clients. Here are best practices.

1. Always Paginate

Never return an entire collection. Even if it is small today, it will grow. Add pagination from day one.

2. Cap the Limit

Math.min(parseInt(req.query.limit) || 20, 100). Prevents abuse. Without a cap, a client can request 10000 items.

3. Use Cursor for Large Collections

For > 10000 docs, use cursor pagination. Faster and stable. For smaller collections, offset is fine.

4. Use Offset for Page Numbers

If the client needs page numbers ("showing 1-20 of 432"), use offset. Return total. Use Promise.all([find, countDocuments]) to run both in parallel.

5. Index the Sort or Cursor Field

Both offset and cursor need an index on the sort or cursor field. Without an index, pagination is not faster than a collection scan.

6. Use .lean() and .select()

.lean() skips hydrating full documents. .select() limits fields. Together they reduce memory and payload.

7. Return Metadata

{ data: items, total, skip, limit } for offset. { data: items, nextCursor, hasMore } for cursor. The client needs metadata to know if there is more.

8. Stable Sort

If you sort by createdAt, ties are possible (two items with the same timestamp). Use a tiebreaker (e.g., _id) to make the sort stable. Otherwise, cursor pagination can skip or duplicate items.

.sort({ createdAt: -1, _id: 1 })

9. Handle Empty Results

Return an empty array, not null. { data: [], total: 0 }. The client should not have to handle null.

10. Validate the Cursor

For cursor pagination, validate that the cursor is a valid date (or whatever type you use). Reject invalid cursors with 400.

11. Use Query Params, Not Body

Pagination belongs in query params (?skip=0&limit=20), not the body. GET requests should not have a body.

12. Document the Pagination Scheme

Tell clients how pagination works: is it offset or cursor? What are the param names? What does the response look like? Swagger or a markdown doc.

The Takeaway

Pagination best practices: always paginate, cap the limit, use cursor for large collections, use offset for page numbers, index the sort or cursor field, use .lean() and .select(), return metadata, use a stable sort (tiebreaker on _id), handle empty results, validate the cursor, use query params not body, and document the scheme.

Always paginate, cap the limit, use cursor for large collections, use offset for page numbers, index the sort or cursor field, use .lean() and .select(), return metadata, use a stable sort (tiebreaker on _id), handle empty results, validate the cursor, use query params not body, and document the scheme.

To prevent abuse. Without a cap, a client can request 10000 items, eating memory and bandwidth. Use Math.min(parseInt(req.query.limit) || 20, 100). Most UIs only show 20 at a time anyway.

If you sort by createdAt, ties are possible (two items with the same timestamp). Without a tiebreaker, cursor pagination can skip or duplicate items. Use .sort({ createdAt: -1, _id: 1 }) to make the sort stable.

So the client does not have to handle null. Return { data: [], total: 0 } for offset or { data: [], nextCursor: null, hasMore: false } for cursor. Consistent shapes make the client simpler.

Because pagination belongs in the URL. GET requests should not have a body. Query params (?skip=0&limit=20) are cacheable, bookmarkable, and shareable. Body pagination breaks HTTP semantics.

Ready to master Node.js completely?

Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.

Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.