Skip to content

Pagination

Burrow provides offset-based pagination utilities for numbered pages. All types and functions are in the root burrow package.

Types

PageRequest

Parsed from query parameters via ParsePageRequest(r):

type PageRequest struct {
    Limit int // items per page (default 20, max 100)
    Page  int // 1-based page number for offset-based pagination
}

Query parameters: ?limit=20&page=2.

PageResult

Returned alongside query results:

type PageResult struct {
    HasMore    bool // convenience: more pages exist
    Page       int  // current page number (1-based)
    TotalPages int  // total number of pages
    TotalCount int  // total number of items
}

Offset-Based Pagination

Best for admin panels, tables, and infinite scroll where users need to jump to specific pages or load the next page.

QuerySet Pagination

Use Den's QuerySet API with Limit and Skip for pagination:

qs := den.NewQuerySet[Note](db, conditions...).
    Limit(pr.Limit).
    Skip(pr.Offset())

Building Results

OffsetResult computes page metadata from a total count:

pageResult := burrow.OffsetResult(pr, totalCount)

Full Example

func (r *Repository) ListAllPaged(ctx context.Context, pr burrow.PageRequest) ([]Note, burrow.PageResult, error) {
    count, err := den.NewQuery[Note](r.db).Count(ctx)
    if err != nil {
        return nil, burrow.PageResult{}, err
    }

    notes, err := den.NewQuery[Note](r.db).
        Sort("created_at", den.Desc).
        Limit(pr.Limit).
        Skip(pr.Offset()).
        All(ctx)
    if err != nil {
        return nil, burrow.PageResult{}, err
    }

    return notes, burrow.OffsetResult(pr, count), nil
}

Handler

func (a *App) AdminList(w http.ResponseWriter, r *http.Request) error {
    pr := burrow.ParsePageRequest(r)
    notes, page, err := a.repo.ListAllPaged(r.Context(), pr)
    if err != nil {
        return burrow.NewHTTPError(http.StatusInternalServerError, "failed to list notes")
    }
    // pass page to template...
}

Building Pagination URLs

PageURL builds a URL that preserves existing query parameters (search terms, filters, sort order) and sets the page parameter. This prevents pagination links from dropping the current query state.

func PageURL(basePath, rawQuery string, page int) string
// In a handler:
rawQuery := r.URL.RawQuery // e.g. "q=alpha&sort=-created_at"
url := burrow.PageURL("/notes", rawQuery, 3)
// => "/notes?page=3&q=alpha&sort=-created_at"

In templates, pass the current query string and use pageURL to generate links:

{{- $base := "/notes" }}
{{- $query := .Query }}
{{- range pageNumbers .Page.Page .Page.TotalPages }}
<a href="{{ pageURL $base $query . }}">{{ . }}</a>
{{- end }}

Built-in Pagination Component

contrib/admin ships a ready-made Tailwind-classed pagination nav as admin/pagination. Call it from any template by passing a struct or map with BasePath, RawQuery, and Page:

{{ template "admin/pagination" (dict "BasePath" "/notes" "RawQuery" .RawQuery "Page" .Page) }}

This renders a <nav> with numbered page links, previous/next buttons, and ellipsis for large page counts. The current page is marked with aria-current="page". Query parameters (search terms, filters) are preserved in pagination links.

JSON API

The JSON CRUD APIs layer wraps results in PageResponse for you (offset and cursor); reach for the helpers below when hand-writing endpoints.

For JSON APIs, wrap results with PageResponse:

type PageResponse[T any] struct {
    Items      []T        `json:"items"`
    Pagination PageResult `json:"pagination"`
}
func (h *Handler) ListAPI(w http.ResponseWriter, r *http.Request) error {
    pr := burrow.ParsePageRequest(r)
    items, page, err := a.repo.ListPaged(r.Context(), pr)
    if err != nil {
        return err
    }
    return burrow.JSON(w, http.StatusOK, burrow.PageResponse[Item]{
        Items:      items,
        Pagination: page,
    })
}