Files
httputil/CLAUDE.md
Rene Nochebuena 285293a75b docs(httputil): correct tier from 3 to 2
httputil depends on xerrors (Tier 0) and valid (Tier 1), placing it at
Tier 2. No infrastructure or lifecycle dependencies exist in this module.
2026-03-19 13:09:32 +00:00

4.2 KiB

httputil

Typed HTTP handler adapters and response helpers for net/http.

Purpose

httputil removes HTTP boilerplate from business logic. Generic adapter functions (Handle, HandleNoBody, HandleEmpty) wrap pure Go functions into http.HandlerFunc values, handling JSON decode, struct validation, JSON encode, and error-to-status mapping automatically. A single Error helper translates any *xerrors.Err to the correct HTTP status and JSON body.

Tier & Dependencies

Tier: 2 (transport layer; depends on Tier 0 xerrors and Tier 1 valid) Module: code.nochebuena.dev/go/httputil Direct imports: code.nochebuena.dev/go/xerrors, code.nochebuena.dev/go/valid

httputil has no logger dependency. It does not import logz, launcher, or any infrastructure module.

Key Design Decisions

  • Generic typed handlers (ADR-001): Three adapter functions cover the common handler shapes. Business functions are pure Go — no http.ResponseWriter or *http.Request in their signature.
  • xerrors → HTTP status mapping (ADR-002): Error(w, err) is the single translation point. Twelve xerrors.Code values map to specific HTTP statuses. Unknown errors become 500. The JSON body always contains "code" and "message".
  • valid.Validator is injected into Handle and HandleEmpty. Validation runs before the business function is called; an invalid request never reaches business logic.
  • HandlerFunc for manual handlers: A func(w, r) error type that implements http.Handler. Use it when a handler needs direct HTTP access but still wants automatic error mapping via Error.

Patterns

Typed handler with request and response:

r.Post("/orders", httputil.Handle(validator, svc.CreateOrder))
// svc.CreateOrder has signature: func(ctx context.Context, req CreateOrderRequest) (Order, error)

Read-only handler (no request body):

r.Get("/orders/{id}", httputil.HandleNoBody(func(ctx context.Context) (Order, error) {
    id := chi.URLParam(r, "id") // r captured from outer scope, or use closure
    return svc.GetOrder(ctx, id)
}))

Write-only handler (no response body):

r.Delete("/orders/{id}", httputil.HandleEmpty(validator, svc.CancelOrder))
// Returns 204 on success

Manual handler:

r.Get("/export", httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
    data, err := svc.Export(r.Context())
    if err != nil {
        return err  // mapped by Error()
    }
    w.Header().Set("Content-Type", "text/csv")
    _, err = w.Write(data)
    return err
}))

Writing responses directly:

httputil.JSON(w, http.StatusCreated, result)
httputil.NoContent(w)
httputil.Error(w, xerrors.NotFound("order %s not found", id))

What to Avoid

  • Do not put HTTP-specific logic in business functions passed to Handle. Keep them free of http.ResponseWriter, *http.Request, and net/http imports.
  • Do not define your own error-to-status mapping alongside Error. All error translation must go through Error; custom mappings fragment the status code contract.
  • Do not use HandleNoBody for endpoints that need to validate query parameters or path variables — it skips validation. Read and validate those values inside the function or use HandlerFunc.
  • Do not add context fields to *xerrors.Err with keys "code" or "message" — those names are reserved by the JSON body format and will shadow the error code and message.
  • Do not import httputil from business/service layers. It is a transport-layer package; the dependency should flow inward only (handlers → services, not the reverse).

Testing Notes

  • Business functions wrapped by Handle can be tested directly without HTTP. Call the function with a plain context.Background() and a typed request value.
  • To test the HTTP adapter itself, use httptest.NewRecorder() and call the returned http.HandlerFunc directly.
  • httputil_test.go covers JSON decode errors, validation errors, business errors (various xerrors.Code values), and successful responses.
  • No mock or stub is needed for valid.Validator in tests — use valid.New(valid.Options{}) directly; it has no external side effects.