This episode covers input handling in Fiber v3: binding body, query, URI, and header to structs with json, form, and query tags; the extractor package for fetching values declaratively with fallbacks; and input validation with go-playground/validator.

In episode 4 you learned to read input manually via c.Params and c.Query. Episode 5 goes one level up: binding, extractors, and validation — mapping input to structs, extracting values declaratively, and making sure data is valid before it's processed.
The most common way: bind a JSON body to a struct with the json tag. Fiber also recognizes form, xml, and msgpack according to Content-Type:
type User struct {
Name string `json:"name" form:"name"`
Age int `json:"age" form:"age"`
}
app.Post("/users", func(c fiber.Ctx) error {
var user User
if err := c.Bind().Body(&user); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
}
return c.JSON(user)
})c.Bind().Body(&user) reads the body, selects the parser based on Content-Type, and fills the struct. If the payload is JSON, the json tags are used; if it's a form, the form tags. Both can be written together as in the example above.
Binding isn't limited to the body. Other sources can also be bound to the same struct:
type Filter struct {
Search string `query:"search"`
Tags []string `query:"tags"`
}
type ProductParam struct {
ID uint `uri:"id"`
}
app.Get("/products", func(c fiber.Ctx) error {
var f Filter
if err := c.Bind().Query(&f); err != nil {
return err
}
return c.JSON(f)
})
app.Get("/products/:id", func(c fiber.Ctx) error {
var p ProductParam
if err := c.Bind().URI(&p); err != nil {
return err
}
return c.JSON(p)
})c.Bind().Query(&f) maps query parameters to query tags, and c.Bind().URI(&p) maps path parameters to uri tags. For headers, c.Bind().Header(...) is available with the header tag, and c.Bind().Cookie(...) for cookies. If you want all sources at once, c.Bind().All(&struct) follows the order URI, body, query, header, then cookie.
The github.com/gofiber/fiber/v3/extractors package provides extractor functions that can be chained. Middleware like session and JWT use it, but you can also use it for your own needs:
import "github.com/gofiber/fiber/v3/extractors"
apiKey := extractors.Chain(
extractors.FromHeader("X-API-Key"),
extractors.FromQuery("api_key"),
extractors.FromCookie("api_key"),
)
app.Use(func(c fiber.Ctx) error {
key, err := apiKey.Extract(c)
if err != nil {
return c.Status(fiber.StatusUnauthorized).SendString("API key dibutuhkan")
}
c.Locals("api_key", key)
return c.Next()
})extractors.Chain(...) tries the X-API-Key header first, then the api_key query, then the api_key cookie. If none are present, .Extract(c) returns an error. This pattern makes extraction logic reusable and usable in many middleware.
Fiber doesn't reinvent validators. It uses the StructValidator interface and integrates it with go-playground/validator:
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v3"
)
type structValidator struct {
validate *validator.Validate
}
func (v *structValidator) Validate(out any) error {
return v.validate.Struct(out)
}
func main() {
app := fiber.New(fiber.Config{
StructValidator: &structValidator{validate: validator.New()},
})
}With fiber.Config{StructValidator: ...}, every binding automatically runs validation if the struct uses validate tags.
type RegisterRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
Age int `json:"age" validate:"gte=18,lte=99"`
}
app.Post("/register", func(c fiber.Ctx) error {
var req RegisterRequest
if err := c.Bind().JSON(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
}
return c.Status(fiber.StatusCreated).JSON(req)
})validate:"required,email" ensures the field is required and email-shaped; min=8 requires a password of at least 8 characters; gte=18,lte=99 limits the age range. c.Bind().JSON(&req) runs automatic validation and returns an error if any field violates the rules.
By default, binding errors can be inspected with errors.As to get details about the failure source:
var req struct {
ID int `uri:"id"`
Name string `json:"name"`
}
if err := c.Bind().All(&req); err != nil {
var be *fiber.BindError
if errors.As(err, &be) && be.Source == fiber.BindSourceURI {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": "resource tidak ditemukan",
})
}
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "request tidak valid",
})
}*fiber.BindError contains Source and Field that describe where the failure came from. There's also WithAutoHandling() which automatically returns status 400 — suitable when you don't need detailed control. Note: validation errors aren't wrapped in BindError, so use errors.As to distinguish them.
curl -X POST -H "Content-Type: application/json" --data '{"name":"Budi","age":30}' http://localhost:3000/users
curl -X POST -H "Content-Type: application/json" \
--data '{"email":"budi@example.com","password":"rahasia123","age":30}' http://localhost:3000/registerKey takeaways:
c.Bind().Body(), .Query(), .URI(), .Header(), and .Cookie() map input to structs.json, form, query, uri, header, cookie.c.Bind().All() combines all sources in the order URI, body, query, header, cookie.extractors package provides declarative extraction with Chain and fallbacks.fiber.Config{StructValidator} with validate tags.*fiber.BindError helps distinguish the source of binding errors.In the next episode, episode 6, we discuss middleware and hooks — writing your own middleware with app.Use, understanding sequential execution and c.Next(), and using v3 lifecycle hooks like OnListen, OnPreShutdown, and OnPostShutdown.