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:

  1. Route registration API — how handlers are attached to paths
  2. Context API — how handlers read requests and write responses
  3. Middleware API — how cross-cutting concerns are wired in

Library API#

Public packages#

PackagePurpose
github.com/labstack/echo/v5Core framework: Echo, Context, Router, Group, StartConfig
github.com/labstack/echo/v5/middleware24 production-ready middleware implementations
github.com/labstack/echo/v5/echotestTest 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) error

Adapter 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 → echo

Built-in middleware catalog (24 implementations)#

Each middleware follows the same dual-API pattern: Foo() for zero-config use, FooWithConfig(cfg FooConfig) for full control.

MiddlewareFilePurpose
BasicAuthbasic_auth.goHTTP Basic Auth with custom validator
BodyDumpbody_dump.goCapture req/resp bodies for logging/debugging
BodyLimitbody_limit.goEnforce max request body size
Gzipcompress.goGzip response compression
ContextTimeoutcontext_timeout.goPer-request deadline via context.WithTimeout
CORScors.goCross-Origin Resource Sharing headers
CSRFcsrf.goCSRF token generation and validation
Decompressdecompress.goDecompress gzip-encoded request bodies
KeyAuthkey_auth.goAPI key authentication
MethodOverridemethod_override.goHTTP method override from form/header/query
Proxyproxy.goReverse proxy with random/round-robin balancing
RateLimiterrate_limiter.goToken-bucket rate limiting (in-memory store included)
Recoverrecover.goPanic recovery with stack traces
HTTPSRedirectredirect.goHTTP→HTTPS redirect (and www variants)
RequestIDrequest_id.goInject unique request ID header
RequestLoggerrequest_logger.goStructured request/response logging
Rewriterewrite.goURL rewriting with regex rules
Securesecure.goSecurity headers (XSS, HSTS, frame options, etc.)
AddTrailingSlashslash.goNormalize trailing slashes
RemoveTrailingSlashslash.goNormalize trailing slashes
Staticstatic.goServe 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() string

Body binding:

c.Bind(i any) error              // auto-detects Content-Type → JSON/XML/form
c.Validate(i any) error          // delegates to configured Validator

Generic 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 defaults

Standalone 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) error

ValueBinder (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) error

Per-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() *Echo

Server 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 []byte

Error 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) TrustOption

Router 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 func

Testing 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:

InterfaceMethod(s)Purpose
RouterAdd, Route, Routes, RemoveURL routing algorithm
BinderBind(c, i)Request deserialization
RendererRender(w, name, data, c)Template rendering
ValidatorValidate(i)Struct validation
JSONSerializerSerialize, DeserializeJSON encoding/decoding

Extension points for middleware:

  • Any MiddlewareFunc can be registered at global, group, or route level
  • MiddlewareConfigurator interface allows middleware factories that return errors (used in middleware/ package internally)
  • RateLimiterStore interface 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 *Echo and *Group means IDEs show the same autocomplete everywhere
  • Generics for type-safe binding (PathParam[T], QueryParam[T]) eliminate interface{} boilerplate in v5 without breaking the simple c.Param() escape hatch
  • Dual middleware API (CORS() vs CORSWithConfig(cfg)) gives progressive disclosure: simple callers get one-liners, power users get full struct config
  • WrapHandler / WrapMiddleware bridge to the stdlib net/http ecosystem at zero cost
  • StartConfig decouples server lifecycle — Echo itself is testable without a TCP port

Trade-offs:

  • Context as a concrete struct (v5 change from interface in v4) prevents type-safe embedding of custom fields; per-request state must go through c.Set/c.Get with any type assertions (mitigated by ContextGet[T])
  • Config-struct construction (NewWithConfig) is verbose compared to functional-options style (no WithX() helpers for fields; you must know what fields exist)
  • No built-in OpenAPI/swagger route introspection; RouteInfo is 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.