This episode dissects Fiber's router thoroughly: path parameters, wildcards, and query parameters; route constraints like :id<int> and minLen; route chaining and domain routing; plus groups, nested groups, and mounting sub-apps for tidy URL organization.

The router is the heart of a web framework. Episode 4 dissects routing, params, and constraints in Fiber v3 — from basic patterns like /users/:id, to new features like typed constraints and domain routing.
After this episode, you can design clean URLs, restrict parameter shapes right at the route level, and organize routes into groups. This is a skill used in every following episode.
Path parameters are written with a colon prefix. Fiber captures the value and makes it available via c.Params:
app.Get("/users/:id", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"id": c.Params("id")})
})
app.Get("/files/*", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"path": c.Params("*")})
})/users/:id matches a single segment — /users/42 — while the wildcard * matches the rest of the path, for example /files/a/b/c.txt. c.Params("*") retrieves the entire portion matched by the wildcard. In Fiber, a wildcard can also be mixed in the middle of a path, for example /users/*/posts.
Query parameters don't affect route matching, but can be read via c.Query:
app.Get("/search", func(c fiber.Ctx) error {
q := c.Query("q", "")
page := c.Query("page", "1")
return c.JSON(fiber.Map{"q": q, "page": page})
})c.Query("q", "") reads the value of parameter q with an empty string default if it's missing. For structured types, you can use c.Bind().Query(&struct) — details in episode 5.
Fiber v3 introduces route constraints — rules validated during matching. If a constraint isn't satisfied, the route is considered unmatched and the request moves on to the next route or toward a 404:
app.Get("/users/:id<int>", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"id": c.Params("id")})
})
app.Get("/posts/:slug<minLen(5)>", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"slug": c.Params("slug")})
})
app.Get("/orders/:date<datetime(\"2006-01-02\")>", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"date": c.Params("date")})
}):id<int> only accepts numbers, :slug<minLen(5)> requires at least 5 characters, and :date<datetime(...)> validates the date format with a Go layout. app.Get("/users/:id<int>", handler) makes a request to /users/abc unmatched and returns a 404. The performance consequence is interesting: constraints are analyzed once at registration time, not per request.
If your constraint need isn't covered, Fiber provides app.RegisterCustomConstraint. The CustomConstraint interface only needs Name and Execute implemented:
type StatusConstraint struct{}
func (*StatusConstraint) Name() string { return "status" }
func (*StatusConstraint) Execute(param string, args ...string) bool {
switch param {
case "active", "inactive", "archived":
return true
}
return false
}
func main() {
app := fiber.New()
app.RegisterCustomConstraint(&StatusConstraint{})
app.Get("/posts/:s<status>", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"status": c.Params("s")})
})
}RegisterCustomConstraint(&StatusConstraint{}) registers a new validator that is then used as <status> in a path. This pattern keeps domain rules in one place and reusable across many routes.
app.RouteChain lets you declare multiple methods on the same path in a single chain:
app.RouteChain("/posts/:id").
Get(func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"action": "get"})
}).
Post(func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"action": "post"})
}).
Delete(func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"action": "delete"})
})Fiber v3 supports routes that only match specific hostnames via app.Domain, with domain parameters retrieved through fiber.DomainParam:
app.Domain("api.example.com").Get("/users", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"tenant": "public"})
})
app.Domain(":tenant.example.com").Get("/", func(c fiber.Ctx) error {
tenant := fiber.DomainParam(c, "tenant")
return c.JSON(fiber.Map{"tenant": tenant})
})app.Domain("api.example.com") creates a separate router that only serves that hostname. The :tenant.example.com pattern is useful for multi-tenant SaaS. Important note: make sure TrustProxy is configured if you're behind a proxy, because the hostname can be influenced by headers — a topic we cover in episode 14.
app.Group creates routes sharing a common prefix. Groups can be nested and used with middleware:
api := app.Group("/api")
v1 := api.Group("/v1", authMiddleware)
v1.Get("/users", listUsers)
v1.Get("/users/:id", getUser)
v1.Post("/users", createUser)The registered routes become /api/v1/users, and all of them pass through authMiddleware, which is attached to the v1 group. app.Group("/api") returns a router that can be grouped again — the pattern used for API versioning.
Fiber v3 also supports mounting another Fiber application as a sub-app, replacing app.Mount from v2:
api := fiber.New()
api.Get("/status", func(c fiber.Ctx) error {
return c.SendString("sub-app berjalan")
})
app := fiber.New()
app.Use("/api", api)app.Use("/api", api) mounts all routes from api under the /api prefix. The difference from v2: the mount prefix isn't stripped, so inside the sub-app, c.Path() still returns the full path like /api/status.
curl -i http://localhost:3000/users/42
curl -i http://localhost:3000/users/abc
curl -i "http://localhost:3000/search?q=fiber&page=2"
curl -i http://localhost:3000/api/v1/usersThe /users/abc request should fail the <int> constraint and return a 404, while /users/42 succeeds. This whole curl -i ... sequence is a quick way to verify router behavior without a browser.
Key takeaways:
:id; wildcards are written *.c.Query("key", "default").:id<int> and :slug<minLen(5)> are validated during matching.RegisterCustomConstraint.RouteChain combines many methods on one path; Domain restricts by hostname.Group for shared prefixes; app.Use("/api", subApp) for mounting.In the next episode, episode 5, we discuss binding, extractors, and validation — mapping body, query, header, and URI to Go structs, using the extractor package to fetch values declaratively, and validating input with go-playground/validator.