CodingNic

REST API Design

Pagination

REST API Design 10 min read

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 page and limit

💡 Why this matters: A collection endpoint returning every single record at once doesn’t scale, a /products endpoint 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

javascript
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 }
  });
});
bash
curl "http://localhost:4504/products?page=2&limit=5"
text
{"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}}
bash
curl "http://localhost:4504/products?page=3&limit=10"
text
{"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

  1. Build a paginated GET /products endpoint over an array of at least 30 items, and confirm page/limit produce the correct slice for several different combinations.
  2. Add a maximum limit (for example, clamp any requested limit above 50 down to 50), and confirm a request for limit=1000 is capped correctly.
  3. Confirm the pagination.totalPages value is correct by requesting the final page and checking it contains the expected remainder of items.
  4. Explain, in your own words, why an endpoint without a default limit value could be a real problem for a very large collection.

Recap

  • Page-based pagination uses page and limit query 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.