Skip to content

Day 101 — Pagination & Filtering

Month 4 · Week 3 · ⬅ Day 100 · Day 102 ➡ · Journal index

🎯 Learning Objective

Parse and clamp limit/offset from the query string safely, slice a page without panics, and understand offset vs cursor pagination.

📚 Topics

  • r.URL.Query(), defaults & clamping; bounds-safe slicing
  • Offset vs cursor (keyset) pagination; filtering via allow-lists

📖 Reading / Sources

📝 Notes

  • Query params are hostile input. limit/offset arrive as strings; coerce, don't trust. Missing/garbage → default; out of range → clamp, don't 400. A list endpoint should degrade gracefully. → [[defensive-parsing]]
  • Always cap limit (e.g. MaxLimit = 100). An uncapped limit=1000000 lets a client DoS your DB and serialiser.
  • Slice bounds must be safe. offset past the end yields an empty page, never a panic: clamp lo := min(offset, total), hi := min(lo+limit, total).
  • Offset pagination is simple but drifts. Inserts/deletes between pages can skip or repeat rows, and deep OFFSET 100000 is slow (the DB still scans them).
  • Cursor / keyset pagination uses WHERE id > $last ORDER BY id LIMIT n — O(log n) via the index and stable under writes; the cursor is the last row's key, base64-encoded. The trade-off: no random "jump to page 50". → [[keyset-pagination]]
  • Filtering needs an allow-list, not raw column names from the client — map ?status=active to a known field to avoid injection and leaking schema.

💻 Code Examples

const (
    DefaultLimit = 20
    MaxLimit     = 100
)

func parse(q url.Values) (limit, offset int) {
    limit, offset = DefaultLimit, 0
    if n, err := strconv.Atoi(q.Get("limit")); err == nil {
        limit = n
    }
    if limit < 1 {
        limit = DefaultLimit
    } else if limit > MaxLimit {
        limit = MaxLimit // cap it — never trust the client's number
    }
    if n, err := strconv.Atoi(q.Get("offset")); err == nil && n > 0 {
        offset = n
    }
    return limit, offset
}

// bounds-safe page slice (no panic even when offset > len(items))
func page(items []T, limit, offset int) []T {
    lo := min(offset, len(items))
    hi := min(lo+limit, len(items))
    return items[lo:hi]
}

Full logic + tests: exercises/month-04/week-3/paginate · Run: go test ./exercises/month-04/week-3/paginate

🏋️ Exercises / Practice

Exercise Status Link
Parse + clamp limit/offset with defaults exercises/month-04/week-3/paginate
Bounds-safe page slicing exercises/month-04/week-3/paginate

🐛 Mistakes Made

  • items[offset : offset+limit] panicked when offset+limit > len. Switched to min-clamped bounds.
  • Left limit uncapped; a test with limit=100000 allocated a huge response.

❓ Open Questions

  • How do I expose cursor pagination in the JSON response — next_cursor field, Link header, or both? (REST APIs commonly do both.)

🧠 Active Recall (answer without looking)

  1. Q: Why prefer clamping limit/offset over returning 400 for bad values?

    A List params are best-effort UI state; coercing to safe defaults keeps the endpoint usable and resilient, while a hard cap on `limit` still protects the server from abuse.

  2. Q: One concrete advantage of keyset (cursor) over offset pagination?

    A It stays O(log n) via the index (no scanning skipped rows) and is stable under concurrent inserts/deletes, so pages don't skip or repeat rows.

🪶 Feynman Reflection

Paging is "give me rows N..N+k". Offset counts from the start every time — simple, but slow and wobbly once data changes underneath you. A cursor instead says "give me rows after this key", which the index can jump to directly and which never double-counts. Either way I clamp the page size so no single request can drown the server.

🕳️ Knowledge Gaps

  • Encoding/decoding opaque cursors and signing them so clients can't forge.

✅ Summary

I can parse and clamp pagination params safely, slice a page without panicking, and explain when to choose cursor over offset pagination.

⏭️ Next Steps / Prep for Tomorrow

  • Day 102: configuration from the environment.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: feat(exercises): clamped pagination parsing and safe slicing (day 101)