Form, Locals & Next
Learn how to handle HTML form submissions, pass data between middleware and handlers using Locals, and control request flow with the Next function in Go Fiber. Covers multipart forms, file uploads, middleware chaining, and real-world patterns for building complete web applications.
Three core concepts power Fiber's request handling: Forms (receiving user-submitted data from HTML forms), Locals (passing data between middleware and handlers within a single request), and Next (controlling whether the request continues to the next handler in the chain). Together, they enable sophisticated request processing pipelines.
Handling Form Data
URL-Encoded Forms
Standard HTML forms submit data as URL-encoded key-value pairs:
// HTML form:
// <form method="POST" action="/register">
// <input name="name" type="text" />
// <input name="email" type="email" />
// <input name="password" type="password" />
// <button type="submit">Register</button>
// </form>
app.Post("/register", func(c *fiber.Ctx) error {
// Read individual form fields
name := c.FormValue("name")
email := c.FormValue("email")
password := c.FormValue("password")
// Validate
if name == "" || email == "" || password == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "All fields are required",
})
}
if len(password) < 8 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Password must be at least 8 characters",
})
}
// Process registration...
return c.JSON(fiber.Map{
"message": "Registration successful",
"user": name,
})
})
Binding Forms to Structs
Use BodyParser to bind form data directly to a struct:
type RegisterForm struct {
Name string `form:"name"`
Email string `form:"email"`
Password string `form:"password"`
Age int `form:"age"`
Newsletter bool `form:"newsletter"`
}
app.Post("/register", func(c *fiber.Ctx) error {
var form RegisterForm
if err := c.BodyParser(&form); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid form data",
})
}
// form.Name, form.Email, etc. are populated
fmt.Printf("Name: %s, Email: %s, Age: %d\n", form.Name, form.Email, form.Age)
return c.JSON(fiber.Map{"success": true})
})
Note: Use form struct tags for form data (not json). BodyParser automatically detects the content type (JSON, form-urlencoded, or multipart) and parses accordingly.
Multipart Forms (File Uploads)
// Single file upload
app.Post("/upload/avatar", func(c *fiber.Ctx) error {
// Get the uploaded file
file, err := c.FormFile("avatar")
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "No file uploaded",
})
}
// Validate file size (max 5 MB)
if file.Size > 5*1024*1024 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "File too large — max 5 MB",
})
}
// Validate file type
contentType := file.Header.Get("Content-Type")
if contentType != "image/jpeg" && contentType != "image/png" && contentType != "image/webp" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Only JPEG, PNG, and WebP images are allowed",
})
}
// Save the file
filename := fmt.Sprintf("%s_%s", uuid.New().String(), file.Filename)
savePath := fmt.Sprintf("./uploads/%s", filename)
if err := c.SaveFile(file, savePath); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to save file",
})
}
return c.JSON(fiber.Map{
"message": "File uploaded successfully",
"filename": filename,
"size": file.Size,
})
})
Multiple File Upload
app.Post("/upload/gallery", func(c *fiber.Ctx) error {
// Get the multipart form
form, err := c.MultipartForm()
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid form data"})
}
// Get all files from the "photos" field
files := form.File["photos"]
if len(files) == 0 {
return c.Status(400).JSON(fiber.Map{"error": "No files uploaded"})
}
if len(files) > 10 {
return c.Status(400).JSON(fiber.Map{"error": "Maximum 10 files allowed"})
}
uploaded := []string{}
for _, file := range files {
filename := fmt.Sprintf("%s_%s", uuid.New().String(), file.Filename)
savePath := fmt.Sprintf("./uploads/gallery/%s", filename)
if err := c.SaveFile(file, savePath); err != nil {
continue // Skip failed files
}
uploaded = append(uploaded, filename)
}
// Also read regular form fields alongside files
albumName := c.FormValue("album_name")
description := c.FormValue("description")
return c.JSON(fiber.Map{
"album": albumName,
"description": description,
"uploaded": len(uploaded),
"files": uploaded,
})
})
Locals: Passing Data Through Middleware
Locals is a request-scoped storage — data stored in Locals is available only for the current request and is automatically cleaned up when the response is sent.
Basic Locals Usage
// Middleware sets data
func AuthMiddleware(c *fiber.Ctx) error {
token := c.Get("Authorization")
user, err := validateToken(token)
if err != nil {
return c.Status(401).JSON(fiber.Map{"error": "Unauthorized"})
}
// Store user in Locals — available to all downstream handlers
c.Locals("user", user)
c.Locals("userID", user.ID)
c.Locals("role", user.Role)
return c.Next()
}
// Handler reads data
app.Get("/profile", AuthMiddleware, func(c *fiber.Ctx) error {
// Retrieve from Locals
user := c.Locals("user").(User) // Type assertion required
return c.JSON(fiber.Map{
"name": user.Name,
"email": user.Email,
"role": user.Role,
})
})
Practical Locals Patterns
// Request ID middleware — adds a unique ID to every request
func RequestIDMiddleware(c *fiber.Ctx) error {
requestID := c.Get("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
c.Locals("requestID", requestID)
c.Set("X-Request-ID", requestID)
return c.Next()
}
// Timing middleware — measures request duration
func TimingMiddleware(c *fiber.Ctx) error {
start := time.Now()
c.Locals("startTime", start)
// Continue to next handler
err := c.Next()
// After handler completes
duration := time.Since(start)
c.Set("X-Response-Time", duration.String())
return err
}
// Tenant middleware — multi-tenancy context
func TenantMiddleware(c *fiber.Ctx) error {
// Extract tenant from subdomain
host := c.Hostname() // e.g., "acme.myapp.com"
parts := strings.Split(host, ".")
if len(parts) < 3 {
return c.Status(400).JSON(fiber.Map{
"error": "Invalid tenant",
})
}
tenantSlug := parts[0]
tenant, err := findTenant(tenantSlug)
if err != nil {
return c.Status(404).JSON(fiber.Map{
"error": "Tenant not found",
})
}
c.Locals("tenant", tenant)
c.Locals("tenantID", tenant.ID)
return c.Next()
}
// Handler using tenant context
app.Get("/api/products", TenantMiddleware, func(c *fiber.Ctx) error {
tenantID := c.Locals("tenantID").(uint)
products := getProductsByTenant(tenantID)
return c.JSON(products)
})
Type-Safe Locals Helper
// Create type-safe helper functions to avoid type assertions everywhere
func GetCurrentUser(c *fiber.Ctx) (*User, bool) {
user, ok := c.Locals("user").(*User)
return user, ok
}
func GetTenantID(c *fiber.Ctx) (uint, bool) {
id, ok := c.Locals("tenantID").(uint)
return id, ok
}
// Usage in handlers
app.Get("/dashboard", AuthMiddleware, func(c *fiber.Ctx) error {
user, ok := GetCurrentUser(c)
if !ok {
return c.Status(500).JSON(fiber.Map{"error": "User context missing"})
}
return c.JSON(fiber.Map{"welcome": user.Name})
})
The Next() Function
c.Next() passes control to the next handler in the chain. It's what makes middleware possible — a middleware processes the request, calls c.Next() to pass control to the next middleware or final handler, and can optionally do post-processing after c.Next() returns.
Middleware Chain Flow
// Middleware 1: Logging
func LoggerMiddleware(c *fiber.Ctx) error {
fmt.Printf("[%s] %s %s\n", time.Now().Format("15:04:05"), c.Method(), c.Path())
// Pass to next middleware/handler
err := c.Next()
// After handler completes — log the response status
fmt.Printf("[%s] %s %s → %d\n",
time.Now().Format("15:04:05"), c.Method(), c.Path(), c.Response().StatusCode())
return err
}
// Middleware 2: Auth check
func RequireAuth(c *fiber.Ctx) error {
token := c.Get("Authorization")
if token == "" {
// DON'T call c.Next() — stop the chain
return c.Status(401).JSON(fiber.Map{"error": "Unauthorized"})
}
// Token exists — continue to the next handler
return c.Next()
}
// Final handler
app.Get("/api/data", LoggerMiddleware, RequireAuth, func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"data": "secret stuff"})
})
// Flow:
// 1. LoggerMiddleware → logs request → calls c.Next()
// 2. RequireAuth → checks token → calls c.Next() (or returns 401)
// 3. Final handler → returns response
// 4. Back to LoggerMiddleware (after c.Next()) → logs response status
Conditional Next
// Role-based access control
func RequireRole(roles ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
user, ok := GetCurrentUser(c)
if !ok {
return c.Status(401).JSON(fiber.Map{"error": "Unauthorized"})
}
// Check if user's role is in the allowed roles
allowed := false
for _, role := range roles {
if user.Role == role {
allowed = true
break
}
}
if !allowed {
// Don't call Next() — block access
return c.Status(403).JSON(fiber.Map{
"error": "Forbidden — requires role: " + strings.Join(roles, " or "),
})
}
// Role matches — continue
return c.Next()
}
}
// Usage
app.Delete("/api/users/:id", AuthMiddleware, RequireRole("admin"), deleteUser)
app.Get("/api/reports", AuthMiddleware, RequireRole("admin", "manager"), getReports)
Putting It All Together
func main() {
app := fiber.New()
// Global middleware — runs on every request
app.Use(RequestIDMiddleware)
app.Use(TimingMiddleware)
app.Use(LoggerMiddleware)
// Public routes — no auth needed
app.Post("/login", loginHandler)
app.Post("/register", registerHandler)
// Protected API routes
api := app.Group("/api/v1", AuthMiddleware)
// User routes
api.Get("/profile", getProfile)
api.Put("/profile", updateProfile)
api.Post("/profile/avatar", uploadAvatar) // Form file upload
// Admin routes — require admin role
admin := api.Group("/admin", RequireRole("admin"))
admin.Get("/users", listUsers)
admin.Delete("/users/:id", deleteUser)
log.Fatal(app.Listen(":3000"))
}
What's Next
You now understand Fiber's form handling, Locals for request-scoped data, and Next for middleware chaining. In the next tutorial, we'll explore Fiber's response methods — Send, SendStatus, SendFile, and Status — the complete toolkit for crafting HTTP responses.