httputil depends on xerrors (Tier 0) and valid (Tier 1), placing it at Tier 2. No infrastructure or lifecycle dependencies exist in this module.
111 lines
4.2 KiB
Markdown
111 lines
4.2 KiB
Markdown
# 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.
|