59 lines
2.8 KiB
Markdown
59 lines
2.8 KiB
Markdown
|
|
# ADR-001: Generic Typed Handler Adapters
|
||
|
|
|
||
|
|
**Status:** Accepted
|
||
|
|
**Date:** 2026-03-18
|
||
|
|
|
||
|
|
## Context
|
||
|
|
|
||
|
|
Standard `net/http` handlers have the signature `func(http.ResponseWriter, *http.Request)`.
|
||
|
|
Business logic that lives inside these handlers must manually decode JSON, validate
|
||
|
|
input, encode responses, and convert errors to status codes — the same boilerplate
|
||
|
|
repeated for every endpoint. This tightly couples business functions to HTTP and
|
||
|
|
makes them hard to test in isolation.
|
||
|
|
|
||
|
|
A common mitigation is a generic "controller" framework, but Go generics allow a
|
||
|
|
lighter approach: thin adapter functions that perform the boilerplate at the HTTP
|
||
|
|
boundary while leaving business logic free of HTTP types.
|
||
|
|
|
||
|
|
## Decision
|
||
|
|
|
||
|
|
Three generic adapter functions are provided, covering the three common handler
|
||
|
|
shapes:
|
||
|
|
|
||
|
|
| Function | Request body | Response body | Success status |
|
||
|
|
|---|---|---|---|
|
||
|
|
| `Handle[Req, Res]` | JSON-decoded `Req` | JSON `Res` | 200 |
|
||
|
|
| `HandleNoBody[Res]` | none | JSON `Res` | 200 |
|
||
|
|
| `HandleEmpty[Req]` | JSON-decoded `Req` | none | 204 |
|
||
|
|
|
||
|
|
Each adapter accepts a plain Go function — e.g.
|
||
|
|
`func(ctx context.Context, req Req) (Res, error)` — and returns an
|
||
|
|
`http.HandlerFunc`. The business function receives a `context.Context` (from the
|
||
|
|
request) and a typed value; it returns a typed value and an error. It has no
|
||
|
|
knowledge of `http.ResponseWriter` or `*http.Request`.
|
||
|
|
|
||
|
|
`Handle` and `HandleEmpty` also accept a `valid.Validator` parameter. After JSON
|
||
|
|
decoding, the adapter calls `v.Struct(req)` before invoking the business function.
|
||
|
|
Validation errors are returned to the caller via `Error(w, err)` without reaching
|
||
|
|
business logic.
|
||
|
|
|
||
|
|
A `HandlerFunc` type (`func(http.ResponseWriter, *http.Request) error`) is also
|
||
|
|
provided for handlers that need direct HTTP access but still want automatic error
|
||
|
|
mapping.
|
||
|
|
|
||
|
|
## Consequences
|
||
|
|
|
||
|
|
- Business functions are pure Go — they take typed inputs and return typed outputs.
|
||
|
|
They can be called directly in unit tests without constructing an HTTP request.
|
||
|
|
- Type parameters are inferred at the call site: `Handle(v, svc.CreateOrder)` — no
|
||
|
|
explicit type arguments needed if the function signature is concrete.
|
||
|
|
- Validation is enforced before business logic runs. There is no path through
|
||
|
|
`Handle` or `HandleEmpty` where an invalid `Req` reaches the function.
|
||
|
|
- `HandleNoBody` skips validation entirely because there is no request body to
|
||
|
|
validate. Path/query parameters are the caller's responsibility.
|
||
|
|
- All three adapters share the same error mapping via `Error(w, err)`, so HTTP
|
||
|
|
status codes are determined consistently by the xerrors code on the returned error.
|
||
|
|
- The adapters impose a fixed response shape (JSON + fixed status). Handlers that
|
||
|
|
need streaming, multipart, or redirect responses should use `HandlerFunc` or plain
|
||
|
|
`http.HandlerFunc` directly.
|