web

package module
v0.6.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 11 Imported by: 0

README

goforj/web logo

App-facing HTTP contracts with an Echo-backed runtime for GoForj applications.

Go Reference CI Go 1.25 or newer Latest release Coverage

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.

Installation

Requires Go 1.25 or newer.

go get github.com/goforj/web

Quick start

package 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.

Choose an entry point

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 map

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.

Common workflows

The quick start above is a complete program. The following recipes are focused excerpts; complete generated programs are available in examples.

Declarative route groups

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)
}

Middleware phases

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.

Graceful server lifecycle

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)
}

Test a handler

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())
	}
}

Expose Prometheus metrics

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}})
})

Generate a route manifest and OpenAPI document

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.RunCached(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,
}, "build/webindex.cache")
if err != nil {
	log.Fatal(err)
}
log.Printf("indexed %d operations", len(manifest.Operations))

RunCached reuses a content-validated analysis snapshot while artifact publication remains changed-only; use Run when persistent caching is unnecessary. Strict promotes unresolved source evidence into an error instead of publishing an ambiguous contract. The returned manifest still contains structured diagnostics for reporting.

Add a WebSocket route

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})
})

Echo escape hatches

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.

Client IP trust

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.

Performance

Go HTTP stack loopback and in-process performance comparison

Whiskers in every panel show the observed sample minimum and maximum. The first panel measures single-core HTTP/1.1 loopback requests per second over a reused connection. The other panels measure in-process ServeHTTP operations per second, and their allocation figures cover the complete route and handler dispatch. Middleware details show the median paired latency added above the plaintext route measured in the same benchmark process. Each primary value is the median of 7 samples at 1s with GOMAXPROCS=1.

Bars are scaled independently within each panel, and small differences should not be treated as rankings. These are microbenchmarks and loopback ceilings, not production capacity forecasts.

Measured with go1.26.1 on linux/arm64 (arm64 (CPU model unavailable)), kernel Linux 7.0.11-orbstack-00360-gc9bc4d96ac70, revision 07fbcb90c743. Build settings: CGO_ENABLED=1, GOARM64=v8.0, GODEBUG=(unset), GOEXPERIMENT=(unset), GOFLAGS=(unset). Benchmark inputs: sha256:e60df1b40baf9fda820de339c3920367faacea80fdd47b235628691298e1a4be. Dependencies: net/http go1.26.1, GoForj Web local checkout, Echo v5.1.0, Gin v1.12.0, Chi v5.3.1, Gorilla Mux v1.8.1, httprouter v1.3.0.

Fiber is omitted because its fasthttp engine is not directly comparable in this shared net/http suite. See the benchmark methodology and recorded sample rows.

Regenerate the measurement and image with:

make benchmark-svg

API

API Index

Group Functions
Adapter Adapter.Echo · Adapter.Router · Adapter.ServeHTTP · New · NewServer · Server.Router · Server.Serve · Server.ServeHTTP · UnwrapContext · UnwrapWebSocketConn · Wrap
Indexing Run · RunCached
Middleware
Auth
BasicAuth · BasicAuthWithConfig · CSRF · CSRFWithConfig · CreateExtractors · KeyAuth · KeyAuthWithConfig
Middleware
Compression
Compress · Decompress · DecompressWithConfig · Gzip · GzipWithConfig
Middleware
Method Override
MethodFromForm · MethodFromHeader · MethodFromQuery · MethodOverride · MethodOverrideWithConfig
Middleware
Path Rewriting
AddTrailingSlash · AddTrailingSlashWithConfig · RemoveTrailingSlash · RemoveTrailingSlashWithConfig · Rewrite · RewriteWithConfig
Middleware
Payloads
BodyDump · BodyDumpWithConfig · BodyLimit · BodyLimitWithConfig · ErrorBodyDump · ErrorBodyDumpWithConfig
Middleware
Proxying
NewRandomBalancer · NewRoundRobinBalancer · Proxy · ProxyWithConfig
Middleware
Rate Limiting
NewRateLimiterMemoryStore · NewRateLimiterMemoryStoreWithConfig · RateLimiter · RateLimiterMemoryStore.Allow · RateLimiterWithConfig
Middleware
Redirects
HTTPSNonWWWRedirect · HTTPSNonWWWRedirectWithConfig · HTTPSRedirect · HTTPSRedirectWithConfig · HTTPSWWWRedirect · HTTPSWWWRedirectWithConfig · NonWWWRedirect · NonWWWRedirectWithConfig · WWWRedirect · WWWRedirectWithConfig
Middleware
Reliability
Recover · RecoverWithConfig
Middleware
Request Lifecycle
ContextTimeout · ContextTimeoutWithConfig · DefaultSkipper · RequestID · RequestIDWithConfig · RequestLoggerWithConfig · Timeout · TimeoutWithConfig
Middleware
Security
CORS · CORSWithConfig · Secure · SecureWithConfig
Middleware
Static Files
Static · StaticWithConfig
Prometheus Default · Handler · Metrics.Handler · Metrics.Middleware · Middleware · MustNew · New · RunPushGatewayGatherer · WriteGatheredMetrics
Route Reporting BuildRouteEntries · RenderRouteTable
Routing MountRouter · NewRoute · NewRouteGroup · NewWebSocketRoute · RegisterRoutes · Route.Handler · Route.HandlerName · Route.IsWebSocket · Route.Method · Route.MiddlewareNames · Route.Middlewares · Route.Path · Route.WebSocketHandler · Route.WithMiddlewareNames · RouteGroup.MiddlewareNames · RouteGroup.Middlewares · RouteGroup.RoutePrefix · RouteGroup.Routes · RouteGroup.WithMiddlewareNames
Testing NewContext

API Reference

Generated from public API comments and examples.

Adapter

echoweb.Adapter.Echo

Echo returns the underlying Echo engine.

adapter := echoweb.New()
fmt.Println(adapter.Echo() != nil)
// true
echoweb.Adapter.Router

Router returns the app-facing router contract.

adapter := echoweb.New()
fmt.Println(adapter.Router() != nil)
// true
echoweb.Adapter.ServeHTTP

ServeHTTP 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)
// 204
echoweb.New

New creates a new Echo-backed web adapter.

adapter := echoweb.New()
fmt.Println(adapter.Router() != nil, adapter.Echo() != nil)
// true true
echoweb.NewServer

NewServer 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 true
echoweb.Server.Router

Router exposes the app-facing router contract.

server, _ := echoweb.NewServer(echoweb.ServerConfig{})
fmt.Println(server.Router() != nil)
// true
echoweb.Server.Serve

Serve 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)
// true
echoweb.Server.ServeHTTP

ServeHTTP 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)
// 204
echoweb.UnwrapContext

UnwrapContext 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)
// true
echoweb.UnwrapWebSocketConn

UnwrapWebSocketConn returns the underlying gorilla websocket connection.

_, ok := echoweb.UnwrapWebSocketConn(nil)
fmt.Println(ok)
// false
echoweb.Wrap

Wrap exposes an existing Echo engine through the web.Router contract.

adapter := echoweb.Wrap(nil)
fmt.Println(adapter.Echo() != nil)
// true

Indexing

webindex.Run

Run 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 true
webindex.RunCached

RunCached indexes API metadata while reusing a content-validated analysis cache at cachePath. Relative cache paths resolve from opts.Root. An empty path behaves like Run. When the active build cannot be fingerprinted safely, RunCached falls back to a full run without persisting state.

Auth Middleware

webmiddleware.BasicAuth

BasicAuth 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")
})
webmiddleware.BasicAuthWithConfig

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")
})
webmiddleware.CSRF

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)
})
webmiddleware.CSRFWithConfig

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)
})
webmiddleware.CreateExtractors

CreateExtractors creates extractors from a lookup definition.

extractors, err := webmiddleware.CreateExtractors("header:X-API-Key,query:token")
fmt.Println(err == nil, len(extractors))
// true 2
webmiddleware.KeyAuth

KeyAuth 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})
})
webmiddleware.KeyAuthWithConfig

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})
})

Compression Middleware

webmiddleware.Compress

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")
})
webmiddleware.Decompress

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)})
})
webmiddleware.DecompressWithConfig

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)
})
webmiddleware.Gzip

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())
webmiddleware.GzipWithConfig

GzipWithConfig enables gzip response compression with custom options.

router := echoweb.New().Router()

router.Use(webmiddleware.GzipWithConfig(webmiddleware.GzipConfig{
	MinLength: 1024,
}))

Method Override Middleware

webmiddleware.MethodFromForm

MethodFromForm gets an override method from a form field.

router := echoweb.New().Router()

router.Pre(webmiddleware.MethodOverrideWithConfig(webmiddleware.MethodOverrideConfig{
	Getter: webmiddleware.MethodFromForm("_method"),
}))
webmiddleware.MethodFromHeader

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"),
}))
webmiddleware.MethodFromQuery

MethodFromQuery gets an override method from a query parameter.

router := echoweb.New().Router()

router.Pre(webmiddleware.MethodOverrideWithConfig(webmiddleware.MethodOverrideConfig{
	Getter: webmiddleware.MethodFromQuery("_method"),
}))
webmiddleware.MethodOverride

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)
})
webmiddleware.MethodOverrideWithConfig

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)
})

Path Rewriting Middleware

webmiddleware.AddTrailingSlash

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")
})
webmiddleware.AddTrailingSlashWithConfig

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")
})
webmiddleware.RemoveTrailingSlash

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")
})
webmiddleware.RemoveTrailingSlashWithConfig

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")
})
webmiddleware.Rewrite

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"))
})
webmiddleware.RewriteWithConfig

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"))
})

Payloads Middleware

webmiddleware.BodyDump

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})
})
webmiddleware.BodyDumpWithConfig

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))
	},
}))
webmiddleware.BodyLimit

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)
})
webmiddleware.BodyLimitWithConfig

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)
})
webmiddleware.ErrorBodyDump

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")
})
webmiddleware.ErrorBodyDumpWithConfig

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)
	},
}))

Proxying Middleware

webmiddleware.NewRandomBalancer

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:8080
webmiddleware.NewRoundRobinBalancer

NewRoundRobinBalancer 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:8080
webmiddleware.Proxy

Proxy 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))
webmiddleware.ProxyWithConfig

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",
	},
}))

Rate Limiting Middleware

webmiddleware.NewRateLimiterMemoryStore

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 false
webmiddleware.NewRateLimiterMemoryStoreWithConfig

NewRateLimiterMemoryStoreWithConfig 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)
// true
webmiddleware.RateLimiter

RateLimiter 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)
})
webmiddleware.RateLimiterMemoryStore.Allow

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 true
webmiddleware.RateLimiterWithConfig

RateLimiterWithConfig 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
	},
}))

Redirects Middleware

webmiddleware.HTTPSNonWWWRedirect

HTTPSNonWWWRedirect redirects to https without www.

router := echoweb.New().Router()
router.Use(webmiddleware.HTTPSNonWWWRedirect())
webmiddleware.HTTPSNonWWWRedirectWithConfig

HTTPSNonWWWRedirectWithConfig returns HTTPS non-WWW redirect middleware with config.

router := echoweb.New().Router()

router.Use(webmiddleware.HTTPSNonWWWRedirectWithConfig(webmiddleware.RedirectConfig{
	Code: 307,
}))
webmiddleware.HTTPSRedirect

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")
})
webmiddleware.HTTPSRedirectWithConfig

HTTPSRedirectWithConfig returns HTTPS redirect middleware with config.

router := echoweb.New().Router()

router.Use(webmiddleware.HTTPSRedirectWithConfig(webmiddleware.RedirectConfig{
	Code: 307,
}))
webmiddleware.HTTPSWWWRedirect

HTTPSWWWRedirect redirects to https + www.

router := echoweb.New().Router()
router.Use(webmiddleware.HTTPSWWWRedirect())
webmiddleware.HTTPSWWWRedirectWithConfig

HTTPSWWWRedirectWithConfig returns HTTPS+WWW redirect middleware with config.

router := echoweb.New().Router()

router.Use(webmiddleware.HTTPSWWWRedirectWithConfig(webmiddleware.RedirectConfig{
	Code: 307,
}))
webmiddleware.NonWWWRedirect

NonWWWRedirect redirects to the non-www host.

router := echoweb.New().Router()
router.Use(webmiddleware.NonWWWRedirect())
webmiddleware.NonWWWRedirectWithConfig

NonWWWRedirectWithConfig returns non-WWW redirect middleware with config.

router := echoweb.New().Router()

router.Use(webmiddleware.NonWWWRedirectWithConfig(webmiddleware.RedirectConfig{
	Code: 307,
}))
webmiddleware.WWWRedirect

WWWRedirect redirects to the www host.

router := echoweb.New().Router()
router.Use(webmiddleware.WWWRedirect())
webmiddleware.WWWRedirectWithConfig

WWWRedirectWithConfig returns WWW redirect middleware with config.

router := echoweb.New().Router()

router.Use(webmiddleware.WWWRedirectWithConfig(webmiddleware.RedirectConfig{
	Code: 307,
}))

Reliability Middleware

webmiddleware.Recover

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")
})
webmiddleware.RecoverWithConfig

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"})
	},
}))

Request Lifecycle Middleware

webmiddleware.ContextTimeout

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})
})
webmiddleware.ContextTimeoutWithConfig

ContextTimeoutWithConfig sets a timeout on the request context with config.

router := echoweb.New().Router()

router.Use(webmiddleware.ContextTimeoutWithConfig(webmiddleware.ContextTimeoutConfig{
	Timeout: time.Second,
}))
webmiddleware.DefaultSkipper

DefaultSkipper always runs the middleware.

fmt.Println(webmiddleware.DefaultSkipper(nil))
// false
webmiddleware.RequestID

RequestID 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"),
	})
})
webmiddleware.RequestIDWithConfig

RequestIDWithConfig returns RequestID middleware with config.

router := echoweb.New().Router()

router.Use(webmiddleware.RequestIDWithConfig(webmiddleware.RequestIDConfig{
	TargetHeader: "X-Correlation-ID",
	ContextKey:   "correlation_id",
}))
webmiddleware.RequestLoggerWithConfig

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)
})
webmiddleware.Timeout

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)
})
webmiddleware.TimeoutWithConfig

TimeoutWithConfig returns a response-timeout middleware with config. Timed work may run on an isolated native adapter context; request state that must cross the timeout boundary should use web.Context.Set and web.Context.Get.

router := echoweb.New().Router()

router.Use(webmiddleware.TimeoutWithConfig(webmiddleware.TimeoutConfig{
	Timeout:      time.Second,
	ErrorMessage: "request timed out",
}))

Security Middleware

webmiddleware.CORS

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})
})
webmiddleware.CORSWithConfig

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})
})
webmiddleware.Secure

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")
})
webmiddleware.SecureWithConfig

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 Files Middleware

webmiddleware.Static

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)
})
webmiddleware.StaticWithConfig

StaticWithConfig serves static content using config.

router := echoweb.New().Router()

router.Use(webmiddleware.StaticWithConfig(webmiddleware.StaticConfig{
	Root:  "public",
	HTML5: true,
}))

Prometheus

webprometheus.Default

Default returns the package-level Prometheus metrics instance.

fmt.Println(webprometheus.Default() == webprometheus.Default())
// true
webprometheus.Handler

Handler 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"))
// true
webprometheus.Metrics.Handler

Handler 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"))
// true
webprometheus.Metrics.Middleware

Middleware 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"))
// true
webprometheus.Middleware

Middleware 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"))
// true
webprometheus.MustNew

MustNew creates a Metrics instance and panics on registration errors.

metrics := webprometheus.MustNew(webprometheus.Config{Registerer: prometheus.NewRegistry(), Gatherer: prometheus.NewRegistry()})
fmt.Println(metrics != nil)
// true
webprometheus.New

New creates a Metrics instance backed by Prometheus collectors.

metrics, err := webprometheus.New(webprometheus.Config{Namespace: "app"})
_ = metrics
fmt.Println(err == nil)
// true
webprometheus.RunPushGatewayGatherer

RunPushGatewayGatherer starts pushing collected metrics until the context finishes.

err := webprometheus.RunPushGatewayGatherer(context.Background(), webprometheus.PushGatewayConfig{})
fmt.Println(err != nil)
// true
webprometheus.WriteGatheredMetrics

WriteGatheredMetrics gathers collected metrics and writes them to the given writer.

var buf bytes.Buffer
err := webprometheus.WriteGatheredMetrics(&buf, prometheus.NewRegistry())
fmt.Println(err == nil)
// true

Route Reporting

BuildRouteEntries

BuildRouteEntries 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 GET
RenderRouteTable

RenderRouteTable 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"))
// true

Routing

MountRouter

MountRouter 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)
// true
NewRoute

NewRoute 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 /healthz
NewRouteGroup

NewRouteGroup 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 1
NewWebSocketRoute

NewWebSocketRoute 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())
// true
RegisterRoutes

RegisterRoutes 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)
// true
Route.Handler

Handler 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())
// 201
Route.HandlerName

HandlerName 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() != "")
// true
Route.IsWebSocket

IsWebSocket 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())
// true
Route.Method

Method returns the HTTP method.

route := web.NewRoute(http.MethodPost, "/users", func(c web.Context) error { return nil })
fmt.Println(route.Method())
// POST
Route.MiddlewareNames

MiddlewareNames 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])
// auth
Route.Middlewares

Middlewares 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()))
// 1
Route.Path

Path returns the path of the route.

route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil })
fmt.Println(route.Path())
// /healthz
Route.WebSocketHandler

WebSocketHandler 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 true
Route.WithMiddlewareNames

WithMiddlewareNames 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()))
// 2
RouteGroup.MiddlewareNames

MiddlewareNames returns original middleware names for route reporting.

group := web.NewRouteGroup("/api", nil).WithMiddlewareNames("auth")
fmt.Println(group.MiddlewareNames()[0])
// auth
RouteGroup.Middlewares

Middlewares 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()))
// 1
RouteGroup.RoutePrefix

RoutePrefix returns the group prefix.

group := web.NewRouteGroup("/api", nil)
fmt.Println(group.RoutePrefix())
// /api
RouteGroup.Routes

Routes 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()))
// 1
RouteGroup.WithMiddlewareNames

WithMiddlewareNames attaches reporting-only middleware names to the group.

group := web.NewRouteGroup("/api", nil).WithMiddlewareNames("auth", "trace")
fmt.Println(len(group.MiddlewareNames()))
// 2

Testing

webtest.NewContext

NewContext 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

Documentation

Overview

Package web provides the app-facing HTTP contracts used by GoForj apps and sibling packages.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BindContext

func BindContext(target Context, ctx context.Context)

BindContext attaches ctx to the request-scoped web.Context without forcing callers to replace the underlying *http.Request when the adapter can carry an override more cheaply.

func MountRouter

func MountRouter(router Router, mounts []RouterMount) error

MountRouter applies mount-style router configuration in declaration order. @group Routing Example: 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)

// true

func RawRequest

func RawRequest(target Context) *http.Request

RawRequest returns the adapter's underlying request when available, without forcing a context-rebound clone. Callers should prefer Context() for scoped execution state and use RawRequest only when they need direct request body or header access.

func RegisterRoutes

func RegisterRoutes(router Router, groups []RouteGroup) error

RegisterRoutes registers route groups onto a router. @group Routing Example: 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)

// true

func RenderRouteTable

func RenderRouteTable(entries []RouteEntry) string

RenderRouteTable renders a route table using simple ASCII borders and ANSI colors. @group Route Reporting Example:

table := web.RenderRouteTable([]web.RouteEntry{{
	Path:    "/api/healthz",
	Handler: "monitoring.Healthz",
	Methods: []string{"GET"},
}})

fmt.Println(strings.Contains(table, "/api/healthz"))

// true

Types

type Context

type Context interface {
	Context() context.Context
	Method() string
	Path() string
	URI() string
	Scheme() string
	Host() string
	Param(name string) string
	Query(name string) string
	Header(name string) string
	Cookie(name string) (*http.Cookie, error)
	RealIP() string
	Request() *http.Request
	SetRequest(request *http.Request)
	Response() Response
	ResponseWriter() http.ResponseWriter
	SetResponseWriter(writer http.ResponseWriter)
	Bind(target any) error
	Set(key string, value any)
	Get(key string) any
	AddHeader(name string, value string)
	SetHeader(name string, value string)
	SetCookie(cookie *http.Cookie)
	JSON(code int, payload any) error
	Blob(code int, contentType string, body []byte) error
	File(path string) error
	Text(code int, body string) error
	HTML(code int, body string) error
	NoContent(code int) error
	Redirect(code int, url string) error
	StatusCode() int
	Native() any
}

Context is the app-facing HTTP context contract.

A Context belongs to one handler invocation. Its request, response, and key/value methods must not be called concurrently or retained after the handler returns. The Context method returns the cancellation-safe standard library context for work that needs to cross goroutine boundaries.

type Handler

type Handler func(Context) error

Handler is the app-facing HTTP handler contract.

type Middleware

type Middleware func(Handler) Handler

Middleware wraps a handler with request/response behavior.

type Response

type Response interface {
	Header() http.Header
	Writer() http.ResponseWriter
	SetWriter(writer http.ResponseWriter)
	StatusCode() int
	Size() int64
	Committed() bool
	Native() any
}

Response is the app-facing HTTP response contract.

type Route

type Route struct {
	// contains filtered or unexported fields
}

Route represents a single route in the application.

func NewRoute

func NewRoute(
	method string,
	route string,
	handler Handler,
	middlewares ...Middleware,
) Route

NewRoute creates a new route using the app-facing web handler contract directly. @group Routing Example:

route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error {
	return c.NoContent(http.StatusOK)
})

fmt.Println(route.Method(), route.Path())

// GET /healthz

func NewWebSocketRoute

func NewWebSocketRoute(
	route string,
	handler WebSocketHandler,
	middlewares ...Middleware,
) Route

NewWebSocketRoute creates a websocket route using the app-facing websocket handler contract. @group Routing Example:

route := web.NewWebSocketRoute("/ws", func(c web.Context, conn web.WebSocketConn) error {
	return nil
})

fmt.Println(route.IsWebSocket())

// true

func (*Route) Handler

func (r *Route) Handler() Handler

Handler returns the route handler. @group Routing Example:

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

// 201

func (*Route) HandlerName

func (r *Route) HandlerName() string

HandlerName returns the original handler name for route reporting. @group Routing Example: route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil }) fmt.Println(route.HandlerName() != "")

// true

func (*Route) IsWebSocket

func (r *Route) IsWebSocket() bool

IsWebSocket reports whether this route upgrades to a websocket connection. @group Routing Example: route := web.NewWebSocketRoute("/ws", func(c web.Context, conn web.WebSocketConn) error { return nil }) fmt.Println(route.IsWebSocket())

// true

func (*Route) Method

func (r *Route) Method() string

Method returns the HTTP method. @group Routing Example: route := web.NewRoute(http.MethodPost, "/users", func(c web.Context) error { return nil }) fmt.Println(route.Method())

// POST

func (*Route) MiddlewareNames

func (r *Route) MiddlewareNames() []string

MiddlewareNames returns original middleware names for route reporting. @group Routing Example: route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil }).WithMiddlewareNames("auth") fmt.Println(route.MiddlewareNames()[0])

// auth

func (*Route) Middlewares

func (r *Route) Middlewares() []Middleware

Middlewares returns the route middleware slice. @group Routing Example: 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()))

// 1

func (*Route) Path

func (r *Route) Path() string

Path returns the path of the route. @group Routing Example: route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil }) fmt.Println(route.Path())

// /healthz

func (*Route) WebSocketHandler

func (r *Route) WebSocketHandler() WebSocketHandler

WebSocketHandler returns the websocket route handler. @group Routing Example:

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 true

func (Route) WithMiddlewareNames

func (r Route) WithMiddlewareNames(names ...string) Route

WithMiddlewareNames attaches reporting-only middleware names to the route. @group Routing Example: route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil }).WithMiddlewareNames("auth", "trace") fmt.Println(len(route.MiddlewareNames()))

// 2

type RouteEntry

type RouteEntry struct {
	Path        string   `json:"path"`
	Handler     string   `json:"handler"`
	Methods     []string `json:"methods"`
	Middlewares []string `json:"middlewares"`
}

RouteEntry represents a single route entry in the list and JSON responses.

func BuildRouteEntries

func BuildRouteEntries(groups []RouteGroup, extra ...RouteEntry) []RouteEntry

BuildRouteEntries builds a sorted slice of route entries from registered groups and extra entries. @group Route Reporting Example:

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 GET

type RouteGroup

type RouteGroup struct {
	// contains filtered or unexported fields
}

RouteGroup represents a group of routes.

func NewRouteGroup

func NewRouteGroup(
	prefix string,
	routes []Route,
	middlewares ...Middleware,
) RouteGroup

NewRouteGroup wraps routes and their accompanied web middleware. @group Routing Example:

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 1

func (*RouteGroup) MiddlewareNames

func (g *RouteGroup) MiddlewareNames() []string

MiddlewareNames returns original middleware names for route reporting. @group Routing Example: group := web.NewRouteGroup("/api", nil).WithMiddlewareNames("auth") fmt.Println(group.MiddlewareNames()[0])

// auth

func (*RouteGroup) Middlewares

func (g *RouteGroup) Middlewares() []Middleware

Middlewares returns the middleware slice for the group. @group Routing Example: group := web.NewRouteGroup("/api", nil, func(next web.Handler) web.Handler { return next }) fmt.Println(len(group.Middlewares()))

// 1

func (*RouteGroup) RoutePrefix

func (g *RouteGroup) RoutePrefix() string

RoutePrefix returns the group prefix. @group Routing Example: group := web.NewRouteGroup("/api", nil) fmt.Println(group.RoutePrefix())

// /api

func (*RouteGroup) Routes

func (g *RouteGroup) Routes() []Route

Routes returns the routes in the group. @group Routing Example:

group := web.NewRouteGroup("/api", []web.Route{
	web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil }),
})

fmt.Println(len(group.Routes()))

// 1

func (RouteGroup) WithMiddlewareNames

func (g RouteGroup) WithMiddlewareNames(names ...string) RouteGroup

WithMiddlewareNames attaches reporting-only middleware names to the group. @group Routing Example: group := web.NewRouteGroup("/api", nil).WithMiddlewareNames("auth", "trace") fmt.Println(len(group.MiddlewareNames()))

// 2

type Router

type Router interface {
	Pre(...Middleware)
	Use(...Middleware)
	Handle(method string, path string, handler Handler, middleware ...Middleware) error
	CONNECT(path string, handler Handler, middleware ...Middleware)
	DELETE(path string, handler Handler, middleware ...Middleware)
	GET(path string, handler Handler, middleware ...Middleware)
	GETWS(path string, handler WebSocketHandler, middleware ...Middleware)
	HEAD(path string, handler Handler, middleware ...Middleware)
	OPTIONS(path string, handler Handler, middleware ...Middleware)
	PATCH(path string, handler Handler, middleware ...Middleware)
	POST(path string, handler Handler, middleware ...Middleware)
	PUT(path string, handler Handler, middleware ...Middleware)
	TRACE(path string, handler Handler, middleware ...Middleware)
	Any(path string, handler Handler, middleware ...Middleware)
	Match(methods []string, path string, handler Handler, middleware ...Middleware)
	Group(prefix string, middleware ...Middleware) Router
}

Router is the app-facing route registration contract.

type RouterMount

type RouterMount func(Router) error

RouterMount configures a router with mount-style behavior such as SPA/static surfaces, adapter-level middleware, or other non-route-group setup.

type WebSocketConn

type WebSocketConn interface {
	ReadJSON(target any) error
	WriteJSON(payload any) error
	Close() error
	Native() any
}

WebSocketConn is the app-facing websocket connection contract.

type WebSocketHandler

type WebSocketHandler func(Context, WebSocketConn) error

WebSocketHandler handles an upgraded websocket connection.

Directories

Path Synopsis
adapter
echoweb
Package echoweb hosts the first adapter from github.com/goforj/web to Echo.
Package echoweb hosts the first adapter from github.com/goforj/web to Echo.
Package webindex analyzes GoForj route declarations and emits route and OpenAPI metadata.
Package webindex analyzes GoForj route declarations and emits route and OpenAPI metadata.
testfixture/routemutation/app
Package app defines the route-composition boundary used by runtime-mutation parity tests.
Package app defines the route-composition boundary used by runtime-mutation parity tests.
testfixture/routemutation/controllers
Package controllers provides route mutation fixtures whose runtime behavior must match indexed output.
Package controllers provides route mutation fixtures whose runtime behavior must match indexed output.
testfixture/routeparity/app
Package app composes the exact route groups used by the runtime parity fixture.
Package app composes the exact route groups used by the runtime parity fixture.
testfixture/routeparity/controllers
Package controllers provides route providers used to compare runtime registration with source indexing.
Package controllers provides route providers used to compare runtime registration with source indexing.
Package webmiddleware provides reusable web.Middleware helpers for common HTTP concerns such as auth, CORS, compression, timeouts, and request shaping.
Package webmiddleware provides reusable web.Middleware helpers for common HTTP concerns such as auth, CORS, compression, timeouts, and request shaping.
Package webprometheus provides Prometheus middleware and scrape handlers for web-based GoForj apps.
Package webprometheus provides Prometheus middleware and scrape handlers for web-based GoForj apps.
Package webtest provides lightweight test helpers for web handlers and middleware.
Package webtest provides lightweight test helpers for web handlers and middleware.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL