App-facing HTTP contracts with an Echo-backed runtime for GoForj applications.
web keeps application handlers behind focused Context, Router, and middleware contracts while Echo stays at the HTTP boundary. It includes an Echo adapter, route declarations, common middleware, WebSockets, Prometheus instrumentation, handler test helpers, and source-aware route and OpenAPI indexing.
The contracts deliberately expose less than Echo. Applications can stay on the smaller surface for ordinary HTTP work and use explicit Echo escape hatches when an integration needs the underlying engine or context.
Requires Go 1.25 or newer.
go get github.com/goforj/webpackage main
import (
"fmt"
"log"
"net/http"
"github.com/goforj/web"
"github.com/goforj/web/adapter/echoweb"
"github.com/goforj/web/webmiddleware"
)
func main() {
adapter := echoweb.New()
router := adapter.Router()
router.Use(
webmiddleware.Recover(),
webmiddleware.RequestID(),
)
router.GET("/healthz", func(c web.Context) error {
return c.Text(http.StatusOK, "ok")
})
router.GET("/users/:id", func(c web.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]any{
"id": id,
"name": fmt.Sprintf("user-%s", id),
})
})
log.Fatal(http.ListenAndServe(":8080", adapter))
}Start the server with go run ., then make requests from another shell:
$ curl -s http://localhost:8080/healthz
ok
$ curl -s http://localhost:8080/users/42
{"id":"42","name":"user-42"}The quick start owns the http.Server directly to keep the first example small. For cancellation-aware graceful shutdown, use echoweb.NewServer.
| Start with | Use it when |
|---|---|
echoweb.New() and router.GET(...) |
Routes are registered directly and your application owns the http.Server. |
web.NewRouteGroup(...) and web.RegisterRoutes(...) |
Routes should be reusable declarations for reporting, indexing, or generated application composition. |
echoweb.NewServer(...) |
The adapter should register route groups and own graceful HTTP shutdown. |
echoweb.Wrap(engine) |
An existing Echo engine needs to expose the app-facing web.Router contract. |
| Package | Purpose |
|---|---|
web |
Handler, context, router, route declaration, WebSocket, and route-reporting contracts. |
adapter/echoweb |
Echo adapter, native escape hatches, and server lifecycle. |
webmiddleware |
Authentication, security, routing, payload, compression, timeout, proxy, and rate-limit middleware. |
webprometheus |
HTTP metrics middleware, scrape handlers, and Pushgateway support. |
webindex |
Source-aware route manifests, diagnostics, schemas, and OpenAPI documents. |
webtest |
Lightweight contexts for isolated handler tests. |
The quick start above is a complete program. The following recipes are focused excerpts; complete generated programs are available in examples.
Route values keep registration data available for route reporting and source-aware tooling instead of burying every route in adapter calls.
adapter := echoweb.New()
routes := []web.Route{
web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error {
return c.NoContent(http.StatusNoContent)
}),
web.NewRoute(http.MethodGet, "/users", func(c web.Context) error {
return c.JSON(http.StatusOK, []map[string]any{{"id": 1}})
}),
}
group := web.NewRouteGroup("/api", routes)
if err := web.RegisterRoutes(adapter.Router(), []web.RouteGroup{group}); err != nil {
log.Fatal(err)
}Use Pre for middleware that must change the request method or path before route matching. Method override, rewrite, and trailing-slash middleware belong in this phase. Use Use for middleware that wraps the matched request handler.
router := echoweb.New().Router()
router.Pre(
webmiddleware.MethodOverride(),
webmiddleware.RemoveTrailingSlash(),
)
store := webmiddleware.NewRateLimiterMemoryStore(rate.Every(time.Second))
router.Use(
webmiddleware.Recover(),
webmiddleware.RequestID(),
webmiddleware.RateLimiter(store),
)Middleware runs in registration order, so put recovery and request identity near the outside of the chain and keep a shared rate-limit store for the lifetime of the application.
Server.Serve listens until its context is cancelled, then shuts down with the configured timeout.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
server, err := echoweb.NewServer(echoweb.ServerConfig{
Addr: ":8080",
RouteGroups: []web.RouteGroup{
web.NewRouteGroup("/api", []web.Route{
web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error {
return c.NoContent(http.StatusNoContent)
}),
}),
},
ShutdownTimeout: 10 * time.Second,
})
if err != nil {
log.Fatal(err)
}
if err := server.Serve(ctx); err != nil {
log.Fatal(err)
}webtest.NewContext runs an isolated handler without booting a router or listener. Use the Echo adapter with httptest when the route mapping itself is part of the behavior under test.
func TestHealthHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
rec := httptest.NewRecorder()
ctx := webtest.NewContext(req, rec, "/healthz", nil)
handler := func(c web.Context) error {
return c.Text(http.StatusOK, "ok")
}
if err := handler(ctx); err != nil {
t.Fatal(err)
}
if rec.Code != http.StatusOK {
t.Fatalf("expected %d, got %d", http.StatusOK, rec.Code)
}
if rec.Body.String() != "ok" {
t.Fatalf("expected ok, got %q", rec.Body.String())
}
}Use one metrics instance for both middleware and scraping. An explicit registry keeps collector ownership local to the application.
registry := prometheus.NewRegistry()
metrics := webprometheus.MustNew(webprometheus.Config{
Namespace: "app",
Registerer: registry,
Gatherer: registry,
})
router := echoweb.New().Router()
router.Use(metrics.Middleware())
router.GET("/metrics", metrics.Handler())
router.GET("/users", func(c web.Context) error {
return c.JSON(http.StatusOK, []map[string]any{{"id": 1}})
})Set RouteCompositionPath to the source file that assembles the application route groups. Scoping the index to that composition keeps unrelated fixtures and unused providers out of the published contract.
manifest, err := webindex.Run(context.Background(), webindex.IndexOptions{
Root: ".",
RouteCompositionPath: "internal/http/routes.go",
OutPath: "build/webindex.json",
DiagnosticsPath: "build/webindex.diagnostics.json",
OpenAPIPath: "build/openapi.json",
Strict: true,
})
if err != nil {
log.Fatal(err)
}
log.Printf("indexed %d operations", len(manifest.Operations))Strict promotes unresolved source evidence into an error instead of publishing an ambiguous contract. The returned manifest still contains structured diagnostics for reporting.
WebSocket handlers use the same app-facing context plus a small connection contract.
router := echoweb.New().Router()
router.GETWS("/ws", func(c web.Context, conn web.WebSocketConn) error {
var message map[string]any
if err := conn.ReadJSON(&message); err != nil {
return err
}
return conn.WriteJSON(map[string]any{"echo": message})
})Use echoweb.Wrap to adapt an existing Echo engine. Adapter.Echo, echoweb.UnwrapContext, and Context.Native expose the underlying implementation when a framework-specific integration genuinely needs it.
If an application only needs Echo and benefits from its full native API everywhere, using Echo directly is reasonable. web earns its place when the smaller handler contract, route declarations, shared middleware surface, testing helpers, metrics, or source-aware indexing are useful application boundaries.
For backward compatibility, the Echo adapter initializes Echo's legacy IP extractor when an engine does not already have one. That extractor trusts forwarding headers without proxy checks, so configure it deliberately before using Context.RealIP for security, rate limiting, or auditing.
For a server reached directly by clients, use the network peer address:
adapter := echoweb.New()
adapter.Echo().IPExtractor = echo.ExtractIPDirect()Behind a trusted proxy, configure echo.ExtractIPFromXFFHeader or echo.ExtractIPFromRealIPHeader with trust options that match the deployment, and ensure the edge proxy removes client-supplied forwarding headers before adding its own.
Generated from public API comments and examples.
Echo returns the underlying Echo engine.
adapter := echoweb.New()
fmt.Println(adapter.Echo() != nil)
// trueRouter returns the app-facing router contract.
adapter := echoweb.New()
fmt.Println(adapter.Router() != nil)
// trueServeHTTP exposes the adapter as a standard http.Handler.
adapter := echoweb.New()
adapter.Router().GET("/healthz", func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
adapter.ServeHTTP(rr, req)
fmt.Println(rr.Code)
// 204New creates a new Echo-backed web adapter.
adapter := echoweb.New()
fmt.Println(adapter.Router() != nil, adapter.Echo() != nil)
// true trueNewServer creates an Echo-backed server from web route groups and mounts.
server, err := echoweb.NewServer(echoweb.ServerConfig{
RouteGroups: []web.RouteGroup{
web.NewRouteGroup("/api", []web.Route{
web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return c.NoContent(http.StatusNoContent) }),
}),
},
})
fmt.Println(err == nil, server.Router() != nil)
// true trueRouter exposes the app-facing router contract.
server, _ := echoweb.NewServer(echoweb.ServerConfig{})
fmt.Println(server.Router() != nil)
// trueServe starts the server and gracefully shuts it down when ctx is cancelled.
server, _ := echoweb.NewServer(echoweb.ServerConfig{Addr: "127.0.0.1:0"})
ctx, cancel := context.WithCancel(context.Background())
cancel()
fmt.Println(server.Serve(ctx) == nil)
// trueServeHTTP exposes the server as an http.Handler for tests and local probing.
server, _ := echoweb.NewServer(echoweb.ServerConfig{
RouteGroups: []web.RouteGroup{
web.NewRouteGroup("/api", []web.Route{
web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return c.NoContent(http.StatusNoContent) }),
}),
},
})
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/healthz", nil)
server.ServeHTTP(rr, req)
fmt.Println(rr.Code)
// 204UnwrapContext returns the underlying Echo context when the web.Context came from this adapter.
adapter := echoweb.New()
adapter.Router().GET("/healthz", func(c web.Context) error {
_, ok := echoweb.UnwrapContext(c)
fmt.Println(ok)
return c.NoContent(http.StatusOK)
})
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
adapter.ServeHTTP(rr, req)
// trueUnwrapWebSocketConn returns the underlying gorilla websocket connection.
_, ok := echoweb.UnwrapWebSocketConn(nil)
fmt.Println(ok)
// falseWrap exposes an existing Echo engine through the web.Router contract.
adapter := echoweb.Wrap(nil)
fmt.Println(adapter.Echo() != nil)
// trueRun indexes API metadata from source and writes artifacts.
manifest, err := webindex.Run(context.Background(), webindex.IndexOptions{
Root: ".",
OutPath: "webindex.json",
})
fmt.Println(err == nil, manifest.Version != "")
// true trueBasicAuth returns basic auth middleware.
router := echoweb.New().Router()
router.Use(webmiddleware.BasicAuth(func(user, pass string, c web.Context) (bool, error) {
return user == "demo" && pass == "secret", nil
}))
router.GET("/admin", func(c web.Context) error {
return c.Text(200, "welcome")
})BasicAuthWithConfig returns basic auth middleware with config.
router := echoweb.New().Router()
router.Use(webmiddleware.BasicAuthWithConfig(webmiddleware.BasicAuthConfig{
Realm: "Admin",
Validator: func(user, pass string, c web.Context) (bool, error) {
return user == "demo" && pass == "secret", nil
},
}))
router.GET("/admin", func(c web.Context) error {
return c.Text(200, "welcome")
})CSRF enables token-based CSRF protection.
router := echoweb.New().Router()
router.Use(webmiddleware.CSRF())
router.POST("/settings", func(c web.Context) error {
return c.NoContent(204)
})CSRFWithConfig enables token-based CSRF protection with config.
router := echoweb.New().Router()
router.Use(webmiddleware.CSRFWithConfig(webmiddleware.CSRFConfig{
CookieName: "_csrf",
TokenLookup: "header:X-CSRF-Token",
}))
router.POST("/settings", func(c web.Context) error {
return c.NoContent(204)
})CreateExtractors creates extractors from a lookup definition.
extractors, err := webmiddleware.CreateExtractors("header:X-API-Key,query:token")
fmt.Println(err == nil, len(extractors))
// true 2KeyAuth returns key auth middleware.
router := echoweb.New().Router()
router.Use(webmiddleware.KeyAuth(func(key string, c web.Context) (bool, error) {
return key == "demo-key", nil
}))
router.GET("/api/reports", func(c web.Context) error {
return c.JSON(200, map[string]any{"ready": true})
})KeyAuthWithConfig returns key auth middleware with config.
router := echoweb.New().Router()
router.Use(webmiddleware.KeyAuthWithConfig(webmiddleware.KeyAuthConfig{
KeyLookup: "query:api_key",
Validator: func(key string, c web.Context) (bool, error) {
return key == "demo-key", nil
},
}))
router.GET("/api/reports", func(c web.Context) error {
return c.JSON(200, map[string]any{"ready": true})
})Compress enables gzip response compression for clients that support it.
router := echoweb.New().Router()
router.Use(webmiddleware.Compress())
router.GET("/reports", func(c web.Context) error {
return c.Text(200, "large report response")
})Decompress inflates gzip-encoded request bodies before handlers read them.
router := echoweb.New().Router()
router.Use(webmiddleware.Decompress())
router.POST("/ingest", func(c web.Context) error {
data, _ := io.ReadAll(c.Request().Body)
return c.JSON(200, map[string]int{"bytes": len(data)})
})DecompressWithConfig inflates gzip-encoded request bodies with custom options.
router := echoweb.New().Router()
router.Use(webmiddleware.DecompressWithConfig(webmiddleware.DecompressConfig{
Skipper: func(c web.Context) bool {
return c.Path() == "/webhooks/raw"
},
}))
router.POST("/ingest", func(c web.Context) error {
return c.NoContent(202)
})Gzip enables gzip response compression for clients that support it.
router := echoweb.New().Router()
router.GET("/feed", func(c web.Context) error {
return c.Text(200, "large feed response")
}, webmiddleware.Gzip())GzipWithConfig enables gzip response compression with custom options.
router := echoweb.New().Router()
router.Use(webmiddleware.GzipWithConfig(webmiddleware.GzipConfig{
MinLength: 1024,
}))MethodFromForm gets an override method from a form field.
router := echoweb.New().Router()
router.Pre(webmiddleware.MethodOverrideWithConfig(webmiddleware.MethodOverrideConfig{
Getter: webmiddleware.MethodFromForm("_method"),
}))MethodFromHeader gets an override method from a request header.
router := echoweb.New().Router()
router.Pre(webmiddleware.MethodOverrideWithConfig(webmiddleware.MethodOverrideConfig{
Getter: webmiddleware.MethodFromHeader("X-HTTP-Method-Override"),
}))MethodFromQuery gets an override method from a query parameter.
router := echoweb.New().Router()
router.Pre(webmiddleware.MethodOverrideWithConfig(webmiddleware.MethodOverrideConfig{
Getter: webmiddleware.MethodFromQuery("_method"),
}))MethodOverride returns method override middleware.
router := echoweb.New().Router()
router.Pre(webmiddleware.MethodOverride())
router.PATCH("/articles/:id", func(c web.Context) error {
return c.NoContent(204)
})MethodOverrideWithConfig returns method override middleware with config.
router := echoweb.New().Router()
router.Pre(webmiddleware.MethodOverrideWithConfig(webmiddleware.MethodOverrideConfig{
Getter: webmiddleware.MethodFromQuery("_method"),
}))
router.DELETE("/articles/:id", func(c web.Context) error {
return c.NoContent(204)
})AddTrailingSlash adds a trailing slash to the request path.
router := echoweb.New().Router()
router.Pre(webmiddleware.AddTrailingSlash())
router.GET("/docs/", func(c web.Context) error {
return c.Text(200, "docs")
})AddTrailingSlashWithConfig returns trailing-slash middleware with config.
router := echoweb.New().Router()
router.Pre(webmiddleware.AddTrailingSlashWithConfig(webmiddleware.TrailingSlashConfig{
RedirectCode: 308,
}))
router.GET("/docs/", func(c web.Context) error {
return c.Text(200, "docs")
})RemoveTrailingSlash removes the trailing slash from the request path.
router := echoweb.New().Router()
router.Pre(webmiddleware.RemoveTrailingSlash())
router.GET("/docs", func(c web.Context) error {
return c.Text(200, "docs")
})RemoveTrailingSlashWithConfig returns remove-trailing-slash middleware with config.
router := echoweb.New().Router()
router.Pre(webmiddleware.RemoveTrailingSlashWithConfig(webmiddleware.TrailingSlashConfig{
RedirectCode: 308,
}))
router.GET("/docs", func(c web.Context) error {
return c.Text(200, "docs")
})Rewrite rewrites the request path using wildcard rules.
router := echoweb.New().Router()
router.Pre(webmiddleware.Rewrite(map[string]string{
"/old/*": "/new/$1",
}))
router.GET("/new/:name", func(c web.Context) error {
return c.Text(200, c.Param("name"))
})RewriteWithConfig rewrites the request path using wildcard and regex rules.
router := echoweb.New().Router()
router.Pre(webmiddleware.RewriteWithConfig(webmiddleware.RewriteConfig{
Rules: map[string]string{"/old/*": "/v2/$1"},
}))
router.GET("/v2/:name", func(c web.Context) error {
return c.Text(200, c.Param("name"))
})BodyDump captures request and response payloads.
router := echoweb.New().Router()
router.Use(webmiddleware.BodyDump(func(c web.Context, reqBody, resBody []byte) {
log.Printf("%s %s -> %d bytes", c.Method(), c.URI(), len(resBody))
}))
router.POST("/webhooks", func(c web.Context) error {
return c.JSON(202, map[string]any{"queued": true})
})BodyDumpWithConfig captures request and response payloads with config.
router := echoweb.New().Router()
router.Use(webmiddleware.BodyDumpWithConfig(webmiddleware.BodyDumpConfig{
Skipper: func(c web.Context) bool {
return c.Path() == "/healthz"
},
Handler: func(c web.Context, reqBody, resBody []byte) {
log.Printf("%s %s -> %d bytes", c.Method(), c.URI(), len(resBody))
},
}))BodyLimit returns middleware that limits request body size.
router := echoweb.New().Router()
router.Use(webmiddleware.BodyLimit("2MB"))
router.POST("/uploads", func(c web.Context) error {
return c.NoContent(204)
})BodyLimitWithConfig returns body limit middleware with config.
router := echoweb.New().Router()
router.Use(webmiddleware.BodyLimitWithConfig(webmiddleware.BodyLimitConfig{
Limit: "10MB",
}))
router.POST("/imports", func(c web.Context) error {
return c.NoContent(202)
})ErrorBodyDump captures response bodies for non-2xx and non-3xx responses.
router := echoweb.New().Router()
router.Use(webmiddleware.ErrorBodyDump(func(c web.Context, status int, body []byte) {
log.Printf("%s %s failed with %d", c.Method(), c.URI(), status)
}))
router.GET("/reports/:id", func(c web.Context) error {
return c.Text(404, "report not found")
})ErrorBodyDumpWithConfig captures response bodies for non-success responses with config.
router := echoweb.New().Router()
router.Use(webmiddleware.ErrorBodyDumpWithConfig(webmiddleware.ErrorBodyDumpConfig{
Skipper: func(c web.Context) bool {
return c.Path() == "/healthz"
},
Handler: func(c web.Context, status int, body []byte) {
log.Printf("%s %s failed with %d", c.Method(), c.URI(), status)
},
}))NewRandomBalancer creates a random proxy balancer.
target, _ := url.Parse("http://localhost:8080")
balancer := webmiddleware.NewRandomBalancer([]*webmiddleware.ProxyTarget{{URL: target}})
fmt.Println(balancer.Next(nil).URL.Host)
// localhost:8080NewRoundRobinBalancer creates a round-robin proxy balancer.
target, _ := url.Parse("http://localhost:8080")
balancer := webmiddleware.NewRoundRobinBalancer([]*webmiddleware.ProxyTarget{{URL: target}})
fmt.Println(balancer.Next(nil).URL.Host)
// localhost:8080Proxy creates a proxy middleware.
target, _ := url.Parse("http://localhost:8080")
balancer := webmiddleware.NewRandomBalancer([]*webmiddleware.ProxyTarget{{URL: target}})
router := echoweb.New().Router()
router.Use(webmiddleware.Proxy(balancer))ProxyWithConfig creates a proxy middleware with config.
target, _ := url.Parse("http://localhost:8080")
balancer := webmiddleware.NewRoundRobinBalancer([]*webmiddleware.ProxyTarget{{URL: target}})
router := echoweb.New().Router()
router.Use(webmiddleware.ProxyWithConfig(webmiddleware.ProxyConfig{
Balancer: balancer,
Rewrite: map[string]string{
"/api/*": "/$1",
},
}))NewRateLimiterMemoryStore creates an in-memory rate limiter store.
store := webmiddleware.NewRateLimiterMemoryStore(rate.Every(time.Second))
allowed1, _ := store.Allow("192.0.2.1")
allowed2, _ := store.Allow("192.0.2.1")
fmt.Println(allowed1, allowed2)
// true falseNewRateLimiterMemoryStoreWithConfig creates an in-memory rate limiter store with config.
store := webmiddleware.NewRateLimiterMemoryStoreWithConfig(webmiddleware.RateLimiterMemoryStoreConfig{Rate: rate.Every(time.Second)})
allowed, _ := store.Allow("192.0.2.1")
fmt.Println(allowed)
// trueRateLimiter creates a rate limiting middleware.
store := webmiddleware.NewRateLimiterMemoryStore(rate.Every(time.Second))
router := echoweb.New().Router()
router.Use(webmiddleware.RateLimiter(store))
router.POST("/api/messages", func(c web.Context) error {
return c.NoContent(202)
})Allow checks whether the given identifier is allowed through.
store := webmiddleware.NewRateLimiterMemoryStore(rate.Every(time.Second))
allowed, err := store.Allow("127.0.0.1")
fmt.Println(err == nil, allowed)
// true trueRateLimiterWithConfig creates a rate limiting middleware with config.
store := webmiddleware.NewRateLimiterMemoryStore(rate.Every(time.Second))
router := echoweb.New().Router()
router.Use(webmiddleware.RateLimiterWithConfig(webmiddleware.RateLimiterConfig{
Store: store,
IdentifierExtractor: func(c web.Context) (string, error) {
return c.Header("X-Account-ID"), nil
},
}))HTTPSNonWWWRedirect redirects to https without www.
router := echoweb.New().Router()
router.Use(webmiddleware.HTTPSNonWWWRedirect())HTTPSNonWWWRedirectWithConfig returns HTTPS non-WWW redirect middleware with config.
router := echoweb.New().Router()
router.Use(webmiddleware.HTTPSNonWWWRedirectWithConfig(webmiddleware.RedirectConfig{
Code: 307,
}))HTTPSRedirect redirects http requests to https.
router := echoweb.New().Router()
router.Use(webmiddleware.HTTPSRedirect())
router.GET("/docs", func(c web.Context) error {
return c.Text(200, "docs")
})HTTPSRedirectWithConfig returns HTTPS redirect middleware with config.
router := echoweb.New().Router()
router.Use(webmiddleware.HTTPSRedirectWithConfig(webmiddleware.RedirectConfig{
Code: 307,
}))HTTPSWWWRedirect redirects to https + www.
router := echoweb.New().Router()
router.Use(webmiddleware.HTTPSWWWRedirect())HTTPSWWWRedirectWithConfig returns HTTPS+WWW redirect middleware with config.
router := echoweb.New().Router()
router.Use(webmiddleware.HTTPSWWWRedirectWithConfig(webmiddleware.RedirectConfig{
Code: 307,
}))NonWWWRedirect redirects to the non-www host.
router := echoweb.New().Router()
router.Use(webmiddleware.NonWWWRedirect())NonWWWRedirectWithConfig returns non-WWW redirect middleware with config.
router := echoweb.New().Router()
router.Use(webmiddleware.NonWWWRedirectWithConfig(webmiddleware.RedirectConfig{
Code: 307,
}))WWWRedirect redirects to the www host.
router := echoweb.New().Router()
router.Use(webmiddleware.WWWRedirect())WWWRedirectWithConfig returns WWW redirect middleware with config.
router := echoweb.New().Router()
router.Use(webmiddleware.WWWRedirectWithConfig(webmiddleware.RedirectConfig{
Code: 307,
}))Recover returns middleware that recovers panics from the handler chain.
router := echoweb.New().Router()
router.Use(webmiddleware.Recover())
router.GET("/panic", func(c web.Context) error {
panic("boom")
})RecoverWithConfig returns recover middleware with config.
router := echoweb.New().Router()
router.Use(webmiddleware.RecoverWithConfig(webmiddleware.RecoverConfig{
DisableStack: true,
HandleError: func(c web.Context, err error, stack []byte) error {
return c.JSON(500, map[string]any{"error": "internal server error"})
},
}))ContextTimeout sets a timeout on the request context.
router := echoweb.New().Router()
router.Use(webmiddleware.ContextTimeout(2 * time.Second))
router.GET("/reports", func(c web.Context) error {
return c.JSON(200, map[string]any{"ready": true})
})ContextTimeoutWithConfig sets a timeout on the request context with config.
router := echoweb.New().Router()
router.Use(webmiddleware.ContextTimeoutWithConfig(webmiddleware.ContextTimeoutConfig{
Timeout: time.Second,
}))DefaultSkipper always runs the middleware.
fmt.Println(webmiddleware.DefaultSkipper(nil))
// falseRequestID returns middleware that sets a request id header and context value.
router := echoweb.New().Router()
router.Use(webmiddleware.RequestID())
router.GET("/healthz", func(c web.Context) error {
return c.JSON(200, map[string]any{
"request_id": c.Get("request_id"),
})
})RequestIDWithConfig returns RequestID middleware with config.
router := echoweb.New().Router()
router.Use(webmiddleware.RequestIDWithConfig(webmiddleware.RequestIDConfig{
TargetHeader: "X-Correlation-ID",
ContextKey: "correlation_id",
}))RequestLoggerWithConfig returns request logger middleware with config.
router := echoweb.New().Router()
router.Use(webmiddleware.RequestLoggerWithConfig(webmiddleware.RequestLoggerConfig{
LogValuesFunc: func(c web.Context, values webmiddleware.RequestLoggerValues) error {
log.Printf("%s %s %d %s", values.Method, values.URI, values.Status, values.Latency)
return nil
},
}))
router.GET("/users/:id", func(c web.Context) error {
return c.NoContent(204)
})Timeout returns a response-timeout middleware.
router := echoweb.New().Router()
router.Use(webmiddleware.Timeout())
router.GET("/healthz", func(c web.Context) error {
return c.NoContent(204)
})TimeoutWithConfig returns a response-timeout middleware with config.
router := echoweb.New().Router()
router.Use(webmiddleware.TimeoutWithConfig(webmiddleware.TimeoutConfig{
Timeout: time.Second,
ErrorMessage: "request timed out",
}))CORS returns Cross-Origin Resource Sharing middleware.
router := echoweb.New().Router()
router.Use(webmiddleware.CORS())
router.GET("/api/healthz", func(c web.Context) error {
return c.JSON(200, map[string]any{"ok": true})
})CORSWithConfig returns CORS middleware with config.
router := echoweb.New().Router()
router.Use(webmiddleware.CORSWithConfig(webmiddleware.CORSConfig{
AllowOrigins: []string{"https://app.example.com"},
AllowMethods: []string{"GET", "POST", "PATCH"},
}))
router.GET("/api/healthz", func(c web.Context) error {
return c.JSON(200, map[string]any{"ok": true})
})Secure sets security-oriented response headers.
router := echoweb.New().Router()
router.Use(webmiddleware.Secure())
router.GET("/", func(c web.Context) error {
return c.Text(200, "home")
})SecureWithConfig sets security-oriented response headers with config.
router := echoweb.New().Router()
router.Use(webmiddleware.SecureWithConfig(webmiddleware.SecureConfig{
ReferrerPolicy: "same-origin",
ContentSecurityPolicy: "default-src 'self'",
}))Static serves static content from the provided root.
router := echoweb.New().Router()
router.Use(webmiddleware.Static("public"))
router.GET("/healthz", func(c web.Context) error {
return c.NoContent(204)
})StaticWithConfig serves static content using config.
router := echoweb.New().Router()
router.Use(webmiddleware.StaticWithConfig(webmiddleware.StaticConfig{
Root: "public",
HTML5: true,
}))Default returns the package-level Prometheus metrics instance.
fmt.Println(webprometheus.Default() == webprometheus.Default())
// trueHandler returns the package-level Prometheus scrape handler.
registry := prometheus.NewRegistry()
counter := prometheus.NewCounter(prometheus.CounterOpts{Name: "demo_total", Help: "demo counter"})
registry.MustRegister(counter)
counter.Inc()
metrics, _ := webprometheus.New(webprometheus.Config{Registerer: prometheus.NewRegistry(), Gatherer: registry})
recorder := httptest.NewRecorder()
ctx := webtest.NewContext(httptest.NewRequest(http.MethodGet, "/metrics", nil), recorder, "/metrics", nil)
_ = metrics.Handler()(ctx)
fmt.Println(strings.Contains(recorder.Body.String(), "demo_total"))
// trueHandler exposes the configured Prometheus metrics as a web.Handler.
registry := prometheus.NewRegistry()
counter := prometheus.NewCounter(prometheus.CounterOpts{Name: "demo_total", Help: "demo counter"})
registry.MustRegister(counter)
counter.Inc()
metrics, _ := webprometheus.New(webprometheus.Config{Registerer: prometheus.NewRegistry(), Gatherer: registry})
recorder := httptest.NewRecorder()
ctx := webtest.NewContext(httptest.NewRequest(http.MethodGet, "/metrics", nil), recorder, "/metrics", nil)
_ = metrics.Handler()(ctx)
fmt.Println(strings.Contains(recorder.Body.String(), "demo_total"))
// trueMiddleware records Prometheus metrics for each request.
registry := prometheus.NewRegistry()
metrics, _ := webprometheus.New(webprometheus.Config{Registerer: registry, Gatherer: registry, Namespace: "example"})
handler := metrics.Middleware()(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
ctx := webtest.NewContext(httptest.NewRequest(http.MethodGet, "/healthz", nil), nil, "/healthz", nil)
_ = handler(ctx)
out := &bytes.Buffer{}
_ = webprometheus.WriteGatheredMetrics(out, registry)
fmt.Println(strings.Contains(out.String(), "example_requests_total"))
// trueMiddleware returns the package-level Prometheus middleware.
registry := prometheus.NewRegistry()
metrics, _ := webprometheus.New(webprometheus.Config{Registerer: registry, Gatherer: registry, Namespace: "example"})
handler := metrics.Middleware()(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
ctx := webtest.NewContext(httptest.NewRequest(http.MethodGet, "/healthz", nil), nil, "/healthz", nil)
_ = handler(ctx)
out := &bytes.Buffer{}
_ = webprometheus.WriteGatheredMetrics(out, registry)
fmt.Println(strings.Contains(out.String(), "example_requests_total"))
// trueMustNew creates a Metrics instance and panics on registration errors.
metrics := webprometheus.MustNew(webprometheus.Config{Registerer: prometheus.NewRegistry(), Gatherer: prometheus.NewRegistry()})
fmt.Println(metrics != nil)
// trueNew creates a Metrics instance backed by Prometheus collectors.
metrics, err := webprometheus.New(webprometheus.Config{Namespace: "app"})
_ = metrics
fmt.Println(err == nil)
// trueRunPushGatewayGatherer starts pushing collected metrics until the context finishes.
err := webprometheus.RunPushGatewayGatherer(context.Background(), webprometheus.PushGatewayConfig{})
fmt.Println(err != nil)
// trueWriteGatheredMetrics gathers collected metrics and writes them to the given writer.
var buf bytes.Buffer
err := webprometheus.WriteGatheredMetrics(&buf, prometheus.NewRegistry())
fmt.Println(err == nil)
// trueBuildRouteEntries builds a sorted slice of route entries from registered groups and extra entries.
entries := web.BuildRouteEntries([]web.RouteGroup{
web.NewRouteGroup("/api", []web.Route{
web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil }),
}),
})
fmt.Println(entries[0].Path, entries[0].Methods[0])
// /api/healthz GETRenderRouteTable renders a route table using simple ASCII borders and ANSI colors.
table := web.RenderRouteTable([]web.RouteEntry{{
Path: "/api/healthz",
Handler: "monitoring.Healthz",
Methods: []string{"GET"},
}})
fmt.Println(strings.Contains(table, "/api/healthz"))
// trueMountRouter applies mount-style router configuration in declaration order.
adapter := echoweb.New()
err := web.MountRouter(adapter.Router(), []web.RouterMount{
func(r web.Router) error {
r.GET("/healthz", func(c web.Context) error { return nil })
return nil
},
})
fmt.Println(err == nil)
// trueNewRoute creates a new route using the app-facing web handler contract directly.
route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error {
return c.NoContent(http.StatusOK)
})
fmt.Println(route.Method(), route.Path())
// GET /healthzNewRouteGroup wraps routes and their accompanied web middleware.
group := web.NewRouteGroup("/api", []web.Route{
web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil }),
})
fmt.Println(group.RoutePrefix(), len(group.Routes()))
// /api 1NewWebSocketRoute creates a websocket route using the app-facing websocket handler contract.
route := web.NewWebSocketRoute("/ws", func(c web.Context, conn web.WebSocketConn) error {
return nil
})
fmt.Println(route.IsWebSocket())
// trueRegisterRoutes registers route groups onto a router.
adapter := echoweb.New()
groups := []web.RouteGroup{
web.NewRouteGroup("/api", []web.Route{
web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil }),
}),
}
err := web.RegisterRoutes(adapter.Router(), groups)
fmt.Println(err == nil)
// trueHandler returns the route handler.
route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error {
return c.NoContent(http.StatusCreated)
})
ctx := webtest.NewContext(nil, nil, "/healthz", nil)
_ = route.Handler()(ctx)
fmt.Println(ctx.StatusCode())
// 201HandlerName returns the original handler name for route reporting.
route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil })
fmt.Println(route.HandlerName() != "")
// trueIsWebSocket reports whether this route upgrades to a websocket connection.
route := web.NewWebSocketRoute("/ws", func(c web.Context, conn web.WebSocketConn) error { return nil })
fmt.Println(route.IsWebSocket())
// trueMethod returns the HTTP method.
route := web.NewRoute(http.MethodPost, "/users", func(c web.Context) error { return nil })
fmt.Println(route.Method())
// POSTMiddlewareNames returns original middleware names for route reporting.
route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil }).WithMiddlewareNames("auth")
fmt.Println(route.MiddlewareNames()[0])
// authMiddlewares returns the route middleware slice.
route := web.NewRoute(
http.MethodGet,
"/healthz",
func(c web.Context) error { return nil },
func(next web.Handler) web.Handler { return next },
)
fmt.Println(len(route.Middlewares()))
// 1Path returns the path of the route.
route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil })
fmt.Println(route.Path())
// /healthzWebSocketHandler returns the websocket route handler.
route := web.NewWebSocketRoute("/ws", func(c web.Context, conn web.WebSocketConn) error {
c.Set("ready", true)
return nil
})
ctx := webtest.NewContext(nil, nil, "/ws", nil)
err := route.WebSocketHandler()(ctx, nil)
fmt.Println(err == nil, ctx.Get("ready"))
// true trueWithMiddlewareNames attaches reporting-only middleware names to the route.
route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil }).WithMiddlewareNames("auth", "trace")
fmt.Println(len(route.MiddlewareNames()))
// 2MiddlewareNames returns original middleware names for route reporting.
group := web.NewRouteGroup("/api", nil).WithMiddlewareNames("auth")
fmt.Println(group.MiddlewareNames()[0])
// authMiddlewares returns the middleware slice for the group.
group := web.NewRouteGroup("/api", nil, func(next web.Handler) web.Handler { return next })
fmt.Println(len(group.Middlewares()))
// 1RoutePrefix returns the group prefix.
group := web.NewRouteGroup("/api", nil)
fmt.Println(group.RoutePrefix())
// /apiRoutes returns the routes in the group.
group := web.NewRouteGroup("/api", []web.Route{
web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil }),
})
fmt.Println(len(group.Routes()))
// 1WithMiddlewareNames attaches reporting-only middleware names to the group.
group := web.NewRouteGroup("/api", nil).WithMiddlewareNames("auth", "trace")
fmt.Println(len(group.MiddlewareNames()))
// 2NewContext creates a new test context around the provided request/recorder pair.
req := httptest.NewRequest(http.MethodGet, "/users/42?expand=roles", nil)
ctx := webtest.NewContext(req, nil, "/users/:id", webtest.PathParams{"id": "42"})
fmt.Println(ctx.Param("id"), ctx.Query("expand"))
// 42 roles