Designing an API (Backend / Producing)
Conventions for building Go Fiber HTTP services that produce the shared API contract.
Core Principle
An API is a contract, not an implementation detail. Every shape, every status code, and every error code is a promise to consumers. Break these intentionally and explicitly — never by accident.
Route Design
GET /api/{resource} # list (paginated)GET /api/{resource}/{id} # get by IDPOST /api/{resource} # createPUT /api/{resource}/{id} # full replacePATCH /api/{resource}/{id} # partial updateDELETE /api/{resource}/{id} # delete- Routes are plural nouns:
/api/users,/api/invoices. - Route parameters are opaque IDs:
/api/users/user_01HW.... - Query parameters for filtering:
/api/users?role=admin&status=active. - Query parameters for pagination:
?page=2&page_size=20.
Required Response Shapes
→ See The Shared Contract for the exact shapes.
| Situation | Shape |
|---|---|
| Success — list | { data: T[], pagination: { ... } } |
| Success — single | T (the resource object) |
| Error — any | { error: { code, message, details?, request_id } } |
Never return 200 with an error body.
Error Codes
Use the standard error codes from The Shared Contract — never invent new codes without adding them to that page.
Go / Fiber Implementation
func (h *UserHandler) List(c *fiber.Ctx) error { ctx := c.Context() log := zerolog.Ctx(ctx)
page := c.QueryInt("page", 1) pageSize := c.QueryInt("page_size", 20)
users, total, err := h.service.List(ctx, page, pageSize) if err != nil { log.Error().Err(err).Msg("failed to list users") return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": fiber.Map{ "code": "server.internal", "message": "An unexpected error occurred.", "request_id": requestid.Get(c), }, }) }
log.Info(). Str("event", "http.request_completed"). Int("status_code", 200). Int64("duration_ms", time.Since(start).Milliseconds()). Msg("request completed")
return c.JSON(fiber.Map{ "data": users, "pagination": fiber.Map{ "page": page, "page_size": pageSize, "total": total, "total_pages": (total + int64(pageSize) - 1) / int64(pageSize), }, })}Idempotency (Required on Mutations)
func IdempotencyMiddleware(store IdempotencyStore) fiber.Handler { return func(c *fiber.Ctx) error { key := c.Get("Idempotency-Key") if key == "" { return c.Next() }
if cached, ok := store.Get(key); ok { return c.Status(cached.Status).JSON(cached.Body) }
if err := c.Next(); err != nil { return err }
store.Set(key, IdempotencyRecord{ Status: c.Response().StatusCode(), Body: c.Response().Body(), }, 24*time.Hour) return nil }}Required Logging
Every request handler must emit a wide-event log on completion. See Request logging (zerolog) for the canonical middleware.
Required fields per completion event: event: "http.request_completed", request_id, status_code, duration_ms, route, method.