Offset-Based Pagination

Classic page-number pagination with total count metadata.

Query Parameters

ParameterDefaultDescription
page1Page number (1-indexed)
size10Items per page (max 1000)
sortSort field: field,direction (repeatable)

Example: ?page=2&size=20&sort=name,desc&sort=id,asc

Parsing Requests

From Query Parameters

func listUsers(w http.ResponseWriter, r *http.Request) {
    req := pageable.PageRequestFromQuery(r.URL.Query())
    // req.Page = 2, req.Size = 20, req.Sort = [{name desc} {id asc}]
}

Invalid values are clamped to defaults — no error handling needed.

Manual Construction

req := pageable.NewPageRequest(1, 20, nil)

Sort Safety

Whitelist allowed sort fields to prevent injection of arbitrary column names:

req := pageable.PageRequestFromQuery(r.URL.Query()).
    SortableFields("id", "name", "created_at").
    WithDefaultSort(pageable.Sort{Field: "id", Direction: pageable.ASC})

Database Helpers

req.Offset()  // (Page - 1) * Size, e.g. 20 for page 2, size 20
req.Limit()   // Size, e.g. 20
req.OrderBy() // "name desc, id asc"

Use directly in SQL queries:

query := fmt.Sprintf(
    "SELECT * FROM users ORDER BY %s LIMIT $1 OFFSET $2",
    req.OrderBy(),
)
rows, err := db.Query(query, req.Limit(), req.Offset())
Always use SortableFields() before passing OrderBy() to SQL queries to prevent SQL injection.

Building Responses

users, total := queryUsers(req.Offset(), req.Limit(), req.OrderBy())

page := pageable.NewPage(users, req, total)

NewPage automatically computes totalPages using ceiling division. A nil items slice is converted to an empty slice so JSON serializes as [] not null.

Response Type

type Page[T any] struct {
    Items    []T          `json:"items"`
    Metadata PageMetadata `json:"metadata"`
}

type PageMetadata struct {
    Page       int   `json:"page"`
    Size       int   `json:"size"`
    TotalItems int64 `json:"totalItems"`
    TotalPages int   `json:"totalPages"`
}

JSON Output

{
  "items": [
    {"id": 21, "name": "Alice"},
    {"id": 22, "name": "Bob"}
  ],
  "metadata": {
    "page": 2,
    "size": 20,
    "totalItems": 95,
    "totalPages": 5
  }
}

Full Example

package main

import (
    "encoding/json"
    "net/http"

    "github.com/ishinvin/pageable"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func listUsers(w http.ResponseWriter, r *http.Request) {
    req := pageable.PageRequestFromQuery(r.URL.Query()).
        SortableFields("id", "name", "created_at").
        WithDefaultSort(pageable.Sort{Field: "id", Direction: pageable.ASC})

    users, total := queryUsers(req.Offset(), req.Limit(), req.OrderBy())

    page := pageable.NewPage(users, req, total)

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(page)
}