As your applications scale and datasets grow, returning all records at once can overwhelm both your backend and frontend. That’s where pagination comes in. Pagination lets you divide results into manageable “pages” and send only a subset of data in each response—boosting performance and improving the user experience.
In this lesson, you’ll implement server-side pagination in a Flask API. You’ll write a reusable pattern to handle query parameters like ?page=2&per_page=5 and return paginated responses from endpoints like /recipes or /posts.
The starter code includes a Flask app and seed data for a resource (e.g., Recipe).
To get started:
pipenv install && pipenv shell
cd server
flask db init
flask db migrate -m "initial migration"
flask db upgrade head
python seed.py
python app.py
You can view the API in your browser or using Postman. Test pagination by visiting http://localhost:5555/recipes?page=1&per_page=5.
Without pagination, your API will return all records in a table—even if the user only needs the first 10. This increases response size, clutters the UI, and slows down both the client and server.
We need to:
- Modify our GET routes (like /recipes) to accept pagination parameters.
- Return a subset of records based on page and per_page.
- Include metadata (like total pages) in the response.
Backend Design Goals:
- Accept
?page=<number>&per_page=<number>
query params. - Use SQLAlchemy’s .paginate() method to query the desired page.
- Return a JSON response with:
- The current page of records
- Metadata (e.g., current page, total pages, total items)
Example Response:
{
"page": 2,
"per_page": 5,
"total": 23,
"total_pages": 5,
"items": [
{"id": 6, "title": "Tiramisu", ...},
{"id": 7, "title": "Risotto", ...}
]
}
In your RecipeList resource get route:
from flask import request
class Recipes(Resource):
def get(self):
page = request.args.get("page", 1, type=int)
per_page = request.args.get("per_page", 5, type=int)
Use .paginate() to fetch just the right records:
pagination = Recipe.query.paginate(page=page, per_page=per_page, error_out=False)
recipes = pagination.items
Include metadata and the records in the JSON response:
return {
"page": page,
"per_page": per_page,
"total": pagination.total,
"total_pages": pagination.pages,
"items": [RecipeSchema().dump(recipe) for recipe in recipes]
}, 200
Your full code solution should look like:
class Recipes(Resource):
def get(self):
page = request.args.get("page", 1, type=int)
per_page = request.args.get("per_page", 5, type=int)
pagination = Recipe.query.paginate(page=page, per_page=per_page, error_out=False)
recipes = pagination.items
return {
"page": page,
"per_page": per_page,
"total": pagination.total,
"total_pages": pagination.pages,
"items": [RecipeSchema().dump(recipe) for recipe in recipes]
}, 200
Try endpoints like:
/recipes?page=1
/recipes?page=2&per_page=3
Ensure:
- No crash if page exceeds total
- Returns correct number of records
- Metadata reflects database accurately
- Commit and push your code:
git add .
git commit -m "final solution"
git push
- If you created a separate feature branch, remember to open a PR on main and merge.
Best Practice documentation steps:
- Add comments to the code to explain purpose and logic, clarifying intent and functionality of your code to other developers.
- Update README text to reflect the functionality of the application following https://makeareadme.com.
- Add screenshot of completed work included in Markdown in README.
- Delete any stale branches on GitHub
- Remove unnecessary/commented out code
- If needed, update git ignore to remove sensitive data
Pagination is a critical tool for building scalable APIs. It prevents unnecessary data transfer and gives clients the control to load only what they need. You’ve now implemented pagination using Flask and SQLAlchemy, returning both records and helpful metadata to support frontend UIs and mobile apps alike.
If the client does not send page or per_page, your route should fall back to sensible defaults (e.g., page=1, per_page=5).
This prevents 500 errors and makes your API more resilient to inconsistent client-side behavior.
You may want to cap per_page values to prevent abuse (e.g., a user requesting 10,000 records at once).
A common pattern is to enforce a maximum like min(per_page, 100) to ensure consistent load.
Clients (especially frontends using tables or infinite scroll) rely on metadata such as total, page, and total_pages to render UI controls.
If you only return the items, clients can’t know when to stop or what options to display.
Pagination is often used with sorting and filtering.
For example, clients may request ?page=1&per_page=10&sort=title&search=pie.
Plan your pagination logic to layer nicely with additional filters or order_by() logic.
Requests like ?page=999 shouldn’t crash or return a 404.
Returning an empty items array with correct metadata helps clients know they're at the end.
{
"page": 999,
"items": [],
"total": 42,
"total_pages": 5
}
Pagination is also valuable for background services, admin tools, or any API where data size impacts bandwidth or performance.
Even internal tools should use pagination to prevent unexpected performance bottlenecks.