π Minimal CRUD
This guide shows a minimal Fiber app with two things most "hello world" examples leave out:
- Liveness, readiness, and startup probes using the
healthcheckmiddleware - A tiny in-memory CRUD for
Item(list,create,get,update,delete)
This example stores data in memory. It is intended for learning, smoke tests, and CI checks, not for production persistence. Restarting the app clears all data.
Set up the moduleβ
mkdir crud && cd crud
go mod init example.com/crud
go get github.com/gofiber/fiber/v3
Full exampleβ
package main
import (
"cmp"
"log"
"slices"
"strconv"
"sync"
"sync/atomic"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/healthcheck"
)
type Item struct {
ID int `json:"id"`
Name string `json:"name"`
}
type store struct {
mu sync.Mutex
items map[int]Item
nextID int
}
func newStore() *store {
return &store{items: make(map[int]Item), nextID: 1}
}
func (s *store) list() []Item {
s.mu.Lock()
out := make([]Item, 0, len(s.items))
for _, it := range s.items {
out = append(out, it)
}
s.mu.Unlock()
// Map iteration is unordered, so sort before answering, and do it off the
// lock: out is local from here on.
slices.SortFunc(out, func(a, b Item) int { return cmp.Compare(a.ID, b.ID) })
return out
}
func (s *store) create(name string) Item {
s.mu.Lock()
defer s.mu.Unlock()
it := Item{ID: s.nextID, Name: name}
s.items[it.ID] = it
s.nextID++
return it
}
func (s *store) get(id int) (Item, bool) {
s.mu.Lock()
defer s.mu.Unlock()
it, ok := s.items[id]
return it, ok
}
func (s *store) update(id int, name string) (Item, bool) {
s.mu.Lock()
defer s.mu.Unlock()
it, ok := s.items[id]
if !ok {
return Item{}, false
}
it.Name = name
s.items[id] = it
return it, true
}
func (s *store) delete(id int) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.items[id]; !ok {
return false
}
delete(s.items, id)
return true
}
func main() {
app := fiber.New()
s := newStore()
// Readiness needs a probe of its own: the default one always answers
// "healthy", so it could never take this instance out of rotation.
var ready atomic.Bool
readiness := healthcheck.Config{Probe: func(fiber.Ctx) bool { return ready.Load() }}
app.Get(healthcheck.LivenessEndpoint, healthcheck.New())
app.Get(healthcheck.ReadinessEndpoint, healthcheck.New(readiness))
app.Get(healthcheck.StartupEndpoint, healthcheck.New())
app.Get("/items", func(c fiber.Ctx) error {
return c.JSON(s.list())
})
app.Post("/items", func(c fiber.Ctx) error {
var body struct {
Name string `json:"name"`
}
if err := c.Bind().Body(&body); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid body")
}
return c.Status(fiber.StatusCreated).JSON(s.create(body.Name))
})
app.Get("/items/:id", func(c fiber.Ctx) error {
id, err := strconv.Atoi(c.Params("id"))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid id")
}
it, ok := s.get(id)
if !ok {
return fiber.NewError(fiber.StatusNotFound, "not found")
}
return c.JSON(it)
})
app.Put("/items/:id", func(c fiber.Ctx) error {
id, err := strconv.Atoi(c.Params("id"))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid id")
}
var body struct {
Name string `json:"name"`
}
if err := c.Bind().Body(&body); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid body")
}
it, ok := s.update(id, body.Name)
if !ok {
return fiber.NewError(fiber.StatusNotFound, "not found")
}
return c.JSON(it)
})
app.Delete("/items/:id", func(c fiber.Ctx) error {
id, err := strconv.Atoi(c.Params("id"))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid id")
}
if !s.delete(id) {
return fiber.NewError(fiber.StatusNotFound, "not found")
}
return c.SendStatus(fiber.StatusNoContent)
})
// In memory, so ready right away and the flag is never observed false. A
// real service starts Listen first and flips this once its database is up.
ready.Store(true)
log.Fatal(app.Listen(":3000"))
}
The route path :id is a parameter; see
Routing. c.Bind().Body maps the request body onto
a struct, see Bind, and fiber.NewError hands the
status and message to the central error handler, see
Error handling. atomic.Bool is a flag that is safe to
read and write from many requests at once.
Try it outβ
Start the app:
go run main.go
Check the probes. Each answers OK as plain text, and -f makes curl exit
non-zero on a 4xx or 5xx, which is what lets a CI step fail. Use
--fail-with-body instead when the log should also carry the error message:
curl -fsS http://localhost:3000/livez
curl -fsS http://localhost:3000/readyz
curl -fsS http://localhost:3000/startupz
Pass ResponseFormat: healthcheck.FormatJSON in the config to answer
{"status":"OK"} instead; see
Healthcheck.
Create an item:
curl -fsS -X POST http://localhost:3000/items \
-H "Content-Type: application/json" \
-d '{"name":"first"}'
# {"id":1,"name":"first"}
List, get, update, delete:
curl -fsS http://localhost:3000/items
curl -fsS http://localhost:3000/items/1
curl -fsS -X PUT http://localhost:3000/items/1 \
-H "Content-Type: application/json" \
-d '{"name":"updated"}'
curl -fsS -X DELETE http://localhost:3000/items/1
A request for an item that is gone answers 404 with the message as plain
text, not as JSON. Error handling shows how to answer
JSON instead:
curl -sS -w '\n%{http_code}\n' http://localhost:3000/items/1
# not found
# 404
Notesβ
- The store uses a
sync.Mutexso it is safe to call from multiple handlers. - The handlers accept any name, including an empty one. See Validation for rejecting bad input.
- Store
falseinreadywhile shutting down so a load balancer drains this instance;app.Hooks().OnPreShutdownruns while the server still answers. - For production persistence, replace the
storewith a database of your choice.