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.