Echo: Middleware Ecosystem & Context Wrapper
Echo is a full HTTP framework centered on echo.Context, a middleware pipeline with Next(), and first-class support for binding, validation, and WebSockets.
It targets teams building JSON APIs and real-time endpoints who want framework ergonomics with a smaller surface than some alternatives.
Summary
Echo handlers have signature func(c echo.Context) error.
Returning an error delegates to HTTPErrorHandler, enabling consistent JSON error envelopes.
Middleware composes as echo.MiddlewareFunc, and the framework ships logging, recovery, CORS, and request ID middleware in its ecosystem.
Recipe
Quick-reference recipe card - copy-paste ready.
e := echo.New()
e.Use(middleware.Recover())
e.Use(middleware.RequestID())
e.POST("/users", func(c echo.Context) error {
var u user
if err := c.Bind(&u); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusCreated, u)
})When to reach for this:
- JSON APIs needing consistent error handling via returned
errorvalues - Services exposing WebSocket endpoints alongside REST routes
- Teams preferring explicit middleware
Next()over wrapper handlers only - Projects wanting built-in bind/validate without assembling libraries manually
Working Example
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type createUser struct {
Email string `json:"email" validate:"required,email"`
Name string `json:"name" validate:"required,min=2"`
}
type user struct {
ID int `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
}
func main() {
e := echo.New()
e.HideBanner = true
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Validator = &customValidator{}
api := e.Group("/api/v1")
api.GET("/health", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
})
api.POST("/users", func(c echo.Context) error {
var in createUser
if err := c.Bind(&in); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if err := c.Validate(&in); err != nil {
return err
}
return c.JSON(http.StatusCreated, user{ID: 1, Email: in.Email, Name: in.Name})
})
e.Start(":8080")
}
type customValidator struct{}
func (cv *customValidator) Validate(i any) error {
// Wire github.com/go-playground/validator in production
return nil
}What this demonstrates:
- Grouped API routes with shared middleware
Bindfor JSON bodies and separateValidatehook- Errors expressed as
errorwithecho.NewHTTPError - Custom validator interface implementation point
Deep Dive
How It Works
echo.New()creates an engine with router, binder, and validator slots.- Middleware functions wrap handlers;
c.Next()continues the chain until the route handler runs. echo.Contextembedshttp.ResponseWriterand*http.Requestaccessors (Request(),Response()).StartandStartTLSlaunchhttp.Serverwith Echo as handler.
Context Capabilities
| Method family | Purpose |
|---|---|
Param, QueryParam, FormValue | Read inputs |
Bind | Decode body/query/path into structs |
JSON, String, Blob, File | Write responses |
Redirect, NoContent | Control status and headers |
Echo, Path, RealIP | Framework metadata |
WebSockets
Echo provides echo.Context upgrade helpers (via golang.org/x/net/websocket patterns and middleware).
Use for notifications, chat, or streaming when HTTP long-polling is insufficient.
Keep connection lifecycle and auth at upgrade time explicit.
Go Notes
// Custom HTTPErrorHandler for problem+json style errors
e.HTTPErrorHandler = func(err error, c echo.Context) {
he, ok := err.(*echo.HTTPError)
if !ok {
he = &echo.HTTPError{Code: http.StatusInternalServerError, Message: http.StatusText(500)}
}
if !c.Response().Committed {
c.JSON(he.Code, map[string]any{"message": he.Message})
}
}Centralizing errors prevents leaking stack traces in production responses.
Gotchas
- Validator not wired by default -
c.Validateis a no-op untile.Validatoris set. Fix: registergo-playground/validatoradapter inmain. - Committed response on error - Writing to the response before returning an error skips
HTTPErrorHandler. Fix: return errors before writing bodies or checkc.Response().Committed. Bindmerges sources - Bind can populate from path, query, and body; unexpected overrides if tags overlap. Fix: useBindoptions or separate bind methods per source.- WebSocket auth - Cookies/headers must be validated during upgrade; middleware order matters. Fix: run auth middleware before upgrade route.
- Global middleware on wrong group - Accidentally registering admin routes on public groups leaks handlers. Fix: use
e.Groupwith per-group middleware for auth boundaries. - Framework error strings - Returning raw
err.Error()to clients leaks internals. Fix: map to stable external codes inHTTPErrorHandler.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Gin | Larger plugin ecosystem and similar JSON speed | You want Echo's error-return handler style |
| chi | stdlib http.Handler portability | You want built-in bind/validate/WebSocket helpers |
| net/http + gorilla/websocket | Minimal dependencies | You prefer integrated Echo middleware |
| Fiber | Non-stdlib fast HTTP | You need strict net/http handler interop |
FAQs
How is Echo different from Gin?
Both are JSON-oriented; Echo handlers return error for centralized handling and documents WebSocket patterns prominently.
Do I need both Bind and Validate?
Bind decodes data; Validate runs struct tags via the configured validator - call both for tag-based validation.
Can Echo use standard http.Handler middleware?
Use middleware adapters or wrap with echo.WrapHandler / WrapHandlerFunc for stdlib handlers.
How do I test Echo handlers?
Use httptest with e.ServeHTTP or echo.New().NewContext with recorded requests.
How does Echo handle trailing slashes?
Configure router RedirectTrailingSlash on the echo instance; align with client URL conventions.
What logger does Echo middleware use?
Default logger middleware writes Apache-style lines; swap for slog in custom middleware for structured logs.
Can I run Echo behind a reverse proxy?
Yes - use middleware.Proxy or trust X-Forwarded-For carefully with IPExtractor configuration.
How do I serve static assets?
e.Static or e.File register file routes; place caching headers in middleware for production CDN offload.
Does Echo support HTTP/2?
When using StartTLS with Go's TLS stack, HTTP/2 is negotiated automatically like stdlib servers.
How do I pass request-scoped values?
Use c.Set and c.Get for framework bag values; prefer c.Request().Context() for cancellation and tracing baggage.
Can Echo mount chi routes?
Mount chi routers with WrapHandler on a wildcard route prefix, minding path prefix stripping.
Is Echo suitable for microservices?
Yes for HTTP/JSON and WebSocket edges; keep service-to-service calls on gRPC when contracts are internal.
Related
- Gin: Fast JSON APIs & Binding - peer full framework comparison
- Go HTTP Frameworks: Routers vs Full Frameworks - context wrapper model
- Web Frameworks Basics - side-by-side router setup
- Framework Selection Decision Guide - Echo ranking by scenario
- Middleware & Decorator Patterns - middleware mental model
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).