Pagination
Introduction
When dealing with large datasets, it's often impractical to load all records at once. Pagination allows you to fetch a subset of records, making data retrieval more efficient and user-friendly. Expanse provides built-in support for pagination of SQLAlchemy queries in an intuitive way which does not require any configuration by default.
Expanse provides two types of pagination out of the box: offset-based and cursor-based pagination.
Offset-based pagination
Offset-based pagination is the most common type of pagination. It uses an offset and a limit to fetch a subset of records.
To start using offset-based pagination, you can use the paginate method on a query object. It will automatically
configure the query's offset and limit based on the request's query parameters. By default, it uses the page query
parameter to detect the current page.
from typing import Annotated
from expanse.database.session import Session
from expanse.pagination.paginator import Paginator
from pydantic import BaseModel
from sqlalchemy import select
from app.models.user import User
class UserSchema(BaseModel):
id: int
name: str
email: str
class UserController:
def index(
self, session: Session
) -> Paginator[Annotated[User, UserSchema]]:
paginator = session.paginate(
select(User).order_by(User.id.desc()),
per_page=20
)
return paginator
from typing import Annotated
from expanse.database.session import AsyncSession
from expanse.pagination.paginator import Paginator
from pydantic import BaseModel
from sqlalchemy import select
from app.models.user import User
class UserSchema(BaseModel):
id: int
name: str
email: str
class UserController:
async def index(
self, session: AsyncSession
) -> Paginator[Annotated[User, UserSchema]]:
paginator = await session.paginate(
select(User).order_by(User.id.desc()),
per_page=20
)
return paginator
In this example, we only need to specify the per_page parameter to define how many records we want to fetch per page,
Expanse will take care of the rest, which includes the proper serialization of the paginated response.
{
"data": [
{
"id": 100,
"name": "User 100",
"email": "[email protected]"
}
// ...
],
"total": 250,
"per_page": 20,
"current_page": 1,
"last_page": 13,
"links": {
"first": "http://example.com/users?page=1",
"last": "http://example.com/users?page=13",
"prev": null,
"next": "http://example.com/users?page=2",
"self": "http://example.com/users?page=1"
}
}
This is the default, envelope-style response for paginated data. An alternative, header-based response is also supported.
The page the paginator needs to fetch is determined by the page query parameter in the request URL. For example,
to fetch the second page of results, you would make a request to:
http://example.com/users?page=2
Controlling the paginated response
By default, paginators are serialized using a thorough, envelope-style response. However, you can customize the response format to make it leaner or by using an alternative, header-based response.
Envelope-based response
The default envelope response includes a complete set of pagination metadata, such as total records, current page, last
page, etc.
However, you might not need all this information in every response. You can customize the envelope response by
annotating the Paginator with the Envelope variant.
from expanse.pagination.offset.adapters.envelope import Envelope
def index(
self, session: Session
) -> Annotated[
Paginator[Annotated[User, UserSchema]],
Envelope(with_links=False)
]:
...
Here are the available options for the Envelope adapter:
with_links: Whether to include pagination links in the response. Default isTrue.
Header-based response
In some cases, you might prefer to include pagination metadata in HTTP headers instead of the response body. You can
achieve this by using the Header adapter.
from expanse.pagination.offset.adapters.header import Header
def index(
self, session: Session
) -> Annotated[
Paginator[Annotated[User, UserSchema]],
Header()
]:
...
When using the Header adapter, the response body will contain only the paginated data as a list of items, while pagination metadata
will be included in the HTTP headers. Here are the headers that will be included in the response:
HTTP/1.1 200 OK
Content-Type: application/json
X-Total-Count: 250
Link: <http://example.com/users?page=1>; rel="first",
<http://example.com/users?page=13>; rel="last",
<http://example.com/users?page=2>; rel="next",
<http://example.com/users?page=1>; rel="self"
[
{
"id": 100,
"name": "User 100",
"email": "[email protected]"
}
// ...
]
X-Total-Count: The otal number of records.Link: Pagination links for the first (first), the last (last), the previous (prev), the next (next), and the current (self) pages.
Cursor-based pagination
Cursor-based pagination is an alternative to offset-based pagination that uses a cursor to keep track of the current
position in the dataset. A cursor is converted to a WHERE clause to fetch the next set of records instead of an OFFSET
clause to skip records. This is the most efficient way to paginate large datasets, and is particularly suited for
infinite scrolling interfaces.
To start using cursor-based pagination, you can use the cursor_paginate method on a query object.
from typing import Annotated
from expanse.database.session import Session
from expanse.pagination.cursor.paginator import CursorPaginator
from pydantic import BaseModel
from sqlalchemy import select
from app.models.user import User
class UserSchema(BaseModel):
id: int
name: str
email: str
class UserController:
def index(
self, session: Session
) -> CursorPaginator[Annotated[User, UserSchema]]:
paginator = session.cursor_paginate(
select(User).order_by(User.id.desc()),
per_page=20,
)
return paginator
from typing import Annotated
from expanse.database.session import AsyncSession
from expanse.pagination.cursor.paginator import CursorPaginator
from pydantic import BaseModel
from sqlalchemy import select
from app.models.user import User
class UserSchema(BaseModel):
id: int
name: str
email: str
class UserController:
async def index(
self, session: AsyncSession
) -> CursorPaginator[Annotated[User, UserSchema]]:
paginator = await session.cursor_paginate(
select(User).order_by(User.id.desc()),
per_page=20,
)
return paginator
In this example, we only need to specify the per_page parameter to define how many records we want to fetch per page,
Expanse will take care of the rest, which includes the proper serialization of the paginated response.
{
"data": [
{
"id": 100,
"name": "User 100",
"email": "[email protected]"
}
// ...
],
"next_cursor": "eyJwYXJhbWV0ZXJzIjogeyJpZCI6IDJ9LCAicmV2ZXJzZWQiOiBmYWxzZX0=",
"previous_cursor": null,
"links": {
"next": "http://example.com/users?cursor=eyJwYXJhbWV0ZXJzIjogeyJpZCI6IDJ9LCAicmV2ZXJzZWQiOiBmYWxzZX0=",
"prev": null,
"self": "http://example.com/users"
}
}
This is the default, envelope-style response for cursor-paginated data. An alternative, header-based response is also supported.
The cursor the paginator needs to fetch is determined by the cursor query parameter in the request URL. For example,
to fetch the next page of results, you would make a request to:
http://example.com/users?cursor=eyJwYXJhbWV0ZXJzIjogeyJpZCI6IDJ9LCAicmV2ZXJzZWQiOiBmYWxzZX0=
Cursor-based pagination requires that the query has a deterministic ordering. Make sure to include an order_by clause
in your query and that it uniquely identifies each record.
Also, the ordering columns must belong to the table being paginated.
Controlling the paginated response
By default, paginators are serialized using a thorough, envelope-style response. However, you can customize the response format to use an alternative, header-based response.
Header-based response
In some cases, you might prefer to include pagination metadata in HTTP headers instead of the response body. You can
achieve this by using the Header adapter.
from expanse.pagination.cursor.adapters.header import Header
def index(
self, session: Session
) -> Annotated[
CursorPaginator[Annotated[User, UserSchema]],
Header()
]:
...
When using the Header adapter, the response body will contain only the paginated data as a list of items, while pagination metadata
will be included in the HTTP headers. Here are the headers that will be included in the response:
HTTP/1.1 200 OK
Content-Type: application/json
X-Total-Count: 250
Link: <http://example.com/users?cursor=eyJwYXJhbWV0ZXJzIjogeyJpZCI6IDJ9LCAicmV2ZXJzZWQiOiBmYWxzZX0%30>; rel="next",
<http://example.com/users>; rel="self"
[
{
"id": 100,
"name": "User 100",
"email": "[email protected]"
}
// ...
]
Link: Pagination links for the previous (prev), the next (next), and the current (self) pages.