Skip to content

vidinfra/paginate

Repository files navigation

paginate

A type-safe, configurable pagination library for Go web applications using Gin and GORM.

✨ Features

  • 🔷 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

🚀 Quick Start

Installation

go get github.com/vidinfra/paginate

🧩 Example Models

type 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"`
}

📄 Basic Usage

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)
})

🔗 With Relations (Preload)

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)
})

⚙️ Custom Configuration

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
)

📊 Response Format

{
  "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"
  }
}

🔍 Filtering + Sorting Example

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"),
)

🛡️ Error Handling

  • Invalid page number → Falls back to default
  • Invalid page size → Enforces min/max bounds
  • DB errors bubble up
  • Defaults validated before query

About

A clean way to navigate large datasets in Go — paginate through data like turning pages in a book.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors