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:
110
CLAUDE.md
Normal file
110
CLAUDE.md
Normal 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.
|
||||
Reference in New Issue
Block a user