Skip to content

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 ID
POST /api/{resource} # create
PUT /api/{resource}/{id} # full replace
PATCH /api/{resource}/{id} # partial update
DELETE /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.

SituationShape
Success — list{ data: T[], pagination: { ... } }
Success — singleT (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

handler/user_handler.go
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)

middleware/idempotency.go
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.