Documentation
¶
Overview ¶
Package httputil provides typed handler adapters and HTTP response helpers.
Handler adapters eliminate HTTP boilerplate — business functions stay pure Go with no knowledge of request parsing or response encoding. All errors flow through a centralized Error handler that logs once at the correct level and writes a standardized JSON response body.
Typed handler adapters ¶
type CreateUserReq struct {
Email string `json:"email" validate:"required,email"`
}
type CreateUserRes struct {
ID string `json:"id"`
}
r.Post("/users", httputil.Handle(v, logger, func(ctx context.Context, req CreateUserReq) (CreateUserRes, error) {
u, err := svc.CreateUser(ctx, req.Email)
if err != nil {
return CreateUserRes{}, err // propagates to Error — logged once, correct HTTP status
}
return CreateUserRes{ID: u.ID}, nil
}))
Request binding ¶
Bind and BindEmpty extend the same decode → validate → call → encode pipeline to requests that carry more than a JSON body. Each field declares its source with a struct tag — path:, query: or json: — and the assembled struct is validated once:
type getRoleReq struct {
RoleID uuid.UUID `path:"roleID" validate:"required"`
Expand []string `query:"expand"`
}
r.Get("/roles/{roleID}", httputil.Bind(v, logger, func(ctx context.Context, req getRoleReq) (RoleRes, error) {
return svc.GetRole(ctx, req.RoleID)
}))
A malformed value is a 400 naming the parameter (never a 500), uuid.UUID and time.Time bind via encoding.TextUnmarshaler, and a mis-tagged struct fails at wiring rather than on a request. Use these instead of HandlerFunc for any route with an identifier or a filter.
Centralized error handler ¶
Error is the single point of error processing for all handlers:
- 5xx → Error level (logz auto-enriches with error_code and WithContext fields)
- 4xx → Warn level (client mistake — not a server failure)
- 499 → Info level (client cancelled the request intentionally)
Call it directly from HandlerFunc for genuinely custom responses — streaming, file downloads, non-JSON content types:
r.Get("/reports/{id}.csv", httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
rows, err := svc.Export(r.Context(), chi.URLParam(r, "id"))
if err != nil {
httputil.Error(logger, w, r, err)
return nil
}
w.Header().Set("Content-Type", "text/csv")
return csv.NewWriter(w).WriteAll(rows)
}).ServeHTTP)
Index ¶
- func Bind[Req, Res any](v valid.Validator, logger logging.Logger, ...) http.HandlerFunc
- func BindEmpty[Req any](v valid.Validator, logger logging.Logger, ...) http.HandlerFunc
- func Error(logger logging.Logger, w http.ResponseWriter, r *http.Request, err error)
- func Handle[Req, Res any](v valid.Validator, logger logging.Logger, ...) http.HandlerFunc
- func HandleEmpty[Req any](v valid.Validator, logger logging.Logger, ...) http.HandlerFunc
- func HandleNoBody[Res any](logger logging.Logger, fn func(ctx context.Context) (Res, error), ...) http.HandlerFunc
- func JSON(w http.ResponseWriter, status int, v any)
- func NoContent(w http.ResponseWriter)
- type HandlerFunc
- type Option
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Bind ¶ added in v1.6.0
func Bind[Req, Res any](v valid.Validator, logger logging.Logger, fn func(ctx context.Context, req Req) (Res, error), opts ...Option) http.HandlerFunc
Bind adapts a typed business function whose request is assembled from more than the JSON body. Each field of Req declares its source with a struct tag:
type updateRoleRequest struct {
RoleID uuid.UUID `path:"roleID" validate:"required"`
Page int `query:"page" default:"1" validate:"min=1"`
Name string `json:"name" validate:"omitempty,max=200"`
}
- path: fills from a chi route parameter (r.Context()).
- query: fills from the URL query string; a repeated parameter binds to a slice.
- json: fills from the JSON body (standard encoding/json).
It then validates the assembled struct once with v and calls fn — the handler signature is identical to Handle. On success Res is encoded as JSON (200 by default, or the WithStatus code). On error it flows through Error.
Conversion covers string, the sized integer/unsigned/float types, bool, and any type whose pointer implements encoding.TextUnmarshaler (so uuid.UUID and time.Time bind with no special-casing). A value that fails to convert is reported as xerrors.ErrInvalidInput naming the parameter — a 400, never a 500.
The struct is reflected over once per type at wiring time and the result cached. A field declaring more than one source tag, an unsupported field type, or a default: that is not a valid value for its field all panic at wiring, so a mis-tagged struct fails the service at boot rather than on a request.
func BindEmpty ¶ added in v1.6.0
func BindEmpty[Req any](v valid.Validator, logger logging.Logger, fn func(ctx context.Context, req Req) error, opts ...Option) http.HandlerFunc
BindEmpty is Bind for a function that returns no response body. Req is filled from path, query and body exactly as in Bind; on success a body-less status is written (204 by default, or the WithStatus code).
Unlike HandleEmpty it does not require a request body: a bodiless request (GET, DELETE, Content-Length: 0) is not an error, so a DELETE /resource/{id} with a path: tag binds directly instead of failing on io.EOF.
func Error ¶
Error is the centralized error handler. It logs at the appropriate level and writes a standardized JSON error body.
Log level is derived from the HTTP status:
- 5xx → Error (unexpected server failure; logz auto-enriches with error_code and context fields)
- 4xx → Warn (client mistake — not a server failure)
- 499 → Info (client cancelled the request intentionally)
The response body always contains code and message; platform_code and context fields attached via xerrors.Err.WithContext are included when present.
func Handle ¶
func Handle[Req, Res any](v valid.Validator, logger logging.Logger, fn func(ctx context.Context, req Req) (Res, error), opts ...Option) http.HandlerFunc
Handle adapts a typed business function to http.HandlerFunc.
- Decodes the JSON request body into Req.
- Validates Req using the provided valid.Validator.
- Calls fn with the request context and decoded Req.
- Encodes Res as JSON on success — HTTP 200 by default, or the code given via WithStatus (e.g. WithStatus(http.StatusCreated) for a resource-creating POST).
- On error: logs via Error (level derived from HTTP status) and writes the standardized JSON body.
func HandleEmpty ¶
func HandleEmpty[Req any](v valid.Validator, logger logging.Logger, fn func(ctx context.Context, req Req) error, opts ...Option) http.HandlerFunc
HandleEmpty adapts a typed function with a request body but no response body. Decodes and validates Req, calls fn, and writes a body-less success — 204 No Content by default, or the code given via WithStatus (e.g. WithStatus(http.StatusAccepted) for async processing). On error: logs via Error and writes the standardized JSON body.
func HandleNoBody ¶
func HandleNoBody[Res any](logger logging.Logger, fn func(ctx context.Context) (Res, error), opts ...Option) http.HandlerFunc
HandleNoBody adapts a typed function with no request body (GET, HEAD). Calls fn with the request context; encodes the result as JSON — HTTP 200 by default, or the code given via WithStatus. On error: logs via Error and writes the standardized JSON body.
Types ¶
type HandlerFunc ¶
type HandlerFunc func(w http.ResponseWriter, r *http.Request) error
HandlerFunc is an http.Handler that returns an error. On non-nil error the error is mapped to the appropriate HTTP response via Error.
Use it for genuinely custom responses — streaming, file downloads, non-JSON content types — where the typed adapters do not fit. It is no longer the answer for path or query parameters: Bind and BindEmpty fill those from struct tags with the same decode → validate → encode guarantees, and a custom success status is set with WithStatus. Reaching for HandlerFunc to read a parameter is the one path by which a handler reaches production without validation running.
func (HandlerFunc) ServeHTTP ¶
func (h HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP implements http.Handler. Errors are written as standardized JSON without logging — no logger is in scope for a bare function type. Use Handle, HandleNoBody, or HandleEmpty for centralized logging, or call Error explicitly.
type Option ¶ added in v1.5.0
type Option func(*options)
Option configures a Handle* adapter. With no options each adapter writes its default success status (200 for the body-returning adapters, 204 for HandleEmpty). Options are applied once at wiring time, not per request.
func WithStatus ¶ added in v1.5.0
WithStatus overrides the success status an adapter writes — e.g. WithStatus(http.StatusCreated) for a POST that creates a resource, or WithStatus(http.StatusAccepted) for an async HandleEmpty.
It exists because the Handle* adapters own only the happy path: they always write a success response, so the status is theirs to set, while error statuses are derived separately from the returned xerror by Error. The code must therefore be 2xx — anything else is a routing mistake, since an error status never belongs on the success path. WithStatus panics on a non-2xx code, and because routes are wired at startup that panic surfaces at boot: the service fails to start rather than emitting a wrong status at request time. (mw.Recover guards requests, so it does not catch a wiring-time panic — which is the point.)