Files
httputil/docs/adr/ADR-001-generic-typed-handlers.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

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 Handle or HandleEmpty where an invalid Req reaches the function.
  • HandleNoBody skips 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 HandlerFunc or plain http.HandlerFunc directly.