Download Files, Params and JSON APIs
Learn how to serve file downloads, work with route parameters, and build structured JSON APIs in Go Fiber. This tutorial covers c.Download, c.SendFile, dynamic route params, type conversion, and advanced JSON response patterns for building production-ready APIs.
Your API needs to do more than return JSON — sometimes you need to serve downloadable files (reports, exports, images), extract dynamic values from URLs, and build well-structured JSON APIs with pagination, filtering, and proper error responses. This tutorial covers all three.
Serving File Downloads
Fiber provides multiple methods for sending files to the client:
c.Download() — Force Browser Download
Use c.Download() when you want the browser to download the file (save dialog appears):
// Download a file — triggers browser save dialog
app.Get("/download/report", func(c *fiber.Ctx) error {
// Download file with a custom filename
return c.Download("./files/annual-report-2024.pdf", "report.pdf")
})
// Dynamic file downloads
app.Get("/download/:filename", func(c *fiber.Ctx) error {
filename := c.Params("filename")
filepath := fmt.Sprintf("./uploads/%s", filename)
// Check if file exists
if _, err := os.Stat(filepath); os.IsNotExist(err) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": "File not found",
})
}
return c.Download(filepath, filename)
})
The first argument is the file path on the server. The second argument (optional) is the filename the user will see when downloading. c.Download() sets the Content-Disposition: attachment header automatically.
c.SendFile() — Inline File Display
Use c.SendFile() when you want the browser to display the file inline (images, PDFs in browser):
// Serve an image inline (browser displays it)
app.Get("/images/:name", func(c *fiber.Ctx) error {
name := c.Params("name")
filepath := fmt.Sprintf("./public/images/%s", name)
return c.SendFile(filepath)
})
// Serve user avatars
app.Get("/avatar/:userid", func(c *fiber.Ctx) error {
userID := c.Params("userid")
// Look up user's avatar path from database
avatarPath := getUserAvatar(userID)
if avatarPath == "" {
// Serve default avatar
return c.SendFile("./public/images/default-avatar.png")
}
return c.SendFile(avatarPath)
})
Key difference: c.Download() forces a save dialog. c.SendFile() lets the browser decide how to display it (images are shown inline, PDFs may open in browser).
Streaming Large Files
For large files (video, large datasets), stream instead of loading the entire file into memory:
app.Get("/stream/video/:name", func(c *fiber.Ctx) error {
name := c.Params("name")
filepath := fmt.Sprintf("./videos/%s", name)
return c.SendFile(filepath)
// Fiber handles range requests automatically
// Client can seek to any position in the video
})
Generating Files on the Fly
import "encoding/csv"
app.Get("/export/users", func(c *fiber.Ctx) error {
c.Set("Content-Type", "text/csv")
c.Set("Content-Disposition", "attachment; filename=users.csv")
writer := csv.NewWriter(c)
// Write CSV header
writer.Write([]string{"ID", "Name", "Email", "Created"})
// Write data rows
users := getAllUsers()
for _, user := range users {
writer.Write([]string{
user.ID,
user.Name,
user.Email,
user.CreatedAt.Format("2006-01-02"),
})
}
writer.Flush()
return nil
})
Working with Route Parameters
Basic Params
// Single param
app.Get("/users/:id", func(c *fiber.Ctx) error {
id := c.Params("id") // string
return c.SendString("User ID: " + id)
})
// Multiple params
app.Get("/users/:userId/posts/:postId", func(c *fiber.Ctx) error {
userID := c.Params("userId")
postID := c.Params("postId")
return c.JSON(fiber.Map{
"user_id": userID,
"post_id": postID,
})
})
// Optional params (add ? suffix)
app.Get("/products/:category?", func(c *fiber.Ctx) error {
category := c.Params("category")
if category == "" {
// No category — return all products
return c.JSON(getAllProducts())
}
return c.JSON(getProductsByCategory(category))
})
Type-Safe Param Parsing
Route params are always strings. Convert them safely:
app.Get("/users/:id", func(c *fiber.Ctx) error {
// Parse as integer
id, err := c.ParamsInt("id")
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid user ID — must be a number",
})
}
user, err := getUserByID(id)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": "User not found",
})
}
return c.JSON(user)
})
Wildcard Routes
// Match any path after /static/
app.Get("/static/*", func(c *fiber.Ctx) error {
// c.Params("*") returns the wildcard portion
filePath := c.Params("*") // e.g., "css/style.css"
return c.SendFile("./public/" + filePath)
})
// Catch-all for SPA routing
app.Get("/*", func(c *fiber.Ctx) error {
return c.SendFile("./public/index.html")
})
Building JSON APIs
Structured Response Pattern
Create a consistent response helper:
// helpers/response.go
package helpers
import "github.com/gofiber/fiber/v2"
type PaginationMeta struct {
Page int `json:"page"`
PerPage int `json:"per_page"`
Total int64 `json:"total"`
TotalPages int `json:"total_pages"`
}
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
Pagination *PaginationMeta `json:"pagination,omitempty"`
}
func Success(c *fiber.Ctx, data interface{}) error {
return c.JSON(Response{
Success: true,
Data: data,
})
}
func SuccessWithMessage(c *fiber.Ctx, data interface{}, message string) error {
return c.JSON(Response{
Success: true,
Data: data,
Message: message,
})
}
func Created(c *fiber.Ctx, data interface{}) error {
return c.Status(fiber.StatusCreated).JSON(Response{
Success: true,
Data: data,
})
}
func Paginated(c *fiber.Ctx, data interface{}, meta PaginationMeta) error {
return c.JSON(Response{
Success: true,
Data: data,
Pagination: &meta,
})
}
func Error(c *fiber.Ctx, status int, message string) error {
return c.Status(status).JSON(Response{
Success: false,
Error: message,
})
}
func BadRequest(c *fiber.Ctx, message string) error {
return Error(c, fiber.StatusBadRequest, message)
}
func NotFound(c *fiber.Ctx, message string) error {
return Error(c, fiber.StatusNotFound, message)
}
func ServerError(c *fiber.Ctx, message string) error {
return Error(c, fiber.StatusInternalServerError, message)
}
Using Response Helpers in Handlers
import "fiber-books-api/helpers"
app.Get("/api/v1/products", func(c *fiber.Ctx) error {
page := c.QueryInt("page", 1)
perPage := c.QueryInt("per_page", 20)
products, total := getProducts(page, perPage)
totalPages := int(total) / perPage
if int(total)%perPage > 0 {
totalPages++
}
return helpers.Paginated(c, products, helpers.PaginationMeta{
Page: page,
PerPage: perPage,
Total: total,
TotalPages: totalPages,
})
})
// Response:
// {
// "success": true,
// "data": [...],
// "pagination": {
// "page": 1,
// "per_page": 20,
// "total": 156,
// "total_pages": 8
// }
// }
Fiber Map for Quick Responses
For simple responses, use fiber.Map (shorthand for map[string]interface{}):
app.Get("/health", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"status": "healthy",
"version": "1.0.0",
"uptime": time.Since(startTime).String(),
})
})
Setting Response Headers
app.Get("/api/data", func(c *fiber.Ctx) error {
// Set custom headers
c.Set("X-Request-ID", uuid.New().String())
c.Set("X-Response-Time", "42ms")
c.Set("Cache-Control", "public, max-age=3600")
return c.JSON(fiber.Map{"data": "value"})
})
What's Next
You now know how to serve file downloads, work with route parameters, and build structured JSON APIs with pagination. In the next tutorial, we'll explore Fiber's cookie handling — setting, reading, parsing, and clearing cookies for session management and user preferences.