httputil depends on xerrors (Tier 0) and valid (Tier 1), placing it at Tier 2. No infrastructure or lifecycle dependencies exist in this module.
2.8 KiB
ADR-001: Generic Typed Handler Adapters
Status: Accepted Date: 2026-03-18
Context
Standard net/http handlers have the signature func(http.ResponseWriter, *http.Request).
Business logic that lives inside these handlers must manually decode JSON, validate
input, encode responses, and convert errors to status codes — the same boilerplate
repeated for every endpoint. This tightly couples business functions to HTTP and
makes them hard to test in isolation.
A common mitigation is a generic "controller" framework, but Go generics allow a lighter approach: thin adapter functions that perform the boilerplate at the HTTP boundary while leaving business logic free of HTTP types.
Decision
Three generic adapter functions are provided, covering the three common handler shapes:
| Function | Request body | Response body | Success status |
|---|---|---|---|
Handle[Req, Res] |
JSON-decoded Req |
JSON Res |
200 |
HandleNoBody[Res] |
none | JSON Res |
200 |
HandleEmpty[Req] |
JSON-decoded Req |
none | 204 |
Each adapter accepts a plain Go function — e.g.
func(ctx context.Context, req Req) (Res, error) — and returns an
http.HandlerFunc. The business function receives a context.Context (from the
request) and a typed value; it returns a typed value and an error. It has no
knowledge of http.ResponseWriter or *http.Request.
Handle and HandleEmpty also accept a valid.Validator parameter. After JSON
decoding, the adapter calls v.Struct(req) before invoking the business function.
Validation errors are returned to the caller via Error(w, err) without reaching
business logic.
A HandlerFunc type (func(http.ResponseWriter, *http.Request) error) is also
provided for handlers that need direct HTTP access but still want automatic error
mapping.
Consequences
- Business functions are pure Go — they take typed inputs and return typed outputs. They can be called directly in unit tests without constructing an HTTP request.
- Type parameters are inferred at the call site:
Handle(v, svc.CreateOrder)— no explicit type arguments needed if the function signature is concrete. - Validation is enforced before business logic runs. There is no path through
HandleorHandleEmptywhere an invalidReqreaches the function. HandleNoBodyskips validation entirely because there is no request body to validate. Path/query parameters are the caller's responsibility.- All three adapters share the same error mapping via
Error(w, err), so HTTP status codes are determined consistently by the xerrors code on the returned error. - The adapters impose a fixed response shape (JSON + fixed status). Handlers that
need streaming, multipart, or redirect responses should use
HandlerFuncor plainhttp.HandlerFuncdirectly.