ZetCode

Go Fiber

last modified October 30, 2026

In this article we show how to create web applications in Go with Fiber. Fiber is an Express-inspired framework built on top of fasthttp; it provides routing, middleware, static file serving and template rendering with a small API.

The examples use Fiber v3, where a handler receives a fiber.Ctx interface and returns an error. All examples were compiled and run with Go 1.27.0 and Fiber v3.6.1; they listen on port 3001, so run one at a time.

Installation

Fiber is a module dependency. The import path contains the major version, and the startup message can be disabled when the program prints its own log line.

$ mkdir fiber-demo
$ cd fiber-demo
$ go mod init com.zetcode/fiber
go: creating new go.mod: module com.zetcode/fiber
$ go get github.com/gofiber/fiber/v3@v3.6.1 2>&1 | grep 'fiber/v3'
go: added github.com/gofiber/fiber/v3 v3.6.1
$ go get github.com/gofiber/template/html/v3@v3.0.12 2>&1 | grep 'template/html'
go: added github.com/gofiber/template/html/v3 v3.0.12

The first application

fiber.New creates an application, Get registers a route and Listen starts the server. A handler returns c.SendString, which writes a text response with status 200.

main.go
package main

import (
    "log"

    "github.com/gofiber/fiber/v3"
)

func main() {

    app := fiber.New()

    app.Get("/", func(c fiber.Ctx) error {
        return c.SendString("Hello from Fiber")
    })

    log.Println("listening on http://localhost:3001")
    log.Fatal(app.Listen(":3001", fiber.ListenConfig{DisableStartupMessage: true}))
}

ListenConfig suppresses the multi-line banner that Fiber normally prints, which keeps the output of the example small. The response has the text content type and a content length of 16 bytes.

$ go run main.go &
2026/10/30 20:40:24 listening on http://localhost:3001
$ curl -i localhost:3001/
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 16

Hello from Fiber

Status codes and headers

c.Status sets the status code and returns the context, so it can be chained with another response method. c.Set adds a response header. c.SendStatus sends only the status and its standard text.

main.go
package main

import (
    "log"

    "github.com/gofiber/fiber/v3"
)

func main() {

    app := fiber.New()

    app.Get("/created", func(c fiber.Ctx) error {
        c.Set("X-App", "fiber-demo")
        return c.SendStatus(fiber.StatusCreated)
    })

    log.Fatal(app.Listen(":3001", fiber.ListenConfig{DisableStartupMessage: true}))
}

SendStatus(201) writes the standard reason phrase Created as the body. For an empty body, use c.Status(fiber.StatusCreated).Send(nil) or c.SendStatus together with a no-content status.

$ curl -i localhost:3001/created
HTTP/1.1 201 Created
Content-Type: text/plain; charset=utf-8
Content-Length: 7
X-App: fiber-demo

Created

JSON responses

c.JSON encodes any value with encoding/json and sets the JSON content type. Struct tags control the field names.

main.go
package main

import (
    "log"

    "github.com/gofiber/fiber/v3"
)

type Movie struct {
    ID    int    `json:"id"`
    Title string `json:"title"`
}

func main() {

    app := fiber.New()

    app.Get("/movies", func(c fiber.Ctx) error {
        movies := []Movie{
            {ID: 1, Title: "Toy Story"},
            {ID: 2, Title: "The Raid"},
            {ID: 3, Title: "Hero"},
        }
        return c.JSON(movies)
    })

    log.Fatal(app.Listen(":3001", fiber.ListenConfig{DisableStartupMessage: true}))
}

The slice is encoded as a JSON array and every struct as an object. Fiber sets Content-Type: application/json; charset=utf-8.

$ curl localhost:3001/movies
[{"id":1,"title":"Toy Story"},{"id":2,"title":"The Raid"},{"id":3,"title":"Hero"}]

Query parameters

c.Query returns a query string value and accepts a default for a missing parameter. The default makes optional parameters explicit.

main.go
package main

import (
    "fmt"
    "log"

    "github.com/gofiber/fiber/v3"
)

func main() {

    app := fiber.New()

    app.Get("/hello", func(c fiber.Ctx) error {
        name := c.Query("name", "guest")
        age := c.Query("age", "unknown")
        return c.SendString(fmt.Sprintf("%s is %s years old", name, age))
    })

    log.Fatal(app.Listen(":3001", fiber.ListenConfig{DisableStartupMessage: true}))
}

The query string is not part of the route, so one handler serves every combination of parameters. Missing values are replaced by the defaults.

$ curl 'localhost:3001/hello?name=Peter&age=45'; echo
Peter is 45 years old
$ curl localhost:3001/hello; echo
guest is unknown years old

Path parameters

A colon in the route declares a path parameter. c.Params returns the matched text, which still has to be converted to a number when the route expects an identifier.

main.go
package main

import (
    "fmt"
    "log"
    "strconv"

    "github.com/gofiber/fiber/v3"
)

func main() {

    app := fiber.New()

    app.Get("/users/:id", func(c fiber.Ctx) error {
        id, err := strconv.Atoi(c.Params("id"))
        if err != nil {
            return fiber.NewError(fiber.StatusBadRequest, "invalid id")
        }
        return c.SendString(fmt.Sprintf("user %d", id))
    })

    log.Fatal(app.Listen(":3001", fiber.ListenConfig{DisableStartupMessage: true}))
}

A valid number is echoed back. A value that cannot be converted returns a fiber.Error, which the default error handler turns into a 400 response with the error message.

$ curl localhost:3001/users/42; echo
user 42
$ curl -i localhost:3001/users/abc
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
Content-Length: 10

invalid id

Request headers

c.Get reads a request header. It also accepts a default value, which is useful for optional headers such as API tokens.

main.go
package main

import (
    "fmt"
    "log"

    "github.com/gofiber/fiber/v3"
)

func main() {

    app := fiber.New()

    app.Get("/agent", func(c fiber.Ctx) error {
        ua := c.Get("User-Agent")
        token := c.Get("X-Token", "anonymous")
        return c.SendString(fmt.Sprintf("agent=%s token=%s", ua, token))
    })

    log.Fatal(app.Listen(":3001", fiber.ListenConfig{DisableStartupMessage: true}))
}

The user agent is always sent by curl, while the custom token is provided only in the second request. Header names are case-insensitive.

$ curl localhost:3001/agent; echo
agent=curl/7.81.0 token=anonymous
$ curl -H 'X-Token: secret' -A 'test-agent' localhost:3001/agent; echo
agent=test-agent token=secret

Static files and downloads

In Fiber v3 static file serving is a middleware. static.New serves a directory, c.SendFile sends a single file for display and c.Download sends it as an attachment.

main.go
package main

import (
    "log"

    "github.com/gofiber/fiber/v3"
    "github.com/gofiber/fiber/v3/middleware/static"
)

func main() {

    app := fiber.New()

    app.Use(static.New("./public"))

    app.Get("/download", func(c fiber.Ctx) error {
        return c.Download("./assets/logo.png")
    })

    app.Get("/view", func(c fiber.Ctx) error {
        return c.SendFile("./assets/logo.png")
    })

    log.Fatal(app.Listen(":3001", fiber.ListenConfig{DisableStartupMessage: true}))
}

The static middleware serves index.html for the root path. The download response adds Content-Disposition, while the view response does not, so a browser displays the image instead of saving it.

$ curl -i localhost:3001/
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 93

<!DOCTYPE html>
...
$ curl -I localhost:3001/download
HTTP/1.1 200 OK
Content-Type: image/png
Content-Length: 219
Content-Disposition: attachment; filename="logo.png"
$ curl -I localhost:3001/view
HTTP/1.1 200 OK
Content-Type: image/png
Content-Length: 219

HTML templates

The html template engine uses the standard html/template syntax. It is registered in the application configuration, and c.Render renders a named template with data.

views/show.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Show</title></head>
<body><p>{{ .Name }} has {{ .Count }} messages.</p></body>
</html>

The template contains two actions and no application logic; the values are supplied by the handler.

main.go
package main

import (
    "log"

    "github.com/gofiber/fiber/v3"
    "github.com/gofiber/template/html/v3"
)

func main() {

    app := fiber.New(fiber.Config{
        Views: html.New("./views", ".html"),
    })

    app.Get("/show", func(c fiber.Ctx) error {
        return c.Render("show", fiber.Map{
            "Name":  "Lucia",
            "Count": 3,
        })
    })

    log.Fatal(app.Listen(":3001", fiber.ListenConfig{DisableStartupMessage: true}))
}

The engine adds the file extension, so the template name is show. The values are passed in a map and inserted at the {{ }} actions by the template engine.

$ curl localhost:3001/show
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Show</title></head>
<body><p>Lucia has 3 messages.</p></body>
</html>

Groups, middleware and errors

app.Group prefixes a set of routes, app.Use adds middleware, and c.Next continues with the next handler. A middleware registered after the routes acts as the not-found handler.

main.go
package main

import (
    "log"

    "github.com/gofiber/fiber/v3"
)

func requestID(c fiber.Ctx) error {
    c.Set("X-Request-ID", "demo-123")
    return c.Next()
}

func main() {

    app := fiber.New()

    app.Use(requestID)

    api := app.Group("/api")
    api.Get("/users", func(c fiber.Ctx) error {
        return c.JSON(fiber.Map{"users": []string{"Lucia", "Peter"}})
    })

    app.Get("/fail", func(c fiber.Ctx) error {
        return fiber.NewError(fiber.StatusBadRequest, "bad input")
    })

    app.Use(func(c fiber.Ctx) error {
        return c.Status(fiber.StatusNotFound).SendString("custom not found")
    })

    log.Fatal(app.Listen(":3001", fiber.ListenConfig{DisableStartupMessage: true}))
}

The request ID middleware runs for every route and adds the header to both successful and failed responses. The group keeps the API prefix in one place, and the final middleware answers every unmatched path with 404.

$ curl -i localhost:3001/api/users; echo
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 27
X-Request-Id: demo-123

{"users":["Lucia","Peter"]}
$ curl -i localhost:3001/fail; echo
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
Content-Length: 9
X-Request-Id: demo-123

bad input
$ curl -i localhost:3001/nope; echo
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
Content-Length: 16
X-Request-Id: demo-123

custom not found

Source

Go Fiber - Github page
Fiber - documentation

In this article we have created web applications in Go with Fiber. We started a server, set status codes and headers, returned JSON, read query, path and header values, served static files and downloads, rendered an HTML template, and combined groups, middleware, errors and a custom 404 handler.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Go tutorials.