A powerful, flexible, and production-ready pagination library for Go with GORM integration. Built with modern Go practices including generics, this library provides multiple patterns to implement pagination in your applications with built-in support for searching, sorting, filtering, relationships, and database security.
- 🚀 Generic Support: Full support for Go generics for type safety
- 🔍 Smart Search: Automatic search across multiple fields with database optimization
- 🗂️ Advanced Filtering: Dynamic filters with custom operators and validation
- 🔗 Relationship Support: Easy preloading with security validation
- 🛢️ Multi-Database: MySQL, PostgreSQL, SQLite, and SQL Server support
- 🛡️ Security First: SQL injection protection and include validation
- ⚡ High Performance: Optimized queries with efficient counting
- 🧪 Production Ready: Comprehensive test coverage and real-world examples
- 📚 Multiple Patterns: From simple one-liners to complex builders
- 🌐 API Ready: Built-in response formatting for REST APIs
go get github.com/Caknoooo/go-pagination
- Quick Start
- Advanced Filtering
- Relationship Loading
- Search Functionality
- Sorting Examples
- Security Features
- URL Parameters
- Response Format
- Real Examples
- Performance Tips
Perfect for getting started quickly with minimal setup:
package main
import (
"github.com/Caknoooo/go-pagination"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name"`
Email string `json:"email"`
}
func GetUsers(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
// 🎯 One line pagination with automatic search!
response := pagination.PaginatedAPIResponse[User](
db, c, "users",
[]string{"name", "email"}, // fields to search in
"Users retrieved successfully",
)
c.JSON(response.Code, response)
}
}
func main() {
r := gin.Default()
r.GET("/users", GetUsers(db))
r.Run(":8080")
}
Try these URLs:
# Basic pagination
curl "http://localhost:8080/users?page=1&per_page=10"
# Search in name and email fields
curl "http://localhost:8080/users?search=john&page=1&per_page=10"
# Sort by name descending
curl "http://localhost:8080/users?sort=name,desc&page=1&per_page=10"
# Combined: search + sort + pagination
curl "http://localhost:8080/users?search=admin&sort=id,desc&page=2&per_page=5"
Create powerful, reusable filters with automatic validation:
type UserFilter struct {
pagination.BaseFilter
ID int `json:"id" form:"id"`
Name string `json:"name" form:"name"`
Email string `json:"email" form:"email"`
IsActive *bool `json:"is_active" form:"is_active"`
Role string `json:"role" form:"role"`
MinAge int `json:"min_age" form:"min_age"`
MaxAge int `json:"max_age" form:"max_age"`
}
// Custom filter implementation
func (f *UserFilter) ApplyFilters(query *gorm.DB) *gorm.DB {
if f.ID > 0 {
query = query.Where("id = ?", f.ID)
}
if f.Name != "" {
query = query.Where("name LIKE ?", "%"+f.Name+"%")
}
if f.Email != "" {
query = query.Where("email LIKE ?", "%"+f.Email+"%")
}
if f.IsActive != nil {
query = query.Where("is_active = ?", *f.IsActive)
}
if f.Role != "" {
query = query.Where("role = ?", f.Role)
}
if f.MinAge > 0 {
query = query.Where("age >= ?", f.MinAge)
}
if f.MaxAge > 0 {
query = query.Where("age <= ?", f.MaxAge)
}
return query
}
// Define searchable fields (will be used for global search)
func (f *UserFilter) GetSearchFields() []string {
return []string{"name", "email", "phone"}
}
func (f *UserFilter) GetTableName() string {
return "users"
}
func (f *UserFilter) GetDefaultSort() string {
return "id asc"
}
// Handler using the custom filter
func GetUsersWithFilter(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
var filter UserFilter
if err := pagination.BindPagination(c, &filter); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
users, total, err := pagination.PaginatedQueryWithFilter[User](db, &filter)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
paginationResponse := pagination.CalculatePagination(filter.GetPagination(), total)
response := pagination.NewPaginatedResponse(200, "Users retrieved successfully", users, paginationResponse)
c.JSON(200, response)
}
}
Advanced filtering examples:
# Filter by specific user ID
curl "http://localhost:8080/users?id=123"
# Filter by name pattern
curl "http://localhost:8080/users?name=john&page=1&per_page=10"
# Filter by role and status
curl "http://localhost:8080/users?role=admin&is_active=true"
# Age range filtering
curl "http://localhost:8080/users?min_age=18&max_age=65"
# Complex combined filtering
curl "http://localhost:8080/users?role=user&is_active=true&min_age=25&search=developer&sort=name,asc"
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name"`
Email string `json:"email"`
Profile Profile `json:"profile,omitempty" gorm:"foreignKey:UserID"`
Posts []Post `json:"posts,omitempty" gorm:"foreignKey:UserID"`
Orders []Order `json:"orders,omitempty" gorm:"foreignKey:UserID"`
}
type Profile struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
Bio string `json:"bio"`
Avatar string `json:"avatar"`
}
type Post struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
Title string `json:"title"`
Content string `json:"content"`
}
type UserFilter struct {
pagination.BaseFilter
Name string `json:"name" form:"name"`
Status string `json:"status" form:"status"`
}
// Implement IncludableQueryBuilder interface
func (f *UserFilter) GetIncludes() []string {
return f.Includes
}
func (f *UserFilter) GetPagination() pagination.PaginationRequest {
return f.Pagination
}
func (f *UserFilter) Validate() {
var validIncludes []string
allowedIncludes := f.GetAllowedIncludes()
for _, include := range f.Includes {
if allowedIncludes[include] {
validIncludes = append(validIncludes, include)
}
}
f.Includes = validIncludes
}
// 🛡️ Security: Define which relationships can be loaded
func (f *UserFilter) GetAllowedIncludes() map[string]bool {
return map[string]bool{
"Profile": true, // ✅ Allow loading user profile
"Posts": true, // ✅ Allow loading user posts
"Orders": true, // ✅ Allow loading user orders
// "Secrets": false, // ❌ Sensitive data - not allowed
}
}
func (f *UserFilter) ApplyFilters(query *gorm.DB) *gorm.DB {
if f.Name != "" {
query = query.Where("name LIKE ?", "%"+f.Name+"%")
}
if f.Status != "" {
query = query.Where("status = ?", f.Status)
}
return query
}
func (f *UserFilter) GetSearchFields() []string {
return []string{"name", "email"}
}
func (f *UserFilter) GetTableName() string {
return "users"
}
func (f *UserFilter) GetDefaultSort() string {
return "id asc"
}
// Handler with automatic include validation
func GetUsersWithRelations(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
filter := &UserFilter{}
filter.BindPagination(c)
c.ShouldBindQuery(filter)
// 🔒 Automatically validates includes and loads relationships
users, total, err := pagination.PaginatedQueryWithIncludable[User](db, filter)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
paginationResponse := pagination.CalculatePagination(filter.GetPagination(), total)
response := pagination.NewPaginatedResponse(200, "Users retrieved successfully", users, paginationResponse)
c.JSON(200, response)
}
}
Relationship loading examples:
# Basic pagination without relationships
curl "http://localhost:8080/users?page=1&per_page=10"
# Load user profiles
curl "http://localhost:8080/users?includes=Profile&page=1&per_page=10"
# Load multiple relationships
curl "http://localhost:8080/users?includes=Profile,Posts&page=1&per_page=10"
# Load all allowed relationships
curl "http://localhost:8080/users?includes=Profile,Posts,Orders&page=1&per_page=10"
# Combine with search and filters
curl "http://localhost:8080/users?includes=Profile,Posts&search=john&status=active&page=1&per_page=10"
# Try loading unauthorized relationship (will be ignored)
curl "http://localhost:8080/users?includes=Profile,Secrets&page=1&per_page=10"
# Only Profile will be loaded, Secrets will be ignored for security
type UserAdvancedFilter struct {
pagination.BaseFilter
Name string `json:"name" form:"name"`
CityName string `json:"city_name" form:"city_name"`
PostTitle string `json:"post_title" form:"post_title"`
}
func (f *UserAdvancedFilter) ApplyFilters(query *gorm.DB) *gorm.DB {
if f.Name != "" {
query = query.Where("users.name LIKE ?", "%"+f.Name+"%")
}
if f.CityName != "" {
query = query.Joins("JOIN profiles ON profiles.user_id = users.id").
Joins("JOIN addresses ON addresses.profile_id = profiles.id").
Where("addresses.city LIKE ?", "%"+f.CityName+"%")
}
if f.PostTitle != "" {
query = query.Joins("JOIN posts ON posts.user_id = users.id").
Where("posts.title LIKE ?", "%"+f.PostTitle+"%")
}
return query
}
func (f *UserAdvancedFilter) GetSearchFields() []string {
return []string{"users.name", "users.email", "profiles.bio"}
}
func (f *UserAdvancedFilter) GetAllowedIncludes() map[string]bool {
return map[string]bool{
"Profile": true, // Load user profile
"Posts": true, // Load user posts
"Profile.Address": true, // Load nested: profile with address
"Posts.Comments": true, // Load nested: posts with comments
"Posts.Tags": true, // Load nested: posts with tags
}
}
Nested relationship examples:
# Load nested relationships
curl "http://localhost:8080/users/advanced?includes=Profile.Address,Posts.Comments"
# Complex filtering with nested loading
curl "http://localhost:8080/users/advanced?includes=Profile.Address&city_name=Jakarta&search=developer"
# Multiple nested relationships
curl "http://localhost:8080/users/advanced?includes=Profile.Address,Posts.Comments,Posts.Tags&page=1&per_page=5"
The library provides powerful automatic search functionality across multiple fields:
type ProductFilter struct {
pagination.BaseFilter
CategoryID int `json:"category_id" form:"category_id"`
MinPrice float64 `json:"min_price" form:"min_price"`
MaxPrice float64 `json:"max_price" form:"max_price"`
InStock *bool `json:"in_stock" form:"in_stock"`
}
// Define which fields should be searchable
func (f *ProductFilter) GetSearchFields() []string {
return []string{"name", "description", "brand", "sku"}
}
func (f *ProductFilter) ApplyFilters(query *gorm.DB) *gorm.DB {
if f.CategoryID > 0 {
query = query.Where("category_id = ?", f.CategoryID)
}
if f.MinPrice > 0 {
query = query.Where("price >= ?", f.MinPrice)
}
if f.MaxPrice > 0 {
query = query.Where("price <= ?", f.MaxPrice)
}
if f.InStock != nil {
query = query.Where("in_stock = ?", *f.InStock)
}
return query
}
func (f *ProductFilter) GetTableName() string {
return "products"
}
func (f *ProductFilter) GetDefaultSort() string {
return "created_at desc"
}
Search examples:
# Search across name, description, brand, and sku fields
curl "http://localhost:8080/products?search=laptop"
# Automatically generates: WHERE (name LIKE '%laptop%' OR description LIKE '%laptop%' OR brand LIKE '%laptop%' OR sku LIKE '%laptop%')
# Combine search with filters
curl "http://localhost:8080/products?search=gaming&category_id=1&min_price=500"
# Search with pagination and sorting
curl "http://localhost:8080/products?search=macbook&sort=price,asc&page=1&per_page=10"
The library automatically optimizes search based on your database:
// For PostgreSQL - Uses ILIKE for case-insensitive search
// Automatically generated: WHERE (name ILIKE '%search%' OR description ILIKE '%search%')
// For MySQL/SQLite - Uses LIKE
// Automatically generated: WHERE (name LIKE '%search%' OR description LIKE '%search%')
type UserSearchFilter struct {
pagination.BaseFilter
Role string `json:"role" form:"role"`
Department string `json:"department" form:"department"`
}
func (f *UserSearchFilter) GetSearchFields() []string {
return []string{
"users.name",
"users.email",
"profiles.bio",
"departments.name",
}
}
func (f *UserSearchFilter) ApplyFilters(query *gorm.DB) *gorm.DB {
// Join tables for search functionality
query = query.Joins("LEFT JOIN profiles ON profiles.user_id = users.id").
Joins("LEFT JOIN departments ON departments.id = users.department_id")
if f.Role != "" {
query = query.Where("users.role = ?", f.Role)
}
if f.Department != "" {
query = query.Where("departments.name = ?", f.Department)
}
return query
}
Advanced search examples:
# Search across multiple tables
curl "http://localhost:8080/users/search?search=developer"
# Searches in: users.name, users.email, profiles.bio, departments.name
# Search with relationship filters
curl "http://localhost:8080/users/search?search=john&role=admin&department=IT"
# Sort by single field ascending (default)
curl "http://localhost:8080/users?sort=name"
# Sort by single field descending
curl "http://localhost:8080/users?sort=name,desc"
# Sort by multiple fields
curl "http://localhost:8080/users?sort=role,asc&sort=name,desc"
# Sort with pagination
curl "http://localhost:8080/users?sort=created_at,desc&page=1&per_page=20"
Custom sorting examples:
# Sort by calculated posts count
curl "http://localhost:8080/users?sort=posts_count,desc"
# Sort by concatenated full name
curl "http://localhost:8080/users?sort=full_name,asc"
# Sort by related table field
curl "http://localhost:8080/users?sort=latest_login,desc"
type SecureUserFilter struct {
pagination.BaseFilter
Status string `json:"status" form:"status"`
}
func (f *SecureUserFilter) GetAllowedIncludes() map[string]bool {
return map[string]bool{
"Profile": true, // ✅ Safe to load
"Posts": true, // ✅ Safe to load
"PublicData": true, // ✅ Safe to load
"SensitiveData": false, // ❌ Blocked for security
"PrivateNotes": false, // ❌ Blocked for security
"AdminData": false, // ❌ Blocked for security
}
}
func (f *SecureUserFilter) Validate() {
// Automatic validation removes unauthorized includes
var validIncludes []string
allowedIncludes := f.GetAllowedIncludes()
for _, include := range f.Includes {
// Validate against whitelist
if allowedIncludes[include] {
// Additional regex validation for SQL injection protection
if isValidInclude(include) {
validIncludes = append(validIncludes, include)
}
}
}
f.Includes = validIncludes
}
// Built-in regex validation for includes
func isValidInclude(include string) bool {
// Only allows alphanumeric, dots, and underscores
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_.]+$`, include)
return matched
}
Security examples:
# Valid includes - will be processed
curl "http://localhost:8080/users?includes=Profile,Posts"
# Invalid includes - will be ignored
curl "http://localhost:8080/users?includes=Profile,SensitiveData,AdminData"
# Only Profile will be loaded
# SQL injection attempt - will be blocked
curl "http://localhost:8080/users?includes=Profile'; DROP TABLE users; --"
# Regex validation will reject this
type ValidatedFilter struct {
pagination.BaseFilter
Email string `json:"email" form:"email" validate:"email"`
Age int `json:"age" form:"age" validate:"min=0,max=120"`
Status string `json:"status" form:"status" validate:"oneof=active inactive pending"`
}
func (f *ValidatedFilter) Validate() error {
// Built-in validation
if f.Email != "" && !isValidEmail(f.Email) {
return errors.New("invalid email format")
}
if f.Age < 0 || f.Age > 120 {
return errors.New("age must be between 0 and 120")
}
validStatuses := map[string]bool{
"active": true, "inactive": true, "pending": true,
}
if f.Status != "" && !validStatuses[f.Status] {
return errors.New("invalid status value")
}
return nil
}
func (f *ValidatedFilter) ApplyFilters(query *gorm.DB) *gorm.DB {
// All inputs are already validated
if f.Email != "" {
query = query.Where("email = ?", f.Email) // Safe to use
}
if f.Age > 0 {
query = query.Where("age = ?", f.Age)
}
if f.Status != "" {
query = query.Where("status = ?", f.Status)
}
return query
}
Parameter | Type | Description | Example | Default |
---|---|---|---|---|
page |
int | Page number | page=2 |
1 |
per_page |
int | Alias for per_page | per_page=25 |
10 |
search |
string | Global search term | search=john |
"" |
sort |
string | Sort field | sort=name |
"" |
order |
string | Sort direction | order=desc |
"asc" |
includes |
string | Comma-separated relations | includes=profile,posts |
"" |
# Single field ascending (default)
?sort=name
# Single field with explicit direction
?sort=name&order=desc
# Alternative comma format
?sort=name,desc
# Multiple fields
?sort=name,asc&sort=created_at,desc
# Basic pagination
GET /api/users?page=1&per_page=10
# Search with pagination
GET /api/users?search=developer&page=2&per_page=5
# Filter with specific fields
GET /api/users?role=admin&status=active&page=1&per_page=20
# Sort with relationships
GET /api/users?includes=profile,posts&sort=name,asc&page=1&per_page=15
# Complex combined query
GET /api/users?search=john&role=user&status=active&includes=profile&sort=created_at,desc&page=1&per_page=10
# Date range filtering (custom implementation)
GET /api/users?created_after=2023-01-01&created_before=2023-12-31&page=1&per_page=10
# Numeric range filtering
GET /api/products?min_price=100&max_price=500&category_id=1&page=1&per_page=10
{
"code": 200,
"status": "success",
"message": "Data retrieved successfully",
"data": [
{
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"profile": {
"id": 1,
"bio": "Software Developer",
"avatar": "avatar.jpg"
},
"posts": [
{
"id": 1,
"title": "My First Post",
"content": "Hello World!"
}
]
}
],
"pagination": {
"page": 1,
"per_page": 10,
"max_page": 15,
"total": 142
}
}
The examples/
folder contains a complete working implementation:
# Navigate to examples directory
cd examples/
# Install dependencies
go mod tidy
# Set up your database (update connection string in main.go)
# Default expects MySQL at localhost:3306 with database 'sports_db'
# Run the example server
go run .
The example server provides these endpoints:
GET /provinces
- Basic province paginationGET /provinces/with-athletes
- Provinces with athlete relationshipsGET /athletes
- Athletes with province/sport filteringGET /sports
- Sports managementGET /events
- Events with date filtering
Test the examples:
# Basic athlete pagination
curl "http://localhost:8080/athletes?page=1&per_page=5"
# Athletes with relationships
curl "http://localhost:8080/athletes?includes=Province,Sport&page=1&per_page=5"
# Search athletes by name
curl "http://localhost:8080/athletes?search=john&includes=Province"
# Filter by province and sport
curl "http://localhost:8080/athletes?province_id=1&sport_id=2&includes=Province,Sport"
# Provinces with their athletes
curl "http://localhost:8080/provinces/with-athletes?includes=Athletes&page=1&per_page=10"
We welcome contributions! Here's how you can help:
# Fork and clone the repository
git clone https://github.com/yourusername/go-pagination.git
cd go-pagination
# Install dependencies
go mod tidy
# Run tests
go test ./...
# Run benchmarks
go test -bench=. ./...
# Run examples
cd examples/
go run .
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature
- Make your changes:
- Add tests for new functionality
- Update documentation
- Follow Go conventions
- Run tests:
go test ./...
- Commit your changes:
git commit -m 'feat: add amazing feature'
- Push to branch:
git push origin feature/amazing-feature
- Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- GORM - The fantastic Go ORM that makes database operations elegant
- Gin - The high-performance Go web framework
- Go Community - For continuous inspiration and feedback
- Contributors - Everyone who has contributed to making this library better
- 📖 Documentation: Check this README and examples
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
- ⭐ Star the repo if you find it useful!