• v0.9.0 285293a75b

    Rene Nochebuena released this 2026-03-19 07:10:22 -06:00 | 0 commits to main since this release

    v0.9.0

    code.nochebuena.dev/go/httputil

    Overview

    httputil removes HTTP boilerplate from business logic. Generic adapter functions wrap
    pure Go functions into http.HandlerFunc values, handling JSON decode, struct validation,
    JSON encode, and error-to-HTTP-status mapping automatically. A single Error helper
    translates any *xerrors.Err to the correct HTTP status and JSON body.

    This is the initial stable release. The API has been designed through multiple architecture
    reviews and validated end-to-end via the todo-api POC. It is versioned v0.9.0 rather than
    v1.0.0 because it has not yet been exercised across all production edge cases, and minor
    API refinements may follow.

    What's Included

    Handler adapters (generic):

    • Handle[Req, Res any](v Validator, fn func(ctx, Req) (Res, error)) http.HandlerFunc
      — decodes JSON body, validates, calls fn, encodes result as JSON 200
    • HandleNoBody[Res any](fn func(ctx) (Res, error)) http.HandlerFunc
      — no body decode or validation; encodes result as JSON 200 (for GET/DELETE)
    • HandleEmpty[Req any](v Validator, fn func(ctx, Req) error) http.HandlerFunc
      — decodes JSON body, validates, calls fn, returns 204 on success

    Manual handler type:

    • HandlerFunc func(w, r) error — implements http.Handler; on non-nil return, calls Error(w, err)

    Response helpers:

    • JSON(w, status, v) — encodes v as JSON, sets Content-Type: application/json
    • NoContent(w) — writes 204 No Content
    • Error(w, err) — maps *xerrors.Err codes to HTTP status codes; falls back to 500 for
      unknown errors; always writes {"code": "...", "message": "..."} JSON body

    xerrors.Code → HTTP status mapping (12 codes):

    Code Status
    ErrInvalidInput 400 Bad Request
    ErrUnauthorized 401 Unauthorized
    ErrPermissionDenied 403 Forbidden
    ErrNotFound 404 Not Found
    ErrAlreadyExists 409 Conflict
    ErrGone 410 Gone
    ErrPreconditionFailed 412 Precondition Failed
    ErrRateLimited 429 Too Many Requests
    ErrInternal 500 Internal Server Error
    ErrNotImplemented 501 Not Implemented
    ErrUnavailable 503 Service Unavailable
    ErrDeadlineExceeded 504 Gateway Timeout

    Installation

    go get code.nochebuena.dev/go/httputil@v0.9.0
    
    import "code.nochebuena.dev/go/httputil"
    
    // Typed handler with request and response
    r.Post("/items", httputil.Handle(validator, svc.CreateItem))
    
    // Read-only handler (no request body)
    r.Get("/items/{id}", httputil.HandleNoBody(func(ctx context.Context) (Item, error) {
        return svc.GetItem(ctx, chi.URLParamFromCtx(ctx, "id"))
    }))
    
    // Write-only handler (no response body)
    r.Delete("/items/{id}", httputil.HandleEmpty(validator, svc.DeleteItem))
    

    Design Highlights

    Business functions are pure Go. Functions passed to Handle, HandleNoBody, and
    HandleEmpty have no http.ResponseWriter or *http.Request in their signature. They
    can be called directly in unit tests with context.Background() and a typed request value.

    Single error translation point. Error(w, err) is the only place where
    xerrors.Code values become HTTP status codes. All handler adapters route through it.
    Custom error-to-status mappings elsewhere would fragment the contract.

    Validation is injected. Handle and HandleEmpty accept a valid.Validator and
    run validation before the business function is called. An invalid request never reaches
    business logic.

    HandlerFunc for manual handlers. When a handler needs direct access to
    http.ResponseWriter or *http.Request (e.g. reading path parameters, setting custom
    headers), use HandlerFunc. It still routes errors through Error.

    Known Limitations & Edge Cases

    • Only JSON request body decoding is supported. Form data, multipart, and other content
      types are not handled by the adapters.
    • No streaming response support. No server-sent events.
    • No content-type negotiation. Responses are always application/json (or empty for 204).
    • HandleNoBody performs no query parameter or path variable 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 in the JSON error body and will shadow the error code and message.
    • httputil is a transport-layer package. Do not import it from business or service layers.

    v0.9.0 → v1.0.0 Roadmap

    • Validate error mapping completeness against all xerrors.Code values added after v0.9.0.
    • Evaluate adding a HandleStream variant for chunked/streaming JSON responses.
    • Consider adding a Content-Type request check in Handle and HandleEmpty to return
      415 Unsupported Media Type for non-JSON bodies.
    • Confirm behaviour with very large request bodies (no size limit is currently enforced).
    Downloads