Gamattowicz
backend

FastAPI Pagination Pt. II: Page/Per_page

FastAPI Pagination Pt. II: Page/Per_page
0 views
10 min read
backend

Introduction

In the first part of our series, we explored the basics of pagination in FastAPI, focusing on the limit/offset strategy - a key technique for managing large datasets in scalable and efficient web applications. This method makes it easier for the user to navigate large datasets by delivering content in smaller, more manageable segments, reducing server load and increasing application performance. Moving forward, in the second part we will focus on an alternative approach: the page/per_page method.

The page/per_page method allows users to specify precisely the page of data they want to access and the number of records displayed on each page. This level of detail not only increases user independence in terms of the content viewed, but also promotes more efficient use of server resources. In this article, we will dive into the implementation of this method in FastAPI, with the aim of achieving smooth and efficient pagination of data.

Page/per_page strategy

Implementation

Before going into the details of implementing the page/per_page method for pagination in FastAPI, it's important to have a solid foundation. If you haven't already done so, I highly recommend revisiting the first part of our tutorial series. There we detailed the initial steps necessary to set up your development environment and database. This foundation is essential as it provides the right infrastructure and understanding to successfully apply the pagination techniques we are about to explore.

In the following sections, we will take you through the process of configuring FastAPI endpoints to support page/per_page pagination. This will include customizing the API logic to handle page number and size parameters, integrating this functionality with database queries, and ensuring that the application can dynamically adapt to different user requests. By following these steps, you will be able to implement a solid pagination system that improves data management and user satisfaction.

For the purposes of simplicity and transparency, we will continue to work with the same files and data sets introduced in the first part of our guide. This decision ensures a smooth integration of the new pagination strategies without having to configure additional components or modify the existing infrastructure. Building on the foundations we have already created, we can directly focus on the implementation of the page/per_page method, adding new endpoints to our FastAPI application.

This approach not only simplifies the development process, but also makes it easier to directly compare the limit/offset strategy discussed earlier with the page/per_page method we intend to explore. Maintaining a consistent context allows us to clearly highlight the advantages and considerations of each pagination strategy in the same application environment.

Ensure that you have used endpoint http://127.0.0.1:8000/movie to populate the database before starting work on the new pagination method.

Preparing Pydantic model

In our tutorial today, we used the Pydantic model that was used for the limit/offset strategy and will adapt it to page/per_page.

The addition of the PaginatedResponseP class marks an evolution towards a page/per_page pagination approach. Like its precursor, this model contains fields in line with the requirements of the pagination strategy: page, per_page, totalItems, nextPageUrl, prevPageUrl and a general results list. Unlike limit/offset pagination, the page field specifies the current page number and the per_page defines the number of items to display on a page, providing a user-friendly data navigation mechanism that allows users to request specific pages directly.

Both paginated response models use the Field Pydantic feature to describe fields, increasing the understandability of the API data structures for developers. The inclusion of the Generic[T] type in these models highlights their universality, allowing the pagination structure to be applied to a variety of data types, therefore increasing the reusability and flexibility of the API to adapt to different entities. This design choice highlights a thoughtful approach to API development, prioritizing adaptability and developer experience.

models.py
from typing import Generic, List, Optional, TypeVar from pydantic import AnyHttpUrl, BaseModel, Field T = TypeVar("T") class Movie(BaseModel): id: int title: str description: str director: str year: int class PaginatedResponse(BaseModel, Generic[T]): limit: int = Field(description="Number of items returned in the response") offset: int = Field(description="Index of the first item returned in the response") totalItems: int = Field(description="Total number of items in the database") nextPageUrl: Optional[AnyHttpUrl] = None Field(description="URL to the next page of results if available, otherwise null") prevPageUrl: Optional[AnyHttpUrl] = None Field( description="URL to the previous page of results if available, otherwise null" ) results: List[T] = Field( description="List of items returned in response according to the provided parameters" ) class PaginatedResponseP(BaseModel, Generic[T]): page: int = Field(description="The current page number being displayed") per_page: int = Field(description="The number of items displayed on each page") totalItems: int = Field(description="Total number of items in the database") nextPageUrl: Optional[AnyHttpUrl] = None Field(description="URL to the next page of results if available, otherwise null") prevPageUrl: Optional[AnyHttpUrl] = None Field( description="URL to the previous page of results if available, otherwise null" ) results: List[T] = Field( description="List of items returned in response according to the provided parameters" )

Creating Endpoint

The core of the update is the addition of an asynchronous utility function, paginate_p, designed to simplify pagination based on page numbers and elements per page rather than offset and limit parameters. This function calculates an offset based on the given page number and per_page value, sends a query to the database to retrieve the relevant piece of video data and dynamically generates URLs for navigation to the next and previous page based on the current page and per_page settings. Including the total number of elements in the response helps to understand the scope of the data and plan navigation.

The get_all_movies_p endpoint then uses this paginate_p function to offer an API route to access paginated movie data. Users can specify the page number and the desired number of items per page via query parameters, making the retrieval more intuitive and responsive. This method simplifies the process of navigating large datasets by allowing direct access to specific pages and adjusting the amount of data returned in each request.

main.py
import json import sqlalchemy from fastapi import FastAPI, Query, Request from database import database, movie_table from models import Movie, PaginatedResponse, PaginatedResponseP app = FastAPI() @app.get("/") def home(): return {"Hello": "World"} @app.post("/movies", response_model=dict, status_code=201) async def load_movies_to_database(): with open("movies_examples.json", "r") as f: data = json.load(f) await database.connect() for movie in data: query = movie_table.select().where(movie_table.c.id == movie["id"]) result = await database.fetch_one(query) if result is None: query = movie_table.insert().values(movie) await database.execute(query) await database.disconnect() return {"status": "Movies loaded successfully"} async def paginate_l( request: Request, limit: int, offset: int ) -> PaginatedResponse[Movie]: await database.connect() query = movie_table.select().limit(limit).offset(offset) movies = await database.fetch_all(query) count_query = sqlalchemy.select(sqlalchemy.func.count()).select_from(movie_table) total = await database.fetch_one(count_query) base_url = str(request.base_url) next_page = ( f"{base_url}movie?limit={limit}&offset={offset + limit}" if offset + limit < total[0] else None ) prev_page = ( f"{base_url}movie?limit={limit}&offset={max(0, offset - limit)}" if offset - limit >= 0 else None ) await database.disconnect() return { "limit": limit, "offset": offset, "totalItems": total[0], "nextPageUrl": next_page, "prevPageUrl": prev_page, "results": movies, } @app.get("/movie", response_model=PaginatedResponse[Movie], status_code=200) async def get_all_movies( request: Request, limit: int = Query(10, gt=0), offset: int = Query(0, ge=0), ): return await paginate_l(request, limit, offset) async def paginate_p( request: Request, page: int, per_page: int ) -> PaginatedResponseP[Movie]: offset = (page - 1) * per_page await database.connect() query = movie_table.select().limit(per_page).offset(offset) movies = await database.fetch_all(query) count_query = sqlalchemy.select(sqlalchemy.func.count()).select_from(movie_table) total = await database.fetch_one(count_query) base_url = str(request.base_url) next_page = ( f"{base_url}moviep?page={page + 1}&per_page={per_page}" if offset + per_page < total[0] else None ) prev_page = ( f"{base_url}moviep?page={page - 1}&per_page={per_page}" if page > 1 else None ) await database.disconnect() return { "page": page, "per_page": per_page, "totalItems": total[0], "nextPageUrl": next_page, "prevPageUrl": prev_page, "results": movies, } @app.get("/moviep", response_model=PaginatedResponseP[Movie], status_code=200) async def get_all_movies_p( request: Request, page: int = Query(1, gt=0), per_page: int = Query(10, gt=0), ): return await paginate_p(request, page, per_page)

Now when we use our new endpoint with query params such as page equal to 3 and per_page equal to 4 (http://127.0.0.1:8000/moviep?page=3&per_page=4), the response should be similar to the one on the screen below:

pagination_fastapi_pt2_screenshot_1

Summary

Pagination using FastAPI's page/per_page strategy offers an intuitive and simple method for users to navigate large datasets. This approach allows users to directly specify which page of data they want to access and the number of elements to be displayed on each page, making it particularly suitable for applications where the concept of 'pages' is integral to the user experience, such as content management systems, e-commerce platforms or social media channels.

One of the main advantages of this strategy is its intuitive nature, which allows users to easily understand and navigate the data. Each page has a consistent number of elements, which increases predictability and user satisfaction. Additionally, the simplicity of specifying page numbers and sizes in the query parameters eliminates the need to manually calculate offsets, simplifying the creation and use of API endpoints.

However, the page/per_page strategy is not without disadvantages, especially when compared to the limit/offset approach. Performance issues can appear in the case of very large datasets, as it can be more computationally intensive to calculate the starting point for data searches on high page numbers. This problem comes from the need to count records in the database to determine page offsets, which may not be as simple as using a simple offset. Furthermore, this pagination method offers less flexibility to retrieve specific ranges of data that do not conform to predefined page boundaries. Users who wish to access a custom slice of data may find the limit/offset method more appropriate. Finally, in environments where the dataset is highly dynamic with frequent additions or deletions, page-based navigation may lead to inconsistencies such as potentially missing or encountering duplicate elements as the composition of pages changes over time.

In general, while the page/per_page pagination strategy improves user experience by providing an easy-to-navigate and predictable presentation of the data, it encounters limitations in terms of performance for large datasets and flexibility in data retrieval. The choice between page/per_page and limit/offset strategies should be determined by the specific needs of the application, taking into account factors such as the size of the dataset, the design of the user interface and the dynamic nature of the data.

Thank you for taking the time to read this article on implementing page/per_page pagination strategies in FastAPI. We hope it has provided you with valuable insights and practical knowledge to improve the user experience and performance of your web applications.

If you have any questions or would like further explanation on any of the topics we have covered, please do not hesitate to contact us via email. We are always happy to help and engage in discussions to help enrich understanding and support the sharing of knowledge by the community. Additionally, stay tuned for the upcoming third article in our pagination series. The whole code from the tutorial described here can be found in this repository.

Remember, taking care of yourself is as important as taking care of your code, so make sure to do both!

Bibliography

List of Sources and References

https://fastapi.tiangolo.com

https://github.com/Gamattowicz/Pagination_FastAPI