go 45 lines · 7 steps

Safe query filtering in a Gin handler

A Gin handler parses filter[...] query params against a whitelist and turns them into safe, parameterized database conditions.

Explained by highlit
1func ListProducts(c *gin.Context) {
2 allowed := map[string]string{
3 "category": "category",
4 "brand": "brand",
5 "status": "status",
6 }
7 
8 filters := make(map[string]interface{})
9 for key, values := range c.Request.URL.Query() {
10 if !strings.HasPrefix(key, "filter[") || !strings.HasSuffix(key, "]") || len(values) == 0 {
11 continue
12 }
13 
14 field := key[len("filter[") : len(key)-1]
15 column, ok := allowed[field]
16 if !ok {
17 c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("unknown filter %q", field)})
18 return
19 }
20 
21 if len(values) > 1 {
22 filters[column] = values
23 } else {
24 filters[column] = values[0]
25 }
26 }
27 
28 query := db.Model(&Product{})
29 for column, value := range filters {
30 switch v := value.(type) {
31 case []string:
32 query = query.Where(column+" IN ?", v)
33 default:
34 query = query.Where(column+" = ?", v)
35 }
36 }
37 
38 var products []Product
39 if err := query.Find(&products).Error; err != nil {
40 c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load products"})
41 return
42 }
43 
44 c.JSON(http.StatusOK, gin.H{"data": products, "filters": filters})
45}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Whitelisting request fields to real column names is what keeps user input from reaching SQL unchecked.
  2. 2Parameterized Where clauses with ? placeholders defend against injection even when the column name comes from a fixed map.
  3. 3A type switch lets one filter map carry both single values and slices, mapping cleanly to = versus IN.

Related explainers

Share this explainer

Here's the card — post it anywhere.

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