Fiber is an Express-style web framework for Go, built on top of the fasthttp engine. If you are coming from JavaScript or PHP, its routing and handler style will feel familiar within minutes — but you get Go’s type safety, concurrency and tiny deployment footprint underneath. This tutorial builds a complete CRUD API for a book collection: routing, handlers, validation, middleware and consistent JSON errors.
Step 1 — Project setup
You need Go installed (1.17 or newer — 1.21+ recommended). Create the project and pull in Fiber:
mkdir bookstore-api && cd bookstore-api
go mod init bookstore-api
go get github.com/gofiber/fiber/v2
Step 2 — The smallest possible server
Create main.go. This is a complete, runnable Fiber application:
package main
import (
"log"
"sync"
"github.com/gofiber/fiber/v2"
)
func main() {
app := fiber.New()
app.Get("/health", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"status": "ok"})
})
log.Fatal(app.Listen(":3000"))
}
Run it with go run main.go and visit http://localhost:3000/health. Every handler receives a *fiber.Ctx (the request context) and returns an error — that signature shapes everything that follows.
Step 3 — Model and in-memory store
For this tutorial the “database” is a slice guarded by a mutex, so the focus stays on the HTTP layer. Swapping it for a real database later only changes the store functions.
type Book struct {
ID int `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
Year int `json:"year"`
}
var (
books []Book
nextID = 1
mu sync.Mutex
)
The struct tags (json:"title") control the JSON field names. Without them, Go exports capitalised field names as-is.
Step 4 — CRUD handlers
Each handler does one thing: parse input, touch the store, write JSON with the right status code.
func listBooks(c *fiber.Ctx) error {
return c.JSON(books)
}
func getBook(c *fiber.Ctx) error {
id, err := c.ParamsInt("id")
if err != nil {
return c.Status(400).JSON(fiber.Map{
"error": "invalid id",
})
}
for _, b := range books {
if b.ID == id {
return c.JSON(b)
}
}
return c.Status(404).JSON(fiber.Map{
"error": "book not found",
})
}
func createBook(c *fiber.Ctx) error {
var input Book
if err := c.BodyParser(&input); err != nil {
return c.Status(400).JSON(fiber.Map{
"error": "invalid JSON body",
})
}
if input.Title == "" || input.Author == "" {
return c.Status(422).JSON(fiber.Map{
"error": "title and author are required",
})
}
mu.Lock()
defer mu.Unlock()
input.ID = nextID
nextID++
books = append(books, input)
return c.Status(201).JSON(input)
}
Delete follows the same pattern: find the index by ID, slice it out with append(books[:i], books[i+1:]...), and return 204 No Content. Update is create’s mirror — parse the body, find the record, replace its fields.
Step 5 — Wire the routes
Group the endpoints under a versioned prefix so the API can evolve without breaking clients:
api := app.Group("/api/v1")
api.Get("/books", listBooks)
api.Get("/books/:id", getBook)
api.Post("/books", createBook)
api.Put("/books/:id", updateBook)
api.Delete("/books/:id", deleteBook)
Step 6 — Middleware: logging, recovery, CORS
Fiber ships the common middleware in its repository. Three lines add request logging, panic recovery and cross-origin access for a frontend:
import (
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
)
app.Use(recover.New())
app.Use(logger.New())
app.Use(cors.New())
Add a custom ErrorHandler in the Fiber config to make every error response the same shape — {"error": "..."} — so API consumers never have to guess (for idiomatic error models and wrapping, review our practical guide to Go error handling). Consistency here is what makes an API feel professional.
Step 7 — Test it like a client
curl -X POST http://localhost:3000/api/v1/books \
-H "Content-Type: application/json" \
-d '{"title":"The Go Programming Language","author":"Alan Donovan & Brian Kernighan","year":2015}'
curl http://localhost:3000/api/v1/books
From here the natural next steps are a real database (PostgreSQL with pgx or sqlc), concurrent background workers (learn how with Goroutines and Channels explained simply), request validation with struct tags, and environment-based configuration for the port and allowed origins. The handler structure above survives all of those changes untouched — which is exactly why it is worth learning on a small project first.
Fiber’s job is the HTTP layer: routing, parsing, responding. Keep business rules in plain Go functions behind the handlers, and the framework never becomes a cage.



