In the last post, we tackled Pagination breaking down large API responses into manageable chunks. But if you've ever wanted to let users control how data is sorted or filter specific results, then you're ready for the next step: sorting and filtering.
Let's dive in and make our APIs even more powerful by adding these features.
If you haven't gone through the previous tutorial, here's the link.
Why Sorting and Filtering Matter
Pagination alone isn't always enough. Imagine a user searching for the newest items or only those created in a specific timeframe. Sorting and filtering let users:
- Sort: Choose the order of results (e.g. newest to oldest)
- Filter: Narrow down results to what they need (e.g. items created today)
By combining pagination with sorting and filtering, we create a more user-friendly API.
Extending Our Database Query
We'll build on the items table from the previous blog. To recap, here's the scheme:
CREATE TABLE items (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
Adding Sorting
Sorting is all about ordering the results. We'll allow users to sort by the name
or created_at
columns in ascending or descending order.
Step 1: Accept Sort Parameters
Users will pass two query parameters:
-
sort
: The column to sort by (name
orcreated_at
). -
order
: The sort direction (asc
ordesc
).
Example URL:
/items?page=1&limit=10&sort=created_at&order=desc
Step 2: Adjust the SQL Query
We'll dynamically modify the SQL query to include sorting:
// Extract sort and order query parameters
sort := r.URL.Query().Get("sort")
order := r.URL.Query().Get("order")
// Validate the sort column
validSortColumns := map[string]bool{"name": true, "created_at": true}
if !validSortColumns[sort] {
sort = "created_at" // Default sort column
}
// Validate the sort order
if order != "asc" && order != "desc" {
order = "asc" // Default sort order
}
// Modify the SQL query
query := fmt.Sprintf("SELECT id, name, created_at FROM items ORDER BY %s %s LIMIT $1 OFFSET $2", sort, order)
rows, err := db.Query(query, limit, offset)
if err != nil {
http.Error(w, "Failed to fetch items", http.StatusInternalServerError)
return
}
Adding Filtering
Filtering lets users refine their search. For example, we can filter items by a date range or items containing a specific keyword in their name.
Step 1: Accept Filter Parameters
We'll support two filters:
-
name
: Search for items containing a specific substring. -
created_after
: Fetch items created after a specific date.
Example URL :
/items?page=1&limit=10&name=Item&created_after=2025-01-10 20:38:57.832777
Step 2: Adjust the SQL Query
We'll add WHERE conditions to handle these filters:
// Extract filter query parameters
nameFilter := r.URL.Query().Get("name")
createdAfter := r.URL.Query().Get("created_after")
// Build the WHERE clause dynamically
whereClauses := []string{}
args := []interface{}{}
argIndex := 1
if nameFilter != "" {
whereClauses = append(whereClauses, "name ILIKE $1")
args = append(args, "%"+nameFilter+"%")
argIndex++
}
if createdAfter != "" {
whereClauses = append(whereClauses, fmt.Sprintf("created_at > $%d", argIndex))
args = append(args, createdAfter)
argIndex++
}
args = append(args, limit, offset)
// Combine WHERE clauses
whereSQL := ""
if len(whereClauses) > 0 {
whereSQL = "WHERE " + strings.Join(whereClauses, " AND ")
}
// Final query
query := fmt.Sprintf("SELECT id, name, created_at FROM items %s ORDER BY %s %s LIMIT $%d OFFSET $%d", whereSQL, sort, order, argIndex, argIndex+1)
rows, err := db.Query(query, args...)
if err != nil {
http.Error(w, "Failed to fetch items", http.StatusInternalServerError)
return
}
Testing the Enhanced API
- Sorting by name in ascending order:
curl "http://localhost:8080/items?page=1&limit=5&sort=name&order=asc"
- Filtering items created after a specific date:
curl "http://localhost:8080/items?page=1&limit=5&created_after=2025-01-10 20:38:57.832777"
- Combining filters and sorting:
curl "http://localhost:8080/items?page=1&limit=5&name=Item&sort=created_at&order=desc"
Common Mistake to Avoid
Not validating user input: Allowing arbitrary columns or invalid sort orders can expose your database to SQL injection. Always validate inputs.
Conclusion / Next Steps
You can find the complete code repository for this tutorial here
With pagination, sorting, and filtering in place, your API is now more user-friendly and flexible. For even more advanced functionality, consider adding:
- Cursor-based pagination for large datasets.
- Faceted filtering for complex searches.
Stay tuned for the next post where we’ll explore these advanced techniques!
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 (0)