DEV Community

Cover image for How to Paginate API Responses in Go
Siddhesh Khandagale
Siddhesh Khandagale

Posted on

How to Paginate API Responses in Go

What Is Pagination, and Why Do We Need It?

Imagine your application has a database with thousands of records. Sending all these records to users in a single API response:

  • Slows down your application.
  • Consumes excessive bandwidth.
  • Overwhelms users with too much data at once.

Pagination solves this problem by splitting the data into smaller pages. Users get only subset of the data at a time, making APIs faster and applications smoother.

PaginationConsider a giant bookshelf with hundreds of books. Instead of searching through the entire shelf, wouldn’t it be easier if the shelf were divided into sections like "Page 1", "Page 2", and so on? That’s exactly what pagination does!

Setting Up the Database

To demonstrate pagination, we’ll use a simple items table in a PostgreSQL database. Here's the schema:

CREATE TABLE items (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);
Enter fullscreen mode Exit fullscreen mode

Now insert some dummy data:

INSERT INTO items (name) 
VALUES 
('Item 1'), ('Item 2'), ('Item 3'), ..., ('Item 100');
Enter fullscreen mode Exit fullscreen mode

Setting Up a Go API with Pagination

Let’s create an API endpoint /items that accepts two query parameters:

page: The page number (default: 1).
limit: The number of records per page (default: 10).

Here’s the full implementation:

package main

import (
    "database/sql"
    "fmt"
    "log"
    "net/http"
    "strconv"

    _ "github.com/lib/pq"
)

func main() {
    // Connect to the database
    db, err := sql.Open("postgres", "user=youruser password=yourpass dbname=yourdb sslmode=disable")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    http.HandleFunc("/items", func(w http.ResponseWriter, r *http.Request) {
        // Extract 'page' and 'limit' query parameters
        page, err := strconv.Atoi(r.URL.Query().Get("page"))
        if err != nil || page < 1 {
            page = 1 // Default to page 1
        }
        limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
        if err != nil || limit < 1 {
            limit = 10 // Default to 10 items per page
        }

        // Calculate the OFFSET
        offset := (page - 1) * limit

        // Query the database
        rows, err := db.Query("SELECT id, name, created_at FROM items LIMIT $1 OFFSET $2", limit, offset)
        if err != nil {
            http.Error(w, "Failed to fetch items", http.StatusInternalServerError)
            return
        }
        defer rows.Close()

        // Process the rows
        items := []map[string]interface{}{}
        for rows.Next() {
            var id int
            var name string
            var createdAt string
            if err := rows.Scan(&id, &name, &createdAt); err != nil {
                http.Error(w, "Failed to scan items", http.StatusInternalServerError)
                return
            }
            items = append(items, map[string]interface{}{
                "id":         id,
                "name":       name,
                "created_at": createdAt,
            })
        }

        // Respond with JSON
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintf(w, `{"page":%d,"limit":%d,"items":%v}`, page, limit, items)
    })

    log.Println("Server is running on http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
Enter fullscreen mode Exit fullscreen mode

Understanding the Logic :

Pagination Parameters

  • Page: Determines which set of records to fetch.
  • Limit: Specifies the number of records per page.

Offset Calculation
The offset determines how many records to skip:
offset = (page - 1) * limit

For example:

  • Page 1 with limit=5 → offset = 0 (skip 0 records).
  • Page 2 with limit=5 → offset = 5 (skip the first 5 records).

SQL Query

We use LIMIT and OFFSET in SQL to fetch the desired records:
SELECT id, name, created_at FROM items ORDER BY id LIMIT 5 OFFSET 5;

Testing Your API

Test your API using tools like Postman, cURL, or directly in a browser:

  • Fetch the first page with 10 items:
curl "http://localhost:8080/items?page=1&limit=10"
Enter fullscreen mode Exit fullscreen mode
  • Fetch the second page with 20 items:
curl "http://localhost:8080/items?page=2&limit=20"
Enter fullscreen mode Exit fullscreen mode

API Response

Here’s an example response for /items?page=2&limit=2:

{
    "page": 2,
    "limit": 2,
    "items": [map[created_at: 2025-01-10T20: 38: 57.832777Z id: 3 name:Item 3
        ] map[created_at: 2025-01-10T20: 38: 57.832777Z id: 4 name:Item 4
        ]
    ]
}
Enter fullscreen mode Exit fullscreen mode

Common Doubts and Pitfalls

1. Why not fetch all records and slice them in Go?
Because it’s inefficient. Imagine loading a million records into memory—your API will slow down and possibly crash.

2. What happens if the page or limit parameters are missing?
Always set defaults (e.g., page=1, limit=10) to ensure your API doesn’t break.

3. Can we optimize this further?
Yes! Use indexes on frequently queried columns (like id or created_at) for faster lookups.

Conclusion

With just a few lines of code and smart database querying, you’ve turned an overwhelming API response into something lightweight and user-friendly.

Want to take it up a notch? Try adding total pages, next/previous links, or even cursor-based pagination for large-scale applications.

To get more information about Golang concepts, projects, etc. and to stay updated on the Tutorials do follow Siddhesh on Twitter and GitHub.

Until then Keep Learning, Keep Building 🚀🚀

Top comments (2)

Collapse
 
mkumaran profile image
Kumaran

nice one, as a next step please include sorting and filtering.

Collapse
 
siddheshk02 profile image
Siddhesh Khandagale

Thank you! Sorting and filtering are great suggestions. I'll consider covering them in a follow-up post, stay tuned!