Offset-Based Pagination
Classic page-number pagination with total count metadata.
Query Parameters
| Parameter | Default | Description |
|---|---|---|
page | 1 | Page number (1-indexed) |
size | 10 | Items per page (max 1000) |
sort | — | Sort 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})
SortableFields(...)removes any sort field not in the whitelistWithDefaultSort(...)applies a fallback sort when no valid sorts remain
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)
}