ADR-0008: Automatic validation middleware via Echo context wrapping

Status

Accepted

Tags

http, echo, validation, middleware, handlers

Decision

Use an AutoValidate() Echo middleware that wraps the context and overrides Bind() to call Validate() automatically. Handlers call c.Bind(&req) once — parsing and validation both happen. No manual c.Validate() calls in handlers.

Why

Calling c.Bind() then c.Validate() in every handler is repetitive and easy to forget. Missing validation silently allows malformed input through to business logic. A middleware wrapper intercepts Bind() at the framework level — validation is guaranteed for every bound request without touching individual handlers.

How it works

Request → AutoValidate() middleware wraps echo.Context

Handler calls c.Bind(&req)

validationContext.Bind() runs:
  1. vc.Context.Bind(i)      // parse JSON into struct
  2. vc.Context.Validate(i)  // validate struct tags automatically

Handler receives validated struct or early 400 error

Exceptions — skip AutoValidate for

  • File uploads (multipart/form-data) — Bind semantics differ
  • Webhooks with custom body parsing
  • Routes that intentionally accept partial/unvalidated input

Rules for agents

  • Never add c.Validate(&req) after c.Bind(&req) in handlers — the middleware handles it
  • Register AutoValidate() globally in main.go after echo.Validator is set
  • For file upload handlers, skip binding through c.Bind() and handle multipart directly
  • The middleware must be registered after e.Validator = validator.New() — middleware reads the validator from the context

Bad pattern (do not generate)

// Manual validation in every handler — repetitive and forgettable
func (h *Handler) Create(c echo.Context) error {
    var req CreateRequest
    if err := c.Bind(&req); err != nil { return err }
    if err := c.Validate(&req); err != nil { return err } // redundant
    // ...
}

Good pattern

// main.go — register once
e.Validator = validator.New()
e.Use(customMiddleware.AutoValidate())

// handler — c.Bind() validates automatically
func (h *Handler) Create(c echo.Context) error {
    var req CreateRequest
    if err := c.Bind(&req); err != nil { return err } // validates too
    // ...
}