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/offsetarrive 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 uncappedlimit=1000000lets a client DoS your DB and serialiser. - Slice bounds must be safe.
offsetpast the end yields an empty page, never a panic: clamplo := 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 100000is 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=activeto 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 whenoffset+limit > len. Switched tomin-clamped bounds.- Left
limituncapped; a test withlimit=100000allocated a huge response.
❓ Open Questions¶
- How do I expose cursor pagination in the JSON response —
next_cursorfield,Linkheader, or both? (REST APIs commonly do both.)
🧠 Active Recall (answer without looking)¶
-
Q: Why prefer clamping
limit/offsetover returning400for 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. -
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)