Skip to content

Repository files navigation

cursor

Keyset (cursor) pagination for Go — the stable, fast alternative to OFFSET/LIMIT.

OFFSET gets slow on deep pages (the database still scans everything it skips) and shifts results when rows are inserted mid-scroll. Keyset pagination seeks straight to the last item you saw, so page 1 and page 10,000 cost the same and the order stays stable. This package handles the two annoying parts: encoding an opaque cursor token and slicing a fetched batch into a page plus the next cursor.

type key struct {
    CreatedAt time.Time `json:"c"`
    ID        string    `json:"id"`
}

// Fetch limit+1 rows ordered by the SAME key you paginate on.
rows := fetchUsers(ctx, afterCursor, limit+1) // []User

page, err := cursor.Slice(rows, limit, func(u User) (any, error) {
    return key{u.CreatedAt, u.ID}, nil
})
// page.Items   -> up to `limit` users
// page.Next    -> opaque token for ?cursor=... (empty on the last page)
// page.HasMore -> whether another page exists

Decode an incoming cursor to build the next query's WHERE:

var k key
if c := r.URL.Query().Get("cursor"); c != "" {
    if err := cursor.Decode(c, &k); err != nil {
        http.Error(w, "bad cursor", http.StatusBadRequest)
        return
    }
    // WHERE (created_at, id) > ($1, $2) ORDER BY created_at, id
}

Why limit+1

Fetch one more row than you intend to return. If it comes back, there's another page (HasMore=true) and the sentinel row is trimmed; the Next cursor is built from the last returned item, not the sentinel.

API

  • Encode(v any) (string, error) — opaque, URL-safe token from any JSON-serializable key.
  • Decode(token string, v any) error — parse a token; returns ErrInvalidCursor on garbage.
  • Slice[T](batch []T, limit int, keyOf func(T) (any, error)) (Page[T], error) — batch → page.

Cursors are base64url-encoded JSON. They are opaque, not encrypted — don't put secrets in the sort key.

Install

go get github.com/hpower2/cursor

License

MIT

About

Keyset (cursor) pagination for Go — the stable, fast alternative to OFFSET/LIMIT.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages