A generic, zero-dependency Go library for pagination and filtering. Provides type-safe paginated responses, a fluent filter builder, sort parsing, and a plug-and-play GORM adapter — all without boilerplate.
| 🔢 Generic responses | Result[T] works for any entity using Go generics |
| 🔍 Fluent filter builder | Compose WHERE clauses with a clean, chainable API |
Build and parse ORDER BY clauses from query strings |
|
| 🗃️ GORM adapter | Drop-in Scope() with automatic COUNT before LIMIT/OFFSET |
| 🛡️ Safe defaults | Normalizes page number, per-page, and enforces max limits |
| ⚡ Zero dependencies | Core package is pure Go; GORM adapter is opt-in |
go get github.com/raykavin/gopagefyRequires Go 1.21+.
package main
import (
"fmt"
"github.com/raykavin/gopagefy"
)
func main() {
page := gopagefy.Page{Number: 1, PerPage: 10}
filters := gopagefy.NewFilterBuilder().
Where("status", gopagefy.Eq, "active").
Where("amount", gopagefy.Gte, 100).
Build()
sorts := gopagefy.NewSortBuilder().
OrderBy("created_at", gopagefy.Desc).
Build()
query := gopagefy.NewQuery(page, filters, sorts)
// Simulate a paginated result
items := []string{"item1", "item2", "item3"}
result := gopagefy.NewResult(items, 42, query.Page)
fmt.Println(result.Page) // 1
fmt.Println(result.TotalPages) // 5
fmt.Println(result.HasNext) // true
}Page holds the pagination request and normalizes itself on demand.
page := gopagefy.Page{Number: 2, PerPage: 20}
page.Normalize() // enforces defaults and MaxPerPage
fmt.Println(page.Offset()) // 20Default values:
| Constant | Value |
|---|---|
DefaultPage |
1 |
DefaultPerPage |
20 |
MaxPerPage |
100 |
Result[T] is the generic paginated response. It works for any type — no type assertions needed.
type Transaction struct {
ID uint
Amount float64
}
rows := []Transaction{{ID: 1, Amount: 99.9}}
total := 150
result := gopagefy.NewResult(rows, total, page)
// result.Data → []Transaction
// result.Total → 150
// result.TotalPages → 8
// result.HasNext → true
// result.HasPrev → trueJSON response shape:
{
"data": [...],
"total": 150,
"page": 2,
"per_page": 20,
"total_pages": 8,
"has_next": true,
"has_prev": true
}Query aggregates Page, []Filter, and []Sort into a single object that travels through your application layers.
query := gopagefy.NewQuery(page, filters, sorts)
// query.Page → normalized Page
// query.Filters → []Filter
// query.Sorts → []SortBuild WHERE conditions with a fluent, readable API.
filters := gopagefy.NewFilterBuilder().
Where("status", gopagefy.Eq, "active").
Where("amount", gopagefy.Gte, 100).
Where("deleted_at", gopagefy.IsNull, nil).
WhereIf(userID != "", "user_id", gopagefy.Eq, userID). // only added if true
Build()| Operator | SQL |
|---|---|
Eq |
= |
Neq |
<> |
Gt |
> |
Gte |
>= |
Lt |
< |
Lte |
<= |
Like |
LIKE '%value%' |
ILike |
ILIKE '%value%' |
In |
IN (?) |
NotIn |
NOT IN (?) |
IsNull |
IS NULL |
IsNotNull |
IS NOT NULL |
WhereIf adds the filter only when the condition is true, keeping the builder clean without extra if blocks.
gopagefy.NewFilterBuilder().
WhereIf(req.Status != "", "status", gopagefy.Eq, req.Status).
WhereIf(req.MinAmount > 0, "amount", gopagefy.Gte, req.MinAmount).
WhereIf(req.Search != "", "description",gopagefy.ILike, req.Search).
Build()sorts := gopagefy.NewSortBuilder().
OrderBy("created_at", gopagefy.Desc).
OrderBy("name", gopagefy.Asc).
Build()Parse a sort string directly from a query parameter (e.g. ?sort=name+asc,created_at+desc):
sorts := gopagefy.ParseSort("name asc,created_at desc")
// []Sort{
// {Field: "name", Direction: Asc},
// {Field: "created_at", Direction: Desc},
// }gopagefy.Scope returns a GORM scope that applies filters, sorts, counts, and pagination in one call.
var rows []Transaction
var total int64
query := gopagefy.NewQuery(page, filters, sorts)
db.Model(&Transaction{}).
Scopes(gopagefy.Scope(query, &total)).
Find(&rows)
result := gopagefy.NewResult(rows, int(total), query.Page)The scope runs
COUNT(*)before applyingLIMITandOFFSET, sototalalways reflects the full dataset.
type ListRequest struct {
gopagefy.Page
Status string `form:"status"`
MinAmount string `form:"min_amount"`
Sort string `form:"sort"`
}
func (h *Handler) List(c *gin.Context) {
var req ListRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
minAmount, _ := strconv.ParseFloat(req.MinAmount, 64)
filters := gopagefy.NewFilterBuilder().
WhereIf(req.Status != "", "status", gopagefy.Eq, req.Status).
WhereIf(minAmount > 0, "amount", gopagefy.Gte, minAmount).
Build()
sorts := gopagefy.ParseSort(req.Sort)
if len(sorts) == 0 {
sorts = gopagefy.NewSortBuilder().
OrderBy("created_at", gopagefy.Desc).
Build()
}
query := gopagefy.NewQuery(req.Page, filters, sorts)
var rows []Transaction
var total int64
h.db.Model(&Transaction{}).
Scopes(gopagefy.Scope(query, &total)).
Find(&rows)
c.JSON(http.StatusOK, gopagefy.NewResult(rows, int(total), query.Page))
}gopagefy/
├── paginator.go # Page, Query, Offset, Normalize
├── filter.go # Filter, Sort, FilterBuilder, SortBuilder, ParseSort
├── paginator.go # Result[T], NewResult[T]
└── gorm.go # Scope() GORM adapter
Always call Normalize() (or use NewQuery()) before using Page, otherwise defaults won't be applied.
// ✅ Good
query := gopagefy.NewQuery(page, filters, sorts)
// ⚠️ Missing normalization
db.Limit(page.PerPage).Offset(page.Offset())Use WhereIf over manual conditionals to keep your filter chains readable.
// ✅ Good
fb.WhereIf(req.Status != "", "status", gopagefy.Eq, req.Status)
// ❌ Avoid
if req.Status != "" {
fb.Where("status", gopagefy.Eq, req.Status)
}Pass Query across layers instead of individual Page, []Filter, []Sort to keep function signatures clean.
// ✅ Good
func (r *repo) List(ctx context.Context, q gopagefy.Query) ([]T, int, error)
// ❌ Avoid
func (r *repo) List(ctx context.Context, page int, perPage int, filters []Filter, ...) Contributions to gopagefy are welcome! Here are some ways you can help improve the project:
- Report bugs and suggest features by opening issues on GitHub
- Submit pull requests with bug fixes or new features
- Improve documentation to help other users and developers
- Share your custom strategies with the community
gopagefy is distributed under the MIT License.
For complete license terms and conditions, see the LICENSE file in the repository.
For support, collaboration, or questions about gopagefy:
Email: raykavin.meireles@gmail.com
GitHub: @raykavin