go 57 lines · 8 steps

Cursor pagination in a Gin handler

A Gin endpoint that pages through posts using opaque, base64-encoded cursors instead of numeric offsets.

Explained by highlit
1package handlers
2 
3import (
4 "encoding/base64"
5 "net/http"
6 "strconv"
7 
8 "github.com/gin-gonic/gin"
9)
10 
11type PostList struct {
12 Data []Post `json:"data"`
13 NextPage *string `json:"next_page,omitempty"`
14}
15 
16func encodeCursor(id int64) string {
17 return base64.URLEncoding.EncodeToString([]byte(strconv.FormatInt(id, 10)))
18}
19 
20func decodeCursor(token string) (int64, error) {
21 raw, err := base64.URLEncoding.DecodeString(token)
22 if err != nil {
23 return 0, err
24 }
25 return strconv.ParseInt(string(raw), 10, 64)
26}
27 
28func (h *Handler) ListPosts(c *gin.Context) {
29 limit, err := strconv.Atoi(c.DefaultQuery("limit", "20"))
30 if err != nil || limit < 1 || limit > 100 {
31 limit = 20
32 }
33 
34 var afterID int64
35 if cursor := c.Query("cursor"); cursor != "" {
36 afterID, err = decodeCursor(cursor)
37 if err != nil {
38 c.JSON(http.StatusBadRequest, gin.H{"error": "invalid cursor"})
39 return
40 }
41 }
42 
43 posts, err := h.repo.ListAfter(c.Request.Context(), afterID, limit+1)
44 if err != nil {
45 c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load posts"})
46 return
47 }
48 
49 resp := PostList{Data: posts}
50 if len(posts) > limit {
51 resp.Data = posts[:limit]
52 next := encodeCursor(resp.Data[len(resp.Data)-1].ID)
53 resp.NextPage = &next
54 }
55 
56 c.JSON(http.StatusOK, resp)
57}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Cursor pagination encodes the last item's key into an opaque token, avoiding the drift and cost of offset-based paging.
  2. 2Fetching limit+1 rows is a cheap trick to detect whether another page exists without a second query.
  3. 3A pointer field with omitempty lets you omit next_page entirely on the last page rather than sending null.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Cursor pagination in a Gin handler — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code