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.
|