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.
This commit is contained in:
2026-03-19 13:09:32 +00:00
commit 285293a75b
14 changed files with 896 additions and 0 deletions

110
CLAUDE.md Normal file
View File

@@ -0,0 +1,110 @@
# 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.