# 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:** ```go 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):** ```go 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):** ```go r.Delete("/orders/{id}", httputil.HandleEmpty(validator, svc.CancelOrder)) // Returns 204 on success ``` **Manual handler:** ```go 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:** ```go 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.