Episode 5 of 8

Params and Queries

Master URL parameters and query strings in Go Fiber. Learn how to extract, validate, and use route params and query strings to build filterable, sortable, and paginated APIs with type-safe parsing and default values.

Every API deals with two types of dynamic URL data: route parameters (/users/:id — identifying a specific resource) and query strings (/users?page=2&sort=name — filtering, sorting, and paginating collections). Fiber provides clean, type-safe methods for both. This tutorial covers extracting, validating, and combining params with queries to build powerful, filterable API endpoints.

Route Parameters (Params)

Basic Parameter Extraction

// Single parameter
app.Get("/users/:id", func(c *fiber.Ctx) error {
    id := c.Params("id") // Always returns a string
    return c.JSON(fiber.Map{"user_id": id})
})
// GET /users/42 → {"user_id": "42"}
// GET /users/abc → {"user_id": "abc"}

// Multiple parameters
app.Get("/orgs/:orgId/teams/:teamId/members/:memberId", func(c *fiber.Ctx) error {
    return c.JSON(fiber.Map{
        "org_id":    c.Params("orgId"),
        "team_id":   c.Params("teamId"),
        "member_id": c.Params("memberId"),
    })
})
// GET /orgs/1/teams/5/members/42
// → {"org_id": "1", "team_id": "5", "member_id": "42"}

Type-Safe Parameter Parsing

// Parse as integer with ParamsInt
app.Get("/products/:id", func(c *fiber.Ctx) error {
    id, err := c.ParamsInt("id")
    if err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Product ID must be a number",
        })
    }

    product, err := findProduct(id)
    if err != nil {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "error": "Product not found",
        })
    }

    return c.JSON(product)
})

// ParamsInt with default value
app.Get("/page/:num", func(c *fiber.Ctx) error {
    pageNum, err := c.ParamsInt("num", 1) // Default to 1
    if err != nil {
        return c.Status(400).JSON(fiber.Map{
            "error": "Invalid page number",
        })
    }
    return c.JSON(fiber.Map{"page": pageNum})
})

Optional Parameters

// Add ? to make a parameter optional
app.Get("/articles/:category?", func(c *fiber.Ctx) error {
    category := c.Params("category")

    if category == "" {
        // No category — return all articles
        articles := getAllArticles()
        return c.JSON(articles)
    }

    // Return articles for the specific category
    articles := getArticlesByCategory(category)
    return c.JSON(articles)
})
// GET /articles → all articles
// GET /articles/tech → tech articles

Wildcard Parameters

// Capture everything after the prefix
app.Get("/files/*", func(c *fiber.Ctx) error {
    filepath := c.Params("*")
    return c.JSON(fiber.Map{"path": filepath})
})
// GET /files/docs/report.pdf → {"path": "docs/report.pdf"}
// GET /files/images/2024/photo.jpg → {"path": "images/2024/photo.jpg"}

Query Strings

Basic Query Extraction

// Read individual query parameters
app.Get("/search", func(c *fiber.Ctx) error {
    query := c.Query("q")       // Search term
    page := c.Query("page")     // Page number (string)
    sort := c.Query("sort")     // Sort field

    return c.JSON(fiber.Map{
        "query": query,
        "page":  page,
        "sort":  sort,
    })
})
// GET /search?q=golang&page=2&sort=date
// → {"query": "golang", "page": "2", "sort": "date"}

// Query with default values
app.Get("/products", func(c *fiber.Ctx) error {
    category := c.Query("category", "all")  // Default: "all"
    sort := c.Query("sort", "created_at")   // Default: "created_at"
    order := c.Query("order", "desc")        // Default: "desc"

    return c.JSON(fiber.Map{
        "category": category,
        "sort":     sort,
        "order":    order,
    })
})
// GET /products → uses all defaults
// GET /products?sort=price&order=asc → overrides sort and order

Type-Safe Query Parsing

app.Get("/products", func(c *fiber.Ctx) error {
    // Parse integers
    page := c.QueryInt("page", 1)        // Default 1
    perPage := c.QueryInt("per_page", 20) // Default 20
    
    // Clamp values
    if perPage > 100 {
        perPage = 100 // Max 100 per page
    }
    if page < 1 {
        page = 1
    }

    // Parse boolean
    inStockStr := c.Query("in_stock", "true")
    inStock := inStockStr == "true"

    // Parse float
    minPriceStr := c.Query("min_price", "0")
    minPrice, _ := strconv.ParseFloat(minPriceStr, 64)
    
    maxPriceStr := c.Query("max_price", "99999")
    maxPrice, _ := strconv.ParseFloat(maxPriceStr, 64)

    return c.JSON(fiber.Map{
        "page":      page,
        "per_page":  perPage,
        "in_stock":  inStock,
        "min_price": minPrice,
        "max_price": maxPrice,
    })
})

Query Parser — Struct Binding

For endpoints with many query parameters, use Fiber's QueryParser to bind directly to a struct:

type ProductFilter struct {
    Page     int     `query:"page"`
    PerPage  int     `query:"per_page"`
    Category string  `query:"category"`
    MinPrice float64 `query:"min_price"`
    MaxPrice float64 `query:"max_price"`
    Sort     string  `query:"sort"`
    Order    string  `query:"order"`
    InStock  bool    `query:"in_stock"`
    Search   string  `query:"q"`
}

app.Get("/products", func(c *fiber.Ctx) error {
    // Parse all query params into struct
    filter := ProductFilter{
        Page:    1,       // defaults
        PerPage: 20,
        Sort:    "created_at",
        Order:   "desc",
    }
    
    if err := c.QueryParser(&filter); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Invalid query parameters",
        })
    }

    // Validate
    if filter.PerPage > 100 {
        filter.PerPage = 100
    }

    // Use the filter
    products, total := queryProducts(filter)

    return c.JSON(fiber.Map{
        "data": products,
        "pagination": fiber.Map{
            "page":        filter.Page,
            "per_page":    filter.PerPage,
            "total":       total,
            "total_pages": (total + int64(filter.PerPage) - 1) / int64(filter.PerPage),
        },
    })
})
// GET /products?category=electronics&min_price=100&max_price=500&sort=price&order=asc&page=2

Combining Params and Queries

// Get posts by user with filtering
app.Get("/users/:userId/posts", func(c *fiber.Ctx) error {
    // Route param — identifies the user
    userID, err := c.ParamsInt("userId")
    if err != nil {
        return c.Status(400).JSON(fiber.Map{
            "error": "Invalid user ID",
        })
    }

    // Query params — filter the user's posts
    status := c.Query("status", "published") // draft, published, archived
    tag := c.Query("tag")                     // filter by tag
    page := c.QueryInt("page", 1)
    perPage := c.QueryInt("per_page", 10)

    posts, total := getUserPosts(userID, status, tag, page, perPage)

    return c.JSON(fiber.Map{
        "user_id": userID,
        "filters": fiber.Map{
            "status": status,
            "tag":    tag,
        },
        "data":  posts,
        "total": total,
    })
})
// GET /users/42/posts?status=published&tag=golang&page=2

Building a Complete Filterable Endpoint

type OrderFilter struct {
    Page     int    `query:"page"`
    PerPage  int    `query:"per_page"`
    Status   string `query:"status"`
    FromDate string `query:"from"`
    ToDate   string `query:"to"`
    MinTotal float64 `query:"min_total"`
    Sort     string `query:"sort"`
    Order    string `query:"order"`
}

app.Get("/api/v1/customers/:customerId/orders", func(c *fiber.Ctx) error {
    // 1. Extract and validate route param
    customerID, err := c.ParamsInt("customerId")
    if err != nil {
        return c.Status(400).JSON(fiber.Map{"error": "Invalid customer ID"})
    }

    // 2. Parse query params
    filter := OrderFilter{
        Page: 1, PerPage: 20, Sort: "created_at", Order: "desc",
    }
    if err := c.QueryParser(&filter); err != nil {
        return c.Status(400).JSON(fiber.Map{"error": "Invalid filters"})
    }

    // 3. Validate query params
    validStatuses := map[string]bool{
        "": true, "pending": true, "confirmed": true,
        "shipped": true, "delivered": true, "cancelled": true,
    }
    if !validStatuses[filter.Status] {
        return c.Status(400).JSON(fiber.Map{"error": "Invalid status filter"})
    }
    if filter.PerPage > 100 {
        filter.PerPage = 100
    }

    // 4. Query data
    orders, total := getCustomerOrders(customerID, filter)

    // 5. Return paginated response
    return c.JSON(fiber.Map{
        "success": true,
        "data":    orders,
        "pagination": fiber.Map{
            "page":        filter.Page,
            "per_page":    filter.PerPage,
            "total":       total,
            "total_pages": (total + int64(filter.PerPage) - 1) / int64(filter.PerPage),
        },
    })
})
// GET /api/v1/customers/42/orders?status=shipped&from=2024-01-01&sort=total&order=desc&page=1

Best Practices

  • Validate all params — Never trust user input. Validate types, ranges, and allowed values
  • Use defaults — Always provide sensible defaults for optional query params
  • Limit per_page — Cap pagination size (max 100) to prevent clients from requesting millions of rows
  • Use QueryParser for complex filters — Struct binding is cleaner than extracting 10 individual query params
  • Document your params — API consumers need to know which params are available and their valid values
  • Consistent naming — Use snake_case for query params (per_page, min_price, not perPage, minPrice)

What's Next

You now master params and queries in Fiber — from basic extraction to complex filterable endpoints. In the next tutorial, we'll cover forms, locals, and the Next() function — handling form submissions, passing data between middleware and handlers, and controlling request flow.

GoFiberTutorialAPI