W05 Learning Activity: Pagination
Overview
- Purpose: Learn about pagination.
- Task: Read the following information and complete the activity instructions below.
Pagination limits how much data an API returns in one response. Without pagination, a list endpoint can become slow, expensive, and difficult for clients to use as the database grows.
In an ecommerce application, pagination might be useful for product catalogs, customer orders, customer accounts, inventory records, or administrative data.
For example, an endpoint like GET /api/products might work well when the database has 10 products. But if the database eventually has hundreds or thousands of products, returning every record every time becomes a problem. The response takes longer to generate, transfers more data over the network (uses more bandwidth), and forces the client to process data it may not need.
Discipleship: Receiving growth line upon line
Pagination makes a large set of information usable by receiving it in portions. In a deeper way, the Lord often teaches and changes us gradually. This can be especially meaningful when you feel behind, overwhelmed, or frustrated by slow progress. Growth in discipleship, learning, and healing often comes in faithful portions rather than all at once.
2 Nephi 28:30 teaches:
For behold, thus saith the Lord God: I will give unto the children of men line upon line, precept upon precept, here a little and there a little; and blessed are those who hearken unto my precepts, and lend an ear unto my counsel, for they shall learn wisdom; for unto him that receiveth I will give more; and from them that shall say, We have enough, from them shall be taken away even that which they have.
Elder David A. Bednar, a modern-day Apostle of Jesus Christ, taught in The Spirit of Revelation:
The gradual increase of light radiating from the rising sun is like receiving a message from God “line upon line, precept upon precept” (2 Nephi 28:30). Most frequently, revelation comes in small increments over time and is granted according to our desire, worthiness, and preparation.
Invitation: Choose one area where you feel overwhelmed. Turn to God in prayer and ask Him to help you with one small step today.
When to Use Pagination
Any endpoint that returns a list of records should be considered for pagination. Some lists may be small today, but grow over time.
Pagination is an API design decision
Pagination is not only a database technique. It is part of the API contract. Clients need to know which query parameters are supported, what defaults are used, what limits are allowed, and how to tell whether more results exist.
Common Query Parameters
The most common approach to limiting results is page-based pagination. The client asks for a page number and a maximum number of records per page.
page: which page of results to return.limit: how many records to return per page.sort: which field should control ordering.order: whether results should be ascending (lowest to highest) or descending (highest to lowest).
GET /api/products?page=2&limit=10&sort=name&order=asc
This request asks for the second page of products, with 10 products per page, sorted alphabetically by product name.
Defaults and Limits
Your API should have default values so clients can use a list endpoint without specifying every parameter. Your API should also have maximum values so clients cannot accidentally request too much data.
Default page: 1
Default limit: 10
Maximum limit: 50
Default sort: createdAt or name, depending on the endpoint
Default order: asc or desc, depending on the endpoint
These values should be consistent across related endpoints unless there is a clear reason to use different defaults.
Set a maximum limit
Do not let clients request unlimited results. A maximum limit protects the API and database from accidental or abusive requests.
Validating Pagination Parameters
Query parameters arrive as strings, so your API needs to parse and validate them before using them in a database query. Invalid pagination values should receive a clear 400 response.
Invalid examples:
GET /api/products?page=abc
GET /api/products?page=0
GET /api/products?limit=-5
GET /api/products?limit=100000
GET /api/products?sort=password
A helpful error response might look like this:
{
"errors": [
{
"field": "limit",
"message": "limit must be a number between 1 and 50."
}
]
}
Response Metadata
A paginated response should include both the data and enough metadata for the client to know what page it received.
{
"data": [],
"pagination": {
"page": 2,
"limit": 10,
"totalItems": 48,
"totalPages": 5,
"hasNextPage": true,
"hasPreviousPage": true
}
}
The metadata helps the client build pagination controls, such as next and previous buttons. It also helps API testers know whether the endpoint is returning the expected slice of data.
Some APIs include links instead of boolean values:
{
"links": {
"self": "/api/products?page=2&limit=10",
"next": "/api/products?page=3&limit=10",
"previous": "/api/products?page=1&limit=10"
}
}
Returning direct links is a design decision, and is not strictly necessary, but you should include enough information for a client to request the next page.
Products Model
Keep database work in the model. The controller will validate the request and pass trusted pagination options to this function. The model then uses skip() and limit() to request only the needed product records.
// src/models/products.js
import { Product } from './product.js';
export const getPaginatedProducts = async ({ filter = {}, page, limit, sort, order }) => {
const skip = (page - 1) * limit;
const sortOptions = { [sort]: order };
const [products, totalItems] = await Promise.all([
Product.find(filter).sort(sortOptions).skip(skip).limit(limit),
Product.countDocuments(filter)
]);
return { products, totalItems };
};
The model does not read req or send a response. It receives a filter and validated options, returns the products and matching count, and can later be reused by another controller or service.
Products Controller
The controller owns HTTP concerns: reading query parameters, validating them, choosing supported sort fields, calling the model, and sending the JSON response.
// src/controllers/products.js
import { getPaginatedProducts } from '../models/products.js';
const allowedSortFields = ['name', 'price', 'category', 'createdAt'];
const parsePositiveInteger = (value, defaultValue) => {
if (value === undefined) {
return defaultValue;
}
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 1) {
return null;
}
return parsed;
};
export const getProducts = async (req, res, next) => {
try {
const page = parsePositiveInteger(req.query.page, 1);
const requestedLimit = parsePositiveInteger(req.query.limit, 10);
if (!page || !requestedLimit || requestedLimit > 50) {
return res.status(400).json({
errors: [{ field: 'pagination', message: 'page and limit must be valid positive numbers. Maximum limit is 50.' }]
});
}
const limit = requestedLimit;
if (req.query.sort && !allowedSortFields.includes(req.query.sort)) {
return res.status(400).json({
errors: [{ field: 'sort', message: 'sort is not supported.' }]
});
}
const sort = req.query.sort || 'name';
const order = req.query.order === 'desc' ? -1 : 1;
const { products, totalItems } = await getPaginatedProducts({
filter: {},
page,
limit,
sort,
order
});
return res.status(200).json({
data: products,
pagination: {
page,
limit,
totalItems,
totalPages: Math.ceil(totalItems / limit),
hasNextPage: page * limit < totalItems,
hasPreviousPage: page > 1
}
});
} catch (error) {
next(error);
}
};
Keeping validation in the controller means the model receives only supported values. The controller uses the count returned by the model to create pagination metadata that describes the same filtered result set as the data array.
Swagger Documentation
Paginated endpoints should document each query parameter. A client should not need to guess the default values or allowed values.
parameters:
- name: page
in: query
schema:
type: integer
default: 1
minimum: 1
- name: limit
in: query
schema:
type: integer
default: 10
minimum: 1
maximum: 50
- name: sort
in: query
schema:
type: string
enum: [name, price, category, createdAt]
- name: order
in: query
schema:
type: string
enum: [asc, desc]
Testing Pagination
When you test pagination, check more than whether the endpoint returns 200. Useful pagination tests include:
- The endpoint returns only the requested number of records.
- The second page returns different records than the first page.
- The response includes pagination metadata.
- Invalid
pageorlimitvalues return400. - The maximum limit is enforced.
- Sorting changes the order of returned results.
Activity Instructions
Check your understanding
- Why should a list endpoint use pagination as its data grows?
Click to expand the example answer
Pagination limits the number of records returned in one response. This reduces unnecessary database work, bandwidth use, and client-side processing when a collection becomes large.
- What is the purpose of the
pageandlimitquery parameters?Click to expand the example answer
pageidentifies which page of results the client wants.limitidentifies the maximum number of records to return on that page. - Why should an API enforce a maximum value for
limit?Click to expand the example answer
A maximum limit prevents accidental or abusive requests for very large result sets, which helps protect the API and database.
- What metadata should a paginated response include to help a client build Next and Previous controls?
Click to expand the example answer
The response should include information such as the current
page,limit,totalItems,totalPages,hasNextPage, andhasPreviousPage. - Why should an API use an allowlist for the
sortparameter?Click to expand the example answer
An allowlist limits sorting to supported, useful fields such as
name,price, orcategory. It keeps the API contract clear and avoids unexpected query behavior.
Other Links:
- Return to: Week Overview | Course Home