Pagination
Objectives
By the end of this lesson, you should be able to:
- Implement page-based pagination on a collection endpoint
- Return pagination metadata alongside the requested page of data
- Choose sensible default and maximum values for
pageandlimit
💡 Why this matters: A collection endpoint returning every single record at once doesn’t scale, a
/productsendpoint with 50,000 rows would be enormous, slow, and mostly wasted if a client only needs to show 10 at a time. Pagination fixes this.
⚠️ A note on verification: every snippet and every response shown below was actually run and tested with real HTTP requests.
Page-Based Pagination
const products = Array.from({ length: 25 }, (_, i) => ({ id: i + 1, name: `Product ${i + 1}` }));
app.get('/products', (req, res) => {
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.max(1, parseInt(req.query.limit) || 10);
const start = (page - 1) * limit;
const end = start + limit;
const results = products.slice(start, end);
const totalItems = products.length;
const totalPages = Math.ceil(totalItems / limit);
res.json({
data: results,
pagination: { page, limit, totalItems, totalPages }
});
});
curl "http://localhost:4504/products?page=2&limit=5"
{"data":[{"id":6,"name":"Product 6"},{"id":7,"name":"Product 7"},{"id":8,"name":"Product 8"},{"id":9,"name":"Product 9"},{"id":10,"name":"Product 10"}],"pagination":{"page":2,"limit":5,"totalItems":25,"totalPages":5}}
curl "http://localhost:4504/products?page=3&limit=10"
{"data":[{"id":21,"name":"Product 21"},{"id":22,"name":"Product 22"},{"id":23,"name":"Product 23"},{"id":24,"name":"Product 24"},{"id":25,"name":"Product 25"}],"pagination":{"page":3,"limit":10,"totalItems":25,"totalPages":3}}
page and limit come from req.query (Module 5), parseInt(...) || defaultValue handles a missing or invalid value gracefully, Math.max(1, ...) prevents a nonsensical page=0 or negative value. .slice(start, end) (Module 2’s array methods) extracts exactly the requested page, (page - 1) * limit correctly computes the starting index for any page.
Why Pagination Metadata Matters
Returning just the sliced array ([...]) tells a client nothing about whether more pages exist, wrapping the response in { data: [...], pagination: {...} } gives a client everything needed to build “next page” / “previous page” controls, or to know it’s already on the last page (page === totalPages), without a separate request just to find out.
Sensible Defaults and Limits
Without a default limit, an unspecified request could be interpreted as “return everything,” defeating the purpose of pagination entirely, defaulting to a reasonable number (10 or 20 are both common) avoids this. A real API typically also caps the maximum allowed limit (rejecting or clamping limit=100000), preventing a single request from accidentally (or intentionally) requesting an enormous, server-straining response.
Try It
- Build a paginated
GET /productsendpoint over an array of at least 30 items, and confirmpage/limitproduce the correct slice for several different combinations. - Add a maximum
limit(for example, clamp any requestedlimitabove 50 down to 50), and confirm a request forlimit=1000is capped correctly. - Confirm the
pagination.totalPagesvalue is correct by requesting the final page and checking it contains the expected remainder of items. - Explain, in your own words, why an endpoint without a default
limitvalue could be a real problem for a very large collection.
Recap
- Page-based pagination uses
pageandlimitquery parameters,.slice()extracts exactly the requested subset. - A paginated response should include metadata (
page,limit,totalItems,totalPages), not just the sliced data alone. - Sensible defaults (a default
limit) and a maximum cap prevent an unbounded, server-straining request.
Next lesson: filtering, sorting, and search, letting a client ask for exactly the data it needs.