A type-safe, configurable pagination library for Go web applications using Gin and GORM.
- 🔷 Type-safe – Uses Go generics for compile-time type safety
- ⚙️ Configurable – Flexible options for pagination limits and defaults
- 🔗 HATEOAS – Automatic generation of navigation links
- 🛡️ Robust – Comprehensive error handling and input validation
- 🎯 Easy to use – Simple, intuitive API
- 📦 Modular – Split into multiple small files for clarity
go get github.com/vidinfra/paginatetype Profile struct {
ID uint
UserID uint
Bio string
}
type Order struct {
ID uint
UserID uint
Item string
}
type User struct {
ID uint `gorm:"primaryKey"`
Name string
Email string
Profile Profile `gorm:"foreignKey:UserID"`
Orders []Order `gorm:"foreignKey:UserID"`
}r.GET("/users", func(c *gin.Context) {
base := db.WithContext(c.Request.Context()).
Model(&User{})
result, err := paginate.New[User](c, base)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
})If you previously used Bun’s .Relation() method, here’s how to achieve the
same with GORM:
r.GET("/users", func(c *gin.Context) {
base := db.WithContext(c.Request.Context()).
Model(&User{}).
Preload("Profile"). // simple preload
Preload("Orders", func(tx *gorm.DB) *gorm.DB { // scoped preload
return tx.Order("orders.id DESC")
})
result, err := paginate.New[User](c, base,
paginate.WithBounds(1, 200),
paginate.WithDefaultSize(20),
paginate.WithDefaultPage(1),
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
})result, err := paginate.New[User](c, base,
paginate.WithBounds(1, 50), // min: 1, max: 50
paginate.WithDefaultSize(15), // default page size: 15
paginate.WithDefaultPage(1), // default start page: 1
){
"data": [
{
"id": 1,
"name": "John",
"email": "john@example.com",
"profile": { "id": 1, "bio": "Loves Go" },
"orders": [
{ "id": 10, "item": "Book" },
{ "id": 9, "item": "Laptop" }
]
}
],
"meta": {
"page": 1,
"page_size": 10,
"total": 100,
"total_pages": 10
},
"links": {
"self": "/users?page=1&limit=10",
"first": "/users?page=1&limit=10",
"next": "/users?page=2&limit=10",
"last": "/users?page=10&limit=10"
}
}Works great with a filter builder:
fb := filter.New(c, base).
AllowFields("name", "email", "profile.bio").
AllowSorts("name", "created_at").
Apply()
if fb.HasErrors() {
c.JSON(400, fb.Result().ToJSONResponse())
return
}
result, err := paginate.New[User](c, fb.Query().
Preload("Profile").
Preload("Orders"),
)- Invalid page number → Falls back to default
- Invalid page size → Enforces min/max bounds
- DB errors bubble up
- Defaults validated before query