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.

MacBook showing code in an editor while testing a Go Fiber API

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.