Skip to content

Pagination Pattern

Standardized pagination for list endpoints.

Overview

flowchart LR
    A[Request] -->|page, pageSize| B[Query]
    B --> C[Count Total]
    B --> D[Fetch Page]
    C --> E[Response]
    D --> E
    E -->|Data + Headers| F[Client]

Response Format

Body

{
  "data": [...],
  "pageNumber": 1,
  "pageSize": 10,
  "totalCount": 57,
  "totalPages": 6
}

Headers

Header Value
X-Pagination-PageNumber Current page
X-Pagination-PageSize Items per page
X-Pagination-TotalCount Total items
X-Pagination-TotalPages Total pages

PaginatedResponse

public class PaginatedResponse<T>
{
    public IList<T> Data { get; init; }
    public int PageNumber { get; init; }
    public int PageSize { get; init; }
    public int TotalCount { get; init; }
    public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
}

Query Implementation

public async Task<PaginatedResponse<TransactionResponse>> Handle(
    GetPaginatedTransactionsQuery request,
    CancellationToken ct)
{
    var query = context.Transactions
        .Where(t => t.GroupId == request.GroupId)
        .Where(t => !t.IsDeleted);

    // Apply filters
    if (request.CategoryId.HasValue)
        query = query.Where(t => t.CategoryId == request.CategoryId);

    // Get total before pagination
    var totalCount = await query.CountAsync(ct);

    // Apply pagination
    var data = await query
        .OrderByDescending(t => t.Date)
        .Skip((request.PageNumber - 1) * request.PageSize)
        .Take(request.PageSize)
        .Select(t => t.ToResponse())
        .ToListAsync(ct);

    return new PaginatedResponse<TransactionResponse>
    {
        Data = data,
        PageNumber = request.PageNumber,
        PageSize = request.PageSize,
        TotalCount = totalCount
    };
}

Controller Helper

protected async Task<IActionResult> ExecuteWithPaginationAsync<TData>(
    IRequest<PaginatedResponse<TData>> request)
{
    var result = await mediator.Send(request);

    Response.Headers["X-Pagination-PageNumber"] = result.PageNumber.ToString();
    Response.Headers["X-Pagination-PageSize"] = result.PageSize.ToString();
    Response.Headers["X-Pagination-TotalCount"] = result.TotalCount.ToString();
    Response.Headers["X-Pagination-TotalPages"] = result.TotalPages.ToString();

    return Ok(result);
}

Client Usage (TanStack Query)

const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
  queryKey: ["transactions", filters],
  queryFn: ({ pageParam = 1 }) =>
    fetchTransactions({ ...filters, page: pageParam }),
  getNextPageParam: (lastPage) =>
    lastPage.pageNumber < lastPage.totalPages
      ? lastPage.pageNumber + 1
      : undefined,
});

Default Page Sizes

Endpoint Default Size
Transactions 10
Markers (sidebar) 20
Users (admin) 20