Skip to content
loco
v1.0
★ 6.9k Get started

Paginate query results

Goal: return a page of rows plus paging metadata (total_pages, total_items, …) from a controller endpoint, instead of loading an entire table.

This builds on Query data with the condition DSL. For exact signatures and the PagerMeta shape, see Query DSL & pagination.

1. Accept pagination params in your request

PaginationQuery is a small, #[serde(flatten)]-friendly struct with two fields, page (1-based, default 1) and page_size (default 25). Flatten it into your controller’s own query-params struct so callers can pass ?page=2&page_size=10 alongside your own filters:

use loco_rs::prelude::*;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct ListQueryParams {
pub title: Option<String>,
#[serde(flatten)]
pub pagination: query::PaginationQuery,
}

2. Paginate an entity query with an optional condition

query::paginate takes a Select<E> (an unresolved entity query), an optional pre-built Condition, and a &PaginationQuery. It applies the condition for you, so don’t call .filter() yourself first:

use axum::extract::{Query, State};
pub async fn list(
State(ctx): State<AppContext>,
Query(params): Query<ListQueryParams>,
) -> Result<Response> {
let condition = params
.title
.as_ref()
.map(|t| query::condition().contains(posts::Column::Title, t).build());
let res = query::paginate(
&ctx.db,
posts::Entity::find(),
condition,
&params.pagination,
)
.await?;
format::json(res)
}

3. Or paginate a pre-built selector with fetch_page

Use fetch_page when you’ve already composed a Select (with your own .filter()/.order_by()/joins) and just need to page over it — it takes no separate condition argument:

let selector = posts::Entity::find()
.filter(query::condition().eq(posts::Column::UserId, user_id).build())
.order_by_desc(posts::Column::CreatedAt);
let res = query::fetch_page(&ctx.db, selector, &query::PaginationQuery::page(2)).await?;

4. What you get back

Both functions return LocoResult<PageResponse<T>>:

pub struct PageResponse<T> {
pub page: Vec<T>,
pub meta: PagerMeta,
}
pub struct PagerMeta {
pub page: u64,
pub page_size: u64,
pub total_pages: u64,
pub total_items: u64,
}

Returning res from a controller (e.g. via format::json(res)) serializes to:

{
"page": [ { "id": 1, "title": "..." }, ... ],
"meta": { "page": 2, "page_size": 25, "total_pages": 4, "total_items": 87 }
}

Result

A request like GET /posts?page=2&page_size=10&title=loco returns exactly one page of matching rows plus enough metadata for a client to render “page 2 of 4” or build next/prev links — without loading the whole table or hand-writing OFFSET/LIMIT math. Remember page is 1-based on the way in; both functions handle the conversion to Sea-ORM’s 0-based paging internally.

Next