Echo — API Surface#
API types#
Library (primary) + embedded Plugin/Middleware extension system
Echo is a pure library — it has no standalone binary, no gRPC services, and no CLI. Consumers import it as a Go module and register routes, middleware, and handlers entirely in code. The API surface has three distinct layers:
- Route registration API — how handlers are attached to paths
- Context API — how handlers read requests and write responses
- Middleware API — how cross-cutting concerns are wired in
Library API#
Public packages#
| Package | Purpose |
|---|---|
github.com/labstack/echo/v5 | Core framework: Echo, Context, Router, Group, StartConfig |
github.com/labstack/echo/v5/middleware | 24 production-ready middleware implementations |
github.com/labstack/echo/v5/echotest | Test helpers: ContextConfig, ToContext, ServeWithHandler |
API style#
Fluent method chaining on Echo and Group structs. Route registration calls return RouteInfo, enabling introspection. Configuration uses a plain struct (Config / per-middleware *Config) rather than functional options — one construction-time struct, zero setters.
Bootstrap API#
// Minimal
e := echo.New()
// With config (config struct overrides defaults selectively)
e := echo.NewWithConfig(echo.Config{
Router: myRouter, // Router interface — swap radix tree
Binder: myBinder, // Binder interface
Renderer: myRenderer, // Renderer interface
Validator: myValidator, // Validator interface
JSONSerializer: mySerializer, // JSONSerializer interface
IPExtractor: echo.ExtractIPFromXFFHeader(),
Logger: slog.New(...),
HTTPErrorHandler: echo.DefaultHTTPErrorHandler(true),
FormParseMaxMemory: 32 << 20,
})New() fills all defaults; NewWithConfig overrides only non-nil fields.
Route registration API#
Routes are registered via HTTP-method-named methods on *Echo and *Group:
// All return RouteInfo for introspection
e.GET(path, handler, middleware...)
e.POST(path, handler, middleware...)
e.PUT(path, handler, middleware...)
e.DELETE(path, handler, middleware...)
e.PATCH(path, handler, middleware...)
e.HEAD(path, handler, middleware...)
e.OPTIONS(path, handler, middleware...)
e.CONNECT(path, handler, middleware...)
e.TRACE(path, handler, middleware...)
// Convenience variants
e.Any(path, handler, middleware...) // registers for all methods
e.Match(methods, path, handler, middleware...) // subset of methods → Routes
e.RouteNotFound(path, handler, middleware...) // catch-all for unmatched routes
e.Add(method, path, handler, middleware...) // generic add
e.AddRoute(route Route) (RouteInfo, error) // low-level, error-returning
// Static file serving
e.Static(pathPrefix, fsRoot, middleware...)
e.StaticFS(pathPrefix, filesystem, middleware...)
e.File(path, file, middleware...)
e.FileFS(path, file, filesystem, middleware...)Route path syntax:
- Static:
/users - Named param:
/users/:id - Wildcard:
/static/*or/files/*filepath
Route grouping:
api := e.Group("/api/v1", authMiddleware) // prefix + optional middleware
v2 := api.Group("/v2") // nesting supported
api.GET("/users/:id", getUserHandler)Middleware API#
Two middleware attachment points:
// Pre-middleware: runs BEFORE routing (affects URL visible to router)
e.Pre(middleware.RemoveTrailingSlash())
e.Pre(middleware.Rewrite(map[string]string{"/old": "/new"}))
// Regular middleware: runs AFTER routing (knows which route matched)
e.Use(middleware.Recover())
e.Use(middleware.RequestLogger())
e.Use(middleware.CORS("*"))
// Route-level middleware: passed inline at registration
e.GET("/admin", adminHandler, authMiddleware, rateLimitMiddleware)
// Group-level middleware: applied to all routes in the group
admin := e.Group("/admin", authMiddleware)Middleware function signature:
type MiddlewareFunc func(next HandlerFunc) HandlerFunc
type HandlerFunc func(c *Context) errorAdapter functions for stdlib compatibility:
echo.WrapHandler(h http.Handler) HandlerFunc // stdlib handler → echo handler
echo.WrapMiddleware(m func(http.Handler) http.Handler) MiddlewareFunc // stdlib middleware → echoBuilt-in middleware catalog (24 implementations)#
Each middleware follows the same dual-API pattern: Foo() for zero-config use, FooWithConfig(cfg FooConfig) for full control.
| Middleware | File | Purpose |
|---|---|---|
BasicAuth | basic_auth.go | HTTP Basic Auth with custom validator |
BodyDump | body_dump.go | Capture req/resp bodies for logging/debugging |
BodyLimit | body_limit.go | Enforce max request body size |
Gzip | compress.go | Gzip response compression |
ContextTimeout | context_timeout.go | Per-request deadline via context.WithTimeout |
CORS | cors.go | Cross-Origin Resource Sharing headers |
CSRF | csrf.go | CSRF token generation and validation |
Decompress | decompress.go | Decompress gzip-encoded request bodies |
KeyAuth | key_auth.go | API key authentication |
MethodOverride | method_override.go | HTTP method override from form/header/query |
Proxy | proxy.go | Reverse proxy with random/round-robin balancing |
RateLimiter | rate_limiter.go | Token-bucket rate limiting (in-memory store included) |
Recover | recover.go | Panic recovery with stack traces |
HTTPSRedirect | redirect.go | HTTP→HTTPS redirect (and www variants) |
RequestID | request_id.go | Inject unique request ID header |
RequestLogger | request_logger.go | Structured request/response logging |
Rewrite | rewrite.go | URL rewriting with regex rules |
Secure | secure.go | Security headers (XSS, HSTS, frame options, etc.) |
AddTrailingSlash | slash.go | Normalize trailing slashes |
RemoveTrailingSlash | slash.go | Normalize trailing slashes |
Static | static.go | Serve static files from a directory |
Context API (handler-facing)#
*Context is the primary interface for handlers. Key method groups:
Request reading:
c.Request() *http.Request
c.Param(name string) string // path parameter
c.ParamOr(name, default string) string
c.QueryParam(name string) string
c.QueryParamOr(name, default string) string
c.QueryParams() url.Values
c.QueryString() string
c.FormValue(name string) string
c.FormFile(name string) (*multipart.FileHeader, error)
c.MultipartForm() (*multipart.Form, error)
c.Cookie(name string) (*http.Cookie, error)
c.Cookies() []*http.Cookie
c.RealIP() string
c.IsTLS() bool
c.IsWebSocket() bool
c.Scheme() stringBody binding:
c.Bind(i any) error // auto-detects Content-Type → JSON/XML/form
c.Validate(i any) error // delegates to configured ValidatorGeneric binder helpers (Go generics):
// Package-level generic functions (type-safe, no interface{})
echo.PathParam[T any](c, key) (T, error)
echo.QueryParam[T any](c, key) (T, error)
echo.FormValue[T any](c, key) (T, error)
echo.ParseValue[T any](value string) (T, error)
// ...and *Or variants with defaultsStandalone bind functions:
echo.BindPathValues(c, target any) error
echo.BindQueryParams(c, target any) error
echo.BindBody(c, target any) error
echo.BindHeaders(c, target any) errorValueBinder (fluent/chained binding):
echo.QueryParamsBinder(c).String("name", &name).Int("age", &age).BindError()
echo.PathValuesBinder(c).Int64("id", &id).BindError()
echo.FormFieldBinder(c).Strings("tags", &tags).BindError()Response writing:
c.JSON(code int, i any) error
c.JSONPretty(code, i, indent) error
c.JSONBlob(code, b []byte) error
c.JSONP(code, callback, i) error
c.XML(code, i) error
c.XMLPretty(code, i, indent) error
c.XMLBlob(code, b) error
c.HTML(code, html string) error
c.HTMLBlob(code, b) error
c.String(code, s string) error
c.Blob(code, contentType, b) error
c.Stream(code, contentType, r io.Reader) error
c.Render(code, name string, data) error // uses configured Renderer
c.File(file string) error
c.FileFS(file string, fs) error
c.Attachment(file, name string) error
c.Inline(file, name string) error
c.NoContent(code) error
c.Redirect(code, url) errorPer-request key-value store:
c.Set(key string, val any)
c.Get(key string) any
// Type-safe generic variants
echo.ContextGet[T any](c, key) (T, error)
echo.ContextGetOr[T any](c, key, defaultValue T) (T, error)Miscellaneous:
c.RouteInfo() RouteInfo // matched route metadata
c.Response() http.ResponseWriter
c.SetRequest(r *http.Request)
c.SetResponse(rw http.ResponseWriter)
c.Logger() *slog.Logger
c.SetLogger(logger *slog.Logger)
c.Echo() *EchoServer lifecycle API#
e.Start(addr) is the zero-config convenience path. Production use goes through StartConfig:
sc := echo.StartConfig{
Address: ":8080",
TLSConfig: tlsCfg,
GracefulTimeout: 30 * time.Second,
BeforeServeFunc: func(s *http.Server) error {
s.ReadHeaderTimeout = 5 * time.Second
return nil
},
ListenerAddrFunc: func(addr net.Addr) {
fmt.Println("listening on", addr)
},
}
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
if err := sc.Start(ctx, e); err != http.ErrServerClosed {
log.Fatal(err)
}
// TLS variant
sc.StartTLS(ctx, e, "cert.pem", "key.pem") // file paths or []byteError handling API#
// Create HTTP errors
echo.NewHTTPError(code int, message string) *HTTPError
// Query status code from any error
echo.StatusCode(err error) int
// Built-in error handler factory
echo.DefaultHTTPErrorHandler(exposeError bool) HTTPErrorHandler
// Type
type HTTPErrorHandler func(c *Context, err error)
// Resolve response/status from a ResponseWriter
echo.ResolveResponseStatus(rw, err) (*Response, status int)IP extraction API#
echo.ExtractIPDirect() IPExtractor
echo.ExtractIPFromRealIPHeader(opts ...TrustOption) IPExtractor
echo.ExtractIPFromXFFHeader(opts ...TrustOption) IPExtractor
echo.LegacyIPExtractor() IPExtractor
// Trust option builders
echo.TrustLoopback(v bool) TrustOption
echo.TrustLinkLocal(v bool) TrustOption
echo.TrustPrivateNet(v bool) TrustOption
echo.TrustIPRange(ipRange *net.IPNet) TrustOptionRouter API#
echo.NewRouter(config RouterConfig) *DefaultRouter
// Concurrent-safe wrapper (read-heavy workloads)
echo.NewConcurrentRouter(r Router) Router
// Virtual host dispatch (route per hostname)
echo.NewVirtualHostHandler(vhosts map[string]*Echo) *Echo
// Route introspection
echo.HandlerName(h HandlerFunc) string // runtime name of handler funcTesting API (echotest package)#
// Build a Context without a running server
cfg := echotest.ContextConfig{
Method: http.MethodPost,
Target: "/users",
Body: strings.NewReader(`{"name":"alice"}`),
Headers: map[string]string{"Content-Type": "application/json"},
}
c := cfg.ToContext(t)
c, rec := cfg.ToContextRecorder(t)
rec = cfg.ServeWithHandler(t, myHandler)
// Load test fixture bytes from testdata/
data := echotest.LoadBytes(t, "response.json", echotest.TrimNewlineEnd)Plugin / Extension system#
Echo’s extension model is interface-based injection, not a plugin framework. Five swappable interfaces are wired at construction:
| Interface | Method(s) | Purpose |
|---|---|---|
Router | Add, Route, Routes, Remove | URL routing algorithm |
Binder | Bind(c, i) | Request deserialization |
Renderer | Render(w, name, data, c) | Template rendering |
Validator | Validate(i) | Struct validation |
JSONSerializer | Serialize, Deserialize | JSON encoding/decoding |
Extension points for middleware:
- Any
MiddlewareFunccan be registered at global, group, or route level MiddlewareConfiguratorinterface allows middleware factories that return errors (used inmiddleware/package internally)RateLimiterStoreinterface in the rate-limiter middleware lets you swap the in-memory store for Redis or another backend
No code generation, no plugin discovery, no RPC boundary. Everything is linked at compile time via Go interfaces.
API style analysis#
Strengths:
- Uniform route registration — identical signatures on
*Echoand*Groupmeans IDEs show the same autocomplete everywhere - Generics for type-safe binding (
PathParam[T],QueryParam[T]) eliminateinterface{}boilerplate in v5 without breaking the simplec.Param()escape hatch - Dual middleware API (
CORS()vsCORSWithConfig(cfg)) gives progressive disclosure: simple callers get one-liners, power users get full struct config WrapHandler/WrapMiddlewarebridge to the stdlibnet/httpecosystem at zero costStartConfigdecouples server lifecycle — Echo itself is testable without a TCP port
Trade-offs:
Contextas a concrete struct (v5 change from interface in v4) prevents type-safe embedding of custom fields; per-request state must go throughc.Set/c.Getwithanytype assertions (mitigated byContextGet[T])- Config-struct construction (
NewWithConfig) is verbose compared to functional-options style (noWithX()helpers for fields; you must know what fields exist) - No built-in OpenAPI/swagger route introspection;
RouteInfois a runtime struct, not a code-gen artifact
Backward compatibility#
Echo follows major version as module path (/v5). Within v5 there is no visible versioning shim — the CLAUDE.md note says this is v4 branch, but go.mod in the checked-out repo is v5. The dual Foo() / FooWithConfig() middleware pattern provides forward compatibility: adding new config fields to a *Config struct is non-breaking.