httputil depends on xerrors (Tier 0) and valid (Tier 1), placing it at Tier 2. No infrastructure or lifecycle dependencies exist in this module.
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.ResponseWriteror*http.Requestin their signature. - xerrors → HTTP status mapping (ADR-002):
Error(w, err)is the single translation point. Twelvexerrors.Codevalues map to specific HTTP statuses. Unknown errors become 500. The JSON body always contains"code"and"message". valid.Validatoris injected intoHandleandHandleEmpty. Validation runs before the business function is called; an invalid request never reaches business logic.HandlerFuncfor manual handlers: Afunc(w, r) errortype that implementshttp.Handler. Use it when a handler needs direct HTTP access but still wants automatic error mapping viaError.
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 ofhttp.ResponseWriter,*http.Request, andnet/httpimports. - Do not define your own error-to-status mapping alongside
Error. All error translation must go throughError; custom mappings fragment the status code contract. - Do not use
HandleNoBodyfor endpoints that need to validate query parameters or path variables — it skips validation. Read and validate those values inside the function or useHandlerFunc. - Do not add context fields to
*xerrors.Errwith keys"code"or"message"— those names are reserved by the JSON body format and will shadow the error code and message. - Do not import
httputilfrom 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
Handlecan be tested directly without HTTP. Call the function with a plaincontext.Background()and a typed request value. - To test the HTTP adapter itself, use
httptest.NewRecorder()and call the returnedhttp.HandlerFuncdirectly. httputil_test.gocovers JSON decode errors, validation errors, business errors (variousxerrors.Codevalues), and successful responses.- No mock or stub is needed for
valid.Validatorin tests — usevalid.New(valid.Options{})directly; it has no external side effects.