spindle

package module
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 3, 2026 License: MIT Imports: 6 Imported by: 0

README

Spindle

Go Reference CI codecov

Pagination middleware for Fiber v3.

Spindle extracts page, limit, offset, and sort parameters from query strings and makes them available to your handlers via context.

Install

go get github.com/mutantkeyboard/spindle

Requires Go 1.25+ and Fiber v3.

Usage

Basic
package main

import (
    "github.com/gofiber/fiber/v3"
    "github.com/mutantkeyboard/spindle"
)

func main() {
    app := fiber.New()

    app.Use(spindle.New())

    app.Get("/users", func(c fiber.Ctx) error {
        pageInfo, ok := spindle.FromContext(c)
        if !ok {
            return fiber.ErrBadRequest
        }

        // pageInfo.Page   - current page (default: 1)
        // pageInfo.Limit  - items per page (default: 10, max: 100)
        // pageInfo.Offset - direct offset (default: 0)
        // pageInfo.Start() - calculated start index
        // pageInfo.Sort   - sort fields

        return c.JSON(pageInfo)
    })

    app.Listen(":3000")
}

Request: GET /users?page=2&limit=20

With Sorting
app.Use(spindle.New(spindle.Config{
    SortKey:      "sort",
    DefaultSort:  "created_at",
    AllowedSorts: []string{"created_at", "name", "email"},
}))

Request: GET /users?page=1&limit=10&sort=name,-created_at

Sort fields are comma-separated. Prefix with - for descending order.

Cursor Pagination

For infinite scroll and keyset pagination:

app.Use(spindle.New(spindle.Config{
    CursorKey: "cursor", // default
}))

app.Get("/users", func(c fiber.Ctx) error {
    pageInfo, ok := spindle.FromContext(c)
    if !ok {
        return fiber.ErrBadRequest
    }

    query := db.Model(&User{}).OrderBy("id ASC").Limit(pageInfo.Limit + 1)

    if vals := pageInfo.CursorValues(); vals != nil {
        query = query.Where("id > ?", vals["id"])
    }

    var users []User
    query.Find(&users)

    hasMore := len(users) > pageInfo.Limit
    if hasMore {
        users = users[:pageInfo.Limit]
        last := users[len(users)-1]
        pageInfo.SetNextCursor(map[string]any{"id": last.ID})
    }

    return c.JSON(fiber.Map{
        "data":        users,
        "has_more":    pageInfo.HasMore,
        "next_cursor": pageInfo.NextCursor,
    })
})

First request: GET /users?limit=20 Next request: GET /users?cursor=<next_cursor>&limit=20

Cursor tokens are opaque base64-encoded values. Invalid cursors return 400.

Custom Config
app.Use(spindle.New(spindle.Config{
    PageKey:      "p",
    LimitKey:     "size",
    DefaultPage:  1,
    DefaultLimit: 25,
    DefaultSort:  "id",
    AllowedSorts: []string{"id", "name", "date"},
    Next: func(c fiber.Ctx) bool {
        return c.Path() == "/health"
    },
}))

Config

Property Type Description Default
Next func(c fiber.Ctx) bool Skip middleware when returns true nil
PageKey string Query key for page number "page"
DefaultPage int Default page number 1
LimitKey string Query key for limit "limit"
DefaultLimit int Default items per page 10
SortKey string Query key for sort ""
DefaultSort string Default sort field "id"
AllowedSorts []string Allowed sort field names []
CursorKey string Query key for cursor token "cursor"
CursorParam string Optional alias for cursor key ""

PageInfo

Retrieved via spindle.FromContext(c):

type PageInfo struct {
    Page       int         // Current page number
    Limit      int         // Items per page (capped at 100)
    Offset     int         // Direct offset
    Sort       []SortField // Sort fields with direction
    Cursor     string      // Cursor token (empty if not in cursor mode)
    HasMore    bool        // True if more results exist (set by handler)
    NextCursor string      // Opaque cursor for next page (set by handler)
}
Methods
  • Start() int - Returns the start index. Uses Offset if set, otherwise (Page-1) * Limit.
  • SortBy(field string, order SortOrder) *PageInfo - Adds a sort field. Chainable.
  • NextPageURL(baseURL string) string - Returns the URL for the next page.
  • PreviousPageURL(baseURL string) string - Returns the URL for the previous page. Empty string if on page 1.
  • CursorValues() map[string]any - Decodes the cursor into key-value pairs. Returns nil if empty or invalid.
  • SetNextCursor(values map[string]any) *PageInfo - Encodes values into an opaque cursor and sets HasMore. Chainable.
  • NextCursorURL(baseURL string) string - Returns the URL for the next cursor page. Empty string if HasMore is false.

Safety

  • Limit is capped at MaxLimit (100) to prevent excessive memory usage
  • Page values below 1 are reset to 1
  • Negative offsets are reset to 0
  • Sort fields are validated against AllowedSorts
  • Invalid cursor tokens return 400 Bad Request

Development

Run tests locally
go test -race -v ./...
Run tests in Docker
docker build -f Dockerfile.test -t spindle-test .
docker run --rm spindle-test
Dev container

Open this project in VS Code with the Dev Containers extension to get a pre-configured Go development environment.

Acknowledgements

Heavily inspired by fiberpaginate by Garrett Ladley.

License

MIT

Documentation

Index

Constants

View Source
const MaxLimit = 100

MaxLimit is the maximum limit allowed.

Variables

View Source
var ConfigDefault = Config{
	Next:         nil,
	PageKey:      "page",
	DefaultPage:  1,
	LimitKey:     "limit",
	DefaultLimit: 10,
	CursorKey:    "cursor",
}

ConfigDefault is the default config.

Functions

func New

func New(config ...Config) fiber.Handler

New creates a new pagination middleware handler.

Types

type Config

type Config struct {
	// Next defines a function to skip this middleware when returned true.
	Next func(c fiber.Ctx) bool

	// PageKey is the query string key for page number.
	PageKey string

	// DefaultPage is the default page number.
	DefaultPage int

	// LimitKey is the query string key for limit.
	LimitKey string

	// DefaultLimit is the default items per page.
	DefaultLimit int

	// SortKey is the query string key for sort.
	SortKey string

	// DefaultSort is the default sort field.
	DefaultSort string

	// AllowedSorts is the list of allowed sort fields.
	AllowedSorts []string

	// CursorKey is the query string key for cursor-based pagination.
	CursorKey string

	// CursorParam is an optional alias for the cursor query key.
	CursorParam string
}

Config defines the config for the pagination middleware.

type PageInfo

type PageInfo struct {
	Page       int         `json:"page"`
	Limit      int         `json:"limit"`
	Offset     int         `json:"offset"`
	Sort       []SortField `json:"sort"`
	Cursor     string      `json:"cursor,omitempty"`
	HasMore    bool        `json:"has_more,omitempty"`
	NextCursor string      `json:"next_cursor,omitempty"`
}

PageInfo contains pagination information.

func FromContext

func FromContext(c fiber.Ctx) (*PageInfo, bool)

FromContext returns the PageInfo from the context.

func NewPageInfo

func NewPageInfo(page, limit, offset int, sort []SortField) *PageInfo

NewPageInfo creates a new PageInfo.

func (*PageInfo) CursorValues added in v1.1.0

func (p *PageInfo) CursorValues() map[string]any

CursorValues decodes the opaque cursor into a key-value map. Returns nil if cursor is empty or invalid.

func (*PageInfo) NextCursorURL added in v1.1.0

func (p *PageInfo) NextCursorURL(baseURL string) string

NextCursorURL returns the URL for the next cursor page. Returns empty string if HasMore is false.

func (*PageInfo) NextPageURL

func (p *PageInfo) NextPageURL(baseURL string) string

NextPageURL returns the URL for the next page.

func (*PageInfo) PreviousPageURL

func (p *PageInfo) PreviousPageURL(baseURL string) string

PreviousPageURL returns the URL for the previous page. Returns empty string if on page 1.

func (*PageInfo) SetNextCursor added in v1.1.0

func (p *PageInfo) SetNextCursor(values map[string]any) *PageInfo

SetNextCursor encodes a key-value map into an opaque cursor token and sets both NextCursor and HasMore on the PageInfo. Chainable.

func (*PageInfo) SortBy

func (p *PageInfo) SortBy(field string, order SortOrder) *PageInfo

SortBy adds a sort field. Chainable.

func (*PageInfo) Start

func (p *PageInfo) Start() int

Start returns the start index based on page/limit or offset.

type SortField

type SortField struct {
	Field string
	Order SortOrder
}

SortField represents a sort field with direction.

type SortOrder

type SortOrder string

SortOrder represents sort order.

const (
	ASC  SortOrder = "asc"
	DESC SortOrder = "desc"
)

func SortOrderFromString

func SortOrderFromString(s string) SortOrder

SortOrderFromString returns a SortOrder from a string.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL