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.
Search across all documentation pages
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.
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.
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:
error valuesNext() over wrapper handlers onlypackage 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:
Bind for JSON bodies and separate Validate hookerror with echo.NewHTTPErrorecho.New() creates an engine with router, binder, and validator slots.c.Next() continues the chain until the route handler runs.echo.Context embeds http.ResponseWriter and *http.Request accessors (Request(), Response()).Start and StartTLS launch http.Server with Echo as handler.| 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 |
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.
// 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.
c.Validate is a no-op until e.Validator is set. Fix: register go-playground/validator adapter in main.HTTPErrorHandler. Fix: return errors before writing bodies or check c.Response().Committed.Bind merges sources - Bind can populate from path, query, and body; unexpected overrides if tags overlap. Fix: use Bind options or separate bind methods per source.e.Group with per-group middleware for auth boundaries.err.Error() to clients leaks internals. Fix: map to stable external codes in HTTPErrorHandler.| 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 |
Both are JSON-oriented; Echo handlers return error for centralized handling and documents WebSocket patterns prominently.
Bind decodes data; Validate runs struct tags via the configured validator - call both for tag-based validation.
Use middleware adapters or wrap with echo.WrapHandler / WrapHandlerFunc for stdlib handlers.
Use httptest with e.ServeHTTP or echo.New().NewContext with recorded requests.
Configure router RedirectTrailingSlash on the echo instance; align with client URL conventions.
Default logger middleware writes Apache-style lines; swap for slog in custom middleware for structured logs.
Yes - use middleware.Proxy or trust X-Forwarded-For carefully with IPExtractor configuration.
e.Static or e.File register file routes; place caching headers in middleware for production CDN offload.
When using StartTLS with Go's TLS stack, HTTP/2 is negotiated automatically like stdlib servers.
Use c.Set and c.Get for framework bag values; prefer c.Request().Context() for cancellation and tracing baggage.
Mount chi routers with WrapHandler on a wildcard route prefix, minding path prefix stripping.
Yes for HTTP/JSON and WebSocket edges; keep service-to-service calls on gRPC when contracts are internal.
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).
Reviewed by Chris St. John·Last updated Jul 16, 2026