Episode 3 of 8

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.

GoFiberTutorialAPI