rgoussu@goussu: ~/library/go/web-frameworks
~/library/go/web-frameworks cat echo.md

Echo deep dive

# Echo's Context interface, centralized HTTPErrorHandler, binding and validation hooks, middleware set with stdlib adapters, and how it compares to Gin.

Conceptsaved 2026-08-09 #go#web#frameworks#echo#middleware#error-handling

Overview

Echo occupies the same batteries-included niche as Gin — its own context type, radix-tree routing, binding, a broad first-party middleware set — but with two design choices that distinguish it: handlers return errors that flow to one centralized HTTPErrorHandler, and echo.Context is an interface, not a struct. The first is Echo's best idea and the main reason to pick it over Gin; the second makes decoration and test doubles cleaner. Like Gin, the framework is ServeHTTP-compatible at the outer edge while replacing stdlib idioms inside.

Key points

  • echo.Context is an interface bundling request, response, path/query params, binding, rendering, and a value store; being an interface, it can be wrapped in custom middleware (a common pattern for adding typed accessors per project).
  • Handlers return errorfunc(c echo.Context) error — instead of writing error responses inline. Business code stays on the happy path; failures become values.
  • Centralized HTTPErrorHandler: every returned error funnels into one function that maps it to a response. echo.NewHTTPError(404, "no such item") carries status + message; anything else defaults to 500. One place to shape the error envelope, log, and hide internals — Gin has no equivalent; error rendering there is per-handler discipline.
  • Binding & validation hooks: c.Bind(&in) populates from JSON/XML/form/query/path; validation is pluggable — set e.Validator to anything implementing Validate(i any) error, conventionally a go-playground/validator wrapper. Explicitly a hook, not a bundled dependency.
  • Middleware: echo.MiddlewareFunc chains around HandlerFuncs; the first-party set covers Logger, Recover, CORS, JWT, RateLimiter, Gzip, BodyLimit, Secure headers.
  • Stdlib-wrap adapters ship in the box: echo.WrapHandler(http.Handler) and echo.WrapMiddleware(func(http.Handler) http.Handler) pull stdlib-shaped code into Echo's chain — friendlier to the http.Handler ecosystem than Gin, though the wrapping still costs the stdlib code access to Echo's context store.
  • Route groups: g := e.Group("/admin", authMW) scope prefix and middleware; routes are named and reversible (e.Reverse) for URL generation.
  • Vs Gin: near-identical performance and feature envelope; choose Echo for the error model and interface-based context, Gin for the larger community and middleware ecosystem. Migrating between them is mechanical but touches every handler signature.

Details

The error-handling pattern in practice

e := echo.New()
e.HTTPErrorHandler = func(err error, c echo.Context) {
    code, msg := http.StatusInternalServerError, "internal error"
    var he *echo.HTTPError
    if errors.As(err, &he) {
        code, msg = he.Code, fmt.Sprint(he.Message)
    }
    if !c.Response().Committed {
        _ = c.JSON(code, map[string]string{"error": msg})
    }
    slog.Error("request failed", "path", c.Path(), "err", err)
}

Domain errors translate once: an errors.Is(err, storage.ErrNotFound) branch in the error handler replaces if err != nil { c.JSON(404, ...) } scattered through handlers. This is the pattern teams bolt onto Gin by hand (middleware inspecting c.Errors); in Echo it is the paved road.

Binding + validation wiring

type Validator struct{ v *validator.Validate }

func (cv *Validator) Validate(i any) error { return cv.v.Struct(i) }

e.Validator = &Validator{v: validator.New()}

e.POST("/items", func(c echo.Context) error {
    var in CreateItem
    if err := c.Bind(&in); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }
    if err := c.Validate(&in); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }
    return c.JSON(http.StatusCreated, in)
})

Related