The final episode of this series brings it all together: building a complete CRUD API with Fiber v3 — model and store, route groups, binding and validation, custom error handler, middleware, and testing with curl. All the material from episodes 0-21 is combined into one project.

All previous episodes covered the pieces. Episode 22 — the series finale — assembles them into a complete CRUD project: a book management application with Fiber v3 that uses routing, groups, middleware, binding, validation, and error handling all at once.
Separate responsibilities into small files so they're easy to test:
main.go — application assembly and routesbook.go — the model with validation tagsstore.go — in-memory data storagehandler.go — CRUD handlers and the error handlermain.go combines configuration, middleware, and routes:
func main() {
app := fiber.New(fiber.Config{
ErrorHandler: customErrorHandler,
BodyLimit: 1 * 1024 * 1024,
})
app.Use(logger.New(), recover.New())
books := app.Group("/books")
books.Get("/", listBooks)
books.Get("/:id<int>", getBook)
books.Post("/", createBook)
books.Put("/:id<int>", updateBook)
books.Delete("/:id<int>", deleteBook)
app.Listen(":3000")
}The custom ErrorHandler (episode 9), the logger/recover middleware (episode 6), and the :id<int> constraint (episode 4) are all installed at once.
type Book struct {
ID int `json:"id"`
Title string `json:"title" validate:"required,min=1"`
Author string `json:"author" validate:"required"`
Year int `json:"year" validate:"gte=1900,lte=2100"`
}The json tags are for binding (episode 5); the validate tags ensure Title/Author are required and Year falls within a sensible range.
type Store struct {
mu sync.RWMutex
items map[int]Book
next int
}
func (s *Store) Create(b Book) Book {
s.mu.Lock()
defer s.mu.Unlock()
s.next++
b.ID = s.next
s.items[b.ID] = b
return b
}
func (s *Store) Get(id int) (Book, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
b, ok := s.items[id]
return b, ok
}sync.RWMutex protects the map from concurrent access because Go handlers run in parallel.
The handlers use binding and automatic validation:
var store = &Store{items: map[int]Book{}}
func createBook(c fiber.Ctx) error {
var b Book
if err := c.Bind().JSON(&b); err != nil {
return err
}
return c.Status(fiber.StatusCreated).JSON(store.Create(b))
}
func listBooks(c fiber.Ctx) error {
return c.JSON(store.All())
}c.Bind().JSON(&b) parses the body and runs validation; errors are forwarded to the ErrorHandler.
func getBook(c fiber.Ctx) error {
id, _ := strconv.Atoi(c.Params("id"))
b, ok := store.Get(id)
if !ok {
return fiber.NewError(fiber.StatusNotFound, "buku tidak ditemukan")
}
return c.JSON(b)
}
func updateBook(c fiber.Ctx) error {
id, _ := strconv.Atoi(c.Params("id"))
var b Book
if err := c.Bind().JSON(&b); err != nil {
return err
}
updated, ok := store.Update(id, b)
if !ok {
return fiber.NewError(fiber.StatusNotFound, "buku tidak ditemukan")
}
return c.JSON(updated)
}
func deleteBook(c fiber.Ctx) error {
id, _ := strconv.Atoi(c.Params("id"))
if err := store.Delete(id); err != nil {
return fiber.NewError(fiber.StatusNotFound, "buku tidak ditemukan")
}
return c.SendStatus(fiber.StatusNoContent)
}getBook returns a 404 via fiber.NewError when the id doesn't exist; deleteBook replies 204 after a successful delete.
func customErrorHandler(c fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
var fe *fiber.Error
if errors.As(err, &fe) {
code = fe.Code
}
return c.Status(code).JSON(fiber.Map{
"error": fiber.Map{
"code": code,
"message": err.Error(),
},
})
}curl -X POST -H "Content-Type: application/json" \
--data '{"title":"Clean Code","author":"Robert Martin","year":2008}' http://localhost:3000/books
curl http://localhost:3000/books
curl http://localhost:3000/books/1
curl -X PUT -H "Content-Type: application/json" --data '{"title":"Clean Code","author":"Robert C. Martin","year":2008}' http://localhost:3000/books/1
curl -X DELETE http://localhost:3000/books/1
curl http://localhost:3000/books/999Congratulations — you've completed the Learning Fiber series! Key takeaways:
:id<int>) keep routes organized.fiber.NewError plus a custom ErrorHandler unify the error format.bun run dev and test with curl.