Skip to content

Latest commit

 

History

History
1672 lines (1292 loc) · 55.2 KB

File metadata and controls

1672 lines (1292 loc) · 55.2 KB

Minimal app-facing HTTP abstractions, middleware, adapters, and route indexing for GoForj.

Go Reference CI Go version Latest tag Go Report Card Codecov Unit tests (executed count)

web coverage adapter/echoweb coverage webindex coverage webmiddleware coverage webprometheus coverage webtest coverage

web is built on top of Echo, which is a fantastic HTTP framework with a fast router, strong middleware story, and a mature ecosystem. GoForj wraps it so applications can code against a smaller app-facing contract while still getting a high-quality underlying engine, reusable middleware packages, testing helpers, route indexing, and framework-owned integration points like Prometheus and generated wiring.

Installation

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() {
	// Pick an adapter that satisfies the app-facing web.Router contract.
	adapter := echoweb.New()
	router := adapter.Router()

	// Attach common app middleware once near the top of the stack.
	router.Use(
		webmiddleware.Recover(),
		webmiddleware.RequestID(),
	)

	// Register a basic health route for uptime checks and local smoke tests.
	router.GET("/healthz", func(c web.Context) error {
		return c.Text(http.StatusOK, "ok")
	})

	// Register an actual application route that returns JSON.
	router.GET("/users/:id", func(c web.Context) error {
		return c.JSON(http.StatusOK, map[string]any{
			"id":   c.Param("id"),
			"name": fmt.Sprintf("user-%s", c.Param("id")),
		})
	})

	// Boot the HTTP server with the adapter as the final handler.
	log.Fatal(http.ListenAndServe(":8080", adapter))
}

Common Patterns

Route Groups

routes := []web.Route{
	web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error {
		return c.NoContent(http.StatusOK)
	}),
	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)

adapter := echoweb.New()
_ = web.RegisterRoutes(adapter.Router(), []web.RouteGroup{group})

Compose Middleware Around A Handler

requestID := webmiddleware.RequestID()
recoverer := webmiddleware.Recover()
limiter := webmiddleware.RateLimiter(
	webmiddleware.NewRateLimiterMemoryStore(rate.Every(time.Second)),
)

handler := web.Handler(func(c web.Context) error {
	return c.Text(http.StatusOK, "ok")
})

wrapped := requestID(recoverer(limiter(handler)))
_ = wrapped

Test A Handler End To End

req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
ctx := webtest.NewContext(req, nil, "/healthz", nil)

handler := webmiddleware.RequestID()(func(c web.Context) error {
	return c.Text(http.StatusOK, "ok")
})

_ = handler(ctx)

fmt.Println(ctx.StatusCode())
fmt.Println(ctx.Response().Header().Get("X-Request-ID") != "")
fmt.Println(ctx.ResponseWriter().(*httptest.ResponseRecorder).Body.String())
// 200
// true
// ok

Expose Prometheus Metrics

adapter := echoweb.New()
metrics, _ := webprometheus.New(webprometheus.Config{Namespace: "app"})

adapter.Router().Use(metrics.Middleware())
adapter.Router().GET("/users", func(c web.Context) error {
	return c.NoContent(http.StatusOK)
})
adapter.Router().GET("/metrics", metrics.Handler())

Generate A Route Index

manifest, err := webindex.Run(context.Background(), webindex.IndexOptions{
	Root:    ".",
	OutPath: "webindex.json",
})

fmt.Println(err == nil, manifest.Version != "")
// true true

Packages

  • web: app-facing interfaces, route registration, route reporting helpers
  • adapter/echoweb: Echo-backed adapter and server bootstrap
  • webmiddleware: grouped HTTP middleware for auth, routing, payloads, rate limiting, and more
  • webprometheus: Prometheus middleware and scrape handler
  • webindex: route manifest and OpenAPI index generation
  • webtest: lightweight handler testing context

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

Middleware - Auth

webmiddleware.BasicAuth

BasicAuth returns basic auth middleware.

mw := webmiddleware.BasicAuth(func(user, pass string, c web.Context) (bool, error) {
	return user == "demo" && pass == "secret", nil
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "basic ZGVtbzpzZWNyZXQ=")
ctx := webtest.NewContext(req, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(ctx.StatusCode())
// 204

webmiddleware.BasicAuthWithConfig

BasicAuthWithConfig returns basic auth middleware with config.

mw := webmiddleware.BasicAuthWithConfig(webmiddleware.BasicAuthConfig{
	Realm: "Example",
	Validator: func(user, pass string, c web.Context) (bool, error) { return true, nil },
})
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(ctx.StatusCode(), ctx.Response().Header().Get("WWW-Authenticate"))
// 401 basic realm=\"Example\"

webmiddleware.CSRF

CSRF enables token-based CSRF protection.

ctx := webtest.NewContext(httptest.NewRequest(http.MethodGet, "/", nil), nil, "/", nil)
handler := webmiddleware.CSRF()(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("Set-Cookie") != "")
// true

webmiddleware.CSRFWithConfig

CSRFWithConfig enables token-based CSRF protection with config.

mw := webmiddleware.CSRFWithConfig(webmiddleware.CSRFConfig{CookieName: "_csrf"})
ctx := webtest.NewContext(httptest.NewRequest(http.MethodGet, "/", nil), nil, "/", nil)
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(strings.Contains(ctx.Response().Header().Get("Set-Cookie"), "_csrf="))
// true

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.

mw := webmiddleware.KeyAuth(func(key string, c web.Context) (bool, error) {
	return key == "demo-key", nil
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "Bearer demo-key")
ctx := webtest.NewContext(req, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(ctx.StatusCode())
// 204

webmiddleware.KeyAuthWithConfig

KeyAuthWithConfig returns key auth middleware with config.

mw := webmiddleware.KeyAuthWithConfig(webmiddleware.KeyAuthConfig{
	Validator: func(key string, c web.Context) (bool, error) { return true, nil },
})
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(ctx.StatusCode())
// 400

Middleware - Compression

webmiddleware.Compress

Compress is an alias for Gzip to match the checklist naming.

req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Accept-Encoding", "gzip")
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.Compress()(func(c web.Context) error {
	return c.Text(http.StatusOK, "hello")
})
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("Content-Encoding"))
// gzip

webmiddleware.Decompress

Decompress decompresses gzip-encoded request bodies.

var body string
compressed := &bytes.Buffer{}
gz := gzip.NewWriter(compressed)
_, _ = gz.Write([]byte("hello"))
_ = gz.Close()
req := httptest.NewRequest(http.MethodPost, "/", compressed)
req.Header.Set("Content-Encoding", webmiddleware.GZIPEncoding)
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.Decompress()(func(c web.Context) error {
	data, _ := io.ReadAll(c.Request().Body)
	body = string(data)
	return c.NoContent(http.StatusNoContent)
})
_ = handler(ctx)
fmt.Println(body, ctx.Request().Header.Get("Content-Encoding"))
// hello

webmiddleware.DecompressWithConfig

DecompressWithConfig decompresses gzip-encoded request bodies with config.

req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("plain"))
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.DecompressWithConfig(webmiddleware.DecompressConfig{})(func(c web.Context) error {
	data, _ := io.ReadAll(c.Request().Body)
	fmt.Println(string(data))
	return nil
})
_ = handler(ctx)
// plain

webmiddleware.Gzip

Gzip compresses responses with gzip.

req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Accept-Encoding", "gzip")
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.Gzip()(func(c web.Context) error {
	return c.Text(http.StatusOK, "hello")
})
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("Content-Encoding"))
// gzip

webmiddleware.GzipWithConfig

GzipWithConfig compresses responses with gzip and config.

req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Accept-Encoding", "gzip")
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.GzipWithConfig(webmiddleware.GzipConfig{MinLength: 256})(func(c web.Context) error {
	return c.Text(http.StatusOK, "short")
})
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("Content-Encoding") == "")
// true

Middleware - Method Override

webmiddleware.MethodFromForm

MethodFromForm gets an override method from a form field.

getter := webmiddleware.MethodFromForm("_method")
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("_method=DELETE"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
ctx := webtest.NewContext(req, nil, "/", nil)
fmt.Println(getter(ctx))
// DELETE

webmiddleware.MethodFromHeader

MethodFromHeader gets an override method from a request header.

getter := webmiddleware.MethodFromHeader("X-HTTP-Method-Override")
ctx := webtest.NewContext(nil, nil, "/", nil)
ctx.Request().Header.Set("X-HTTP-Method-Override", "PATCH")
fmt.Println(getter(ctx))
// PATCH

webmiddleware.MethodFromQuery

MethodFromQuery gets an override method from a query parameter.

getter := webmiddleware.MethodFromQuery("_method")
req := httptest.NewRequest(http.MethodPost, "/?_method=PUT", nil)
ctx := webtest.NewContext(req, nil, "/", nil)
fmt.Println(getter(ctx))
// PUT

webmiddleware.MethodOverride

MethodOverride returns method override middleware.

req := httptest.NewRequest(http.MethodPost, "/", nil)
req.Header.Set("X-HTTP-Method-Override", http.MethodPatch)
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.MethodOverride()(func(c web.Context) error {
	fmt.Println(c.Method())
	return nil
})
_ = handler(ctx)
// PATCH

webmiddleware.MethodOverrideWithConfig

MethodOverrideWithConfig returns method override middleware with config.

req := httptest.NewRequest(http.MethodPost, "/?_method=DELETE", nil)
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.MethodOverrideWithConfig(webmiddleware.MethodOverrideConfig{
	Getter: webmiddleware.MethodFromQuery("_method"),
})(func(c web.Context) error {
	fmt.Println(c.Method())
	return nil
})
_ = handler(ctx)
// DELETE

Middleware - Path Rewriting

webmiddleware.AddTrailingSlash

AddTrailingSlash adds a trailing slash to the request path.

req := httptest.NewRequest(http.MethodGet, "/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
handler := webmiddleware.AddTrailingSlash()(func(c web.Context) error {
	fmt.Println(c.Request().URL.Path)
	return nil
})
_ = handler(ctx)
// /docs/

webmiddleware.AddTrailingSlashWithConfig

AddTrailingSlashWithConfig returns trailing-slash middleware with config.

req := httptest.NewRequest(http.MethodGet, "/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
handler := webmiddleware.AddTrailingSlashWithConfig(webmiddleware.TrailingSlashConfig{RedirectCode: 308})(func(c web.Context) error {
	return c.NoContent(204)
})
_ = handler(ctx)
fmt.Println(ctx.StatusCode(), ctx.Response().Header().Get("Location"))
// 308 /docs/

webmiddleware.RemoveTrailingSlash

RemoveTrailingSlash removes the trailing slash from the request path.

req := httptest.NewRequest(http.MethodGet, "/docs/", nil)
ctx := webtest.NewContext(req, nil, "/docs/", nil)
handler := webmiddleware.RemoveTrailingSlash()(func(c web.Context) error {
	fmt.Println(c.Request().URL.Path)
	return nil
})
_ = handler(ctx)
// /docs

webmiddleware.RemoveTrailingSlashWithConfig

RemoveTrailingSlashWithConfig returns remove-trailing-slash middleware with config.

req := httptest.NewRequest(http.MethodGet, "/docs/", nil)
ctx := webtest.NewContext(req, nil, "/docs/", nil)
handler := webmiddleware.RemoveTrailingSlashWithConfig(webmiddleware.TrailingSlashConfig{RedirectCode: 308})(func(c web.Context) error {
	return c.NoContent(204)
})
_ = handler(ctx)
fmt.Println(ctx.StatusCode(), ctx.Response().Header().Get("Location"))
// 308 /docs

webmiddleware.Rewrite

Rewrite rewrites the request path using wildcard rules.

req := httptest.NewRequest(http.MethodGet, "/old/users", nil)
ctx := webtest.NewContext(req, nil, "/old/*", nil)
handler := webmiddleware.Rewrite(map[string]string{"/old/*": "/new/$1"})(func(c web.Context) error {
	fmt.Println(c.Request().URL.Path)
	return nil
})
_ = handler(ctx)
// /new/users

webmiddleware.RewriteWithConfig

RewriteWithConfig rewrites the request path using wildcard and regex rules.

req := httptest.NewRequest(http.MethodGet, "/old/users", nil)
ctx := webtest.NewContext(req, nil, "/old/*", nil)
handler := webmiddleware.RewriteWithConfig(webmiddleware.RewriteConfig{
	Rules: map[string]string{"/old/*": "/v2/$1"},
})(func(c web.Context) error {
	fmt.Println(c.Request().URL.Path)
	return nil
})
_ = handler(ctx)
// /v2/users

Middleware - Payloads

webmiddleware.BodyDump

BodyDump captures request and response payloads.

var captured string
mw := webmiddleware.BodyDump(func(c web.Context, reqBody, resBody []byte) {
	captured = fmt.Sprintf("%s -> %s", string(reqBody), string(resBody))
})
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("ping"))
ctx := webtest.NewContext(req, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.Text(http.StatusOK, "pong") })
_ = handler(ctx)
fmt.Println(captured)
// ping -> pong

webmiddleware.BodyDumpWithConfig

BodyDumpWithConfig captures request and response payloads with config.

mw := webmiddleware.BodyDumpWithConfig(webmiddleware.BodyDumpConfig{
	Handler: func(c web.Context, reqBody, resBody []byte) { fmt.Println(string(resBody)) },
})
ctx := webtest.NewContext(httptest.NewRequest(http.MethodGet, "/", nil), nil, "/", nil)
handler := mw(func(c web.Context) error { return c.Text(http.StatusOK, "ok") })
_ = handler(ctx)
// ok

webmiddleware.BodyLimit

BodyLimit returns middleware that limits request body size.

req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("hello"))
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.BodyLimit("2B")(func(c web.Context) error {
	return c.NoContent(http.StatusOK)
})
_ = handler(ctx)
fmt.Println(ctx.StatusCode())
// 413

webmiddleware.BodyLimitWithConfig

BodyLimitWithConfig returns body limit middleware with config.

req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("ok"))
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.BodyLimitWithConfig(webmiddleware.BodyLimitConfig{Limit: "2KB"})(func(c web.Context) error {
	return c.NoContent(http.StatusNoContent)
})
_ = handler(ctx)
fmt.Println(ctx.StatusCode())
// 204

webmiddleware.ErrorBodyDump

ErrorBodyDump captures response bodies for non-2xx and non-3xx responses.

var captured string
mw := webmiddleware.ErrorBodyDump(func(c web.Context, status int, body []byte) {
	captured = fmt.Sprintf("%d:%s", status, string(body))
})
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.Text(http.StatusBadRequest, "nope") })
_ = handler(ctx)
fmt.Println(captured)
// 400:nope

webmiddleware.ErrorBodyDumpWithConfig

ErrorBodyDumpWithConfig captures response bodies for non-success responses with config.

mw := webmiddleware.ErrorBodyDumpWithConfig(webmiddleware.ErrorBodyDumpConfig{
	Handler: func(c web.Context, status int, body []byte) { fmt.Println(status) },
})
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.Text(http.StatusInternalServerError, "boom") })
_ = handler(ctx)
// 500

Middleware - Proxying

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}})
req := httptest.NewRequest(http.MethodGet, "/", nil)
ctx := webtest.NewContext(req, nil, "/", nil)
_ = webmiddleware.Proxy(balancer)(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.Get("target").(*webmiddleware.ProxyTarget).URL.Host)
// localhost:8080

webmiddleware.ProxyWithConfig

ProxyWithConfig creates a proxy middleware with config.

target, _ := url.Parse("http://localhost:8080")
mw := webmiddleware.ProxyWithConfig(webmiddleware.ProxyConfig{
	Balancer: webmiddleware.NewRandomBalancer([]*webmiddleware.ProxyTarget{{URL: target}}),
})
req := httptest.NewRequest(http.MethodGet, "/old/path", nil)
ctx := webtest.NewContext(req, nil, "/", nil)
_ = mw(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.Get("target").(*webmiddleware.ProxyTarget).URL.Host)
// localhost:8080

Middleware - Rate Limiting

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))
handler := webmiddleware.RateLimiter(store)(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
req1 := httptest.NewRequest(http.MethodGet, "/", nil)
req1.RemoteAddr = "192.0.2.10:1234"
ctx1 := webtest.NewContext(req1, nil, "/", nil)
_ = handler(ctx1)
req2 := httptest.NewRequest(http.MethodGet, "/", nil)
req2.RemoteAddr = "192.0.2.10:1234"
ctx2 := webtest.NewContext(req2, nil, "/", nil)
_ = handler(ctx2)
fmt.Println(ctx1.StatusCode(), ctx2.StatusCode())
// 204 429

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))
mw := webmiddleware.RateLimiterWithConfig(webmiddleware.RateLimiterConfig{Store: store})
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusAccepted) })
_ = handler(ctx)
fmt.Println(ctx.StatusCode())
// 202

Middleware - Redirects

webmiddleware.HTTPSNonWWWRedirect

HTTPSNonWWWRedirect redirects to https without www.

req := httptest.NewRequest(http.MethodGet, "http://www.example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.HTTPSNonWWWRedirect()(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.Response().Header().Get("Location"))
// https://example.com/docs

webmiddleware.HTTPSNonWWWRedirectWithConfig

HTTPSNonWWWRedirectWithConfig returns HTTPS non-WWW redirect middleware with config.

req := httptest.NewRequest(http.MethodGet, "http://www.example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.HTTPSNonWWWRedirectWithConfig(webmiddleware.RedirectConfig{Code: http.StatusTemporaryRedirect})(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.StatusCode())
// 307

webmiddleware.HTTPSRedirect

HTTPSRedirect redirects http requests to https.

req := httptest.NewRequest(http.MethodGet, "http://example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.HTTPSRedirect()(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.StatusCode(), ctx.Response().Header().Get("Location"))
// 301 https://example.com/docs

webmiddleware.HTTPSRedirectWithConfig

HTTPSRedirectWithConfig returns HTTPS redirect middleware with config.

req := httptest.NewRequest(http.MethodGet, "http://example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.HTTPSRedirectWithConfig(webmiddleware.RedirectConfig{Code: http.StatusTemporaryRedirect})(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.StatusCode())
// 307

webmiddleware.HTTPSWWWRedirect

HTTPSWWWRedirect redirects to https + www.

req := httptest.NewRequest(http.MethodGet, "http://example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.HTTPSWWWRedirect()(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.Response().Header().Get("Location"))
// https://www.example.com/docs

webmiddleware.HTTPSWWWRedirectWithConfig

HTTPSWWWRedirectWithConfig returns HTTPS+WWW redirect middleware with config.

req := httptest.NewRequest(http.MethodGet, "http://example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.HTTPSWWWRedirectWithConfig(webmiddleware.RedirectConfig{Code: http.StatusTemporaryRedirect})(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.StatusCode())
// 307

webmiddleware.NonWWWRedirect

NonWWWRedirect redirects to the non-www host.

req := httptest.NewRequest(http.MethodGet, "http://www.example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.NonWWWRedirect()(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.Response().Header().Get("Location"))
// http://example.com/docs

webmiddleware.NonWWWRedirectWithConfig

NonWWWRedirectWithConfig returns non-WWW redirect middleware with config.

req := httptest.NewRequest(http.MethodGet, "http://www.example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.NonWWWRedirectWithConfig(webmiddleware.RedirectConfig{Code: http.StatusTemporaryRedirect})(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.StatusCode())
// 307

webmiddleware.WWWRedirect

WWWRedirect redirects to the www host.

req := httptest.NewRequest(http.MethodGet, "http://example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.WWWRedirect()(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.Response().Header().Get("Location"))
// http://www.example.com/docs

webmiddleware.WWWRedirectWithConfig

WWWRedirectWithConfig returns WWW redirect middleware with config.

req := httptest.NewRequest(http.MethodGet, "http://example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.WWWRedirectWithConfig(webmiddleware.RedirectConfig{Code: http.StatusTemporaryRedirect})(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.StatusCode())
// 307

Middleware - Reliability

webmiddleware.Recover

Recover returns middleware that recovers panics from the handler chain.

ctx := webtest.NewContext(nil, nil, "/", nil)
handler := webmiddleware.Recover()(func(c web.Context) error {
	panic("boom")
})
fmt.Println(handler(ctx) != nil)
// true

webmiddleware.RecoverWithConfig

RecoverWithConfig returns recover middleware with config.

ctx := webtest.NewContext(nil, nil, "/", nil)
handler := webmiddleware.RecoverWithConfig(webmiddleware.RecoverConfig{DisableErrorHandler: true})(func(c web.Context) error {
	panic("boom")
})
fmt.Println(handler(ctx) != nil)
// true

Middleware - Request Lifecycle

webmiddleware.ContextTimeout

ContextTimeout sets a timeout on the request context.

ctx := webtest.NewContext(nil, nil, "/", nil)
handler := webmiddleware.ContextTimeout(2 * time.Second)(func(c web.Context) error {
	fmt.Println(c.Request().Context().Err() == nil)
	return nil
})
_ = handler(ctx)
// true

webmiddleware.ContextTimeoutWithConfig

ContextTimeoutWithConfig sets a timeout on the request context with config.

ctx := webtest.NewContext(nil, nil, "/", nil)
handler := webmiddleware.ContextTimeoutWithConfig(webmiddleware.ContextTimeoutConfig{Timeout: time.Second})(func(c web.Context) error {
	fmt.Println(c.Request().Context().Err() == nil)
	return nil
})
_ = handler(ctx)
// true

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.

mw := webmiddleware.RequestID()
handler := mw(func(c web.Context) error {
	_ = c.Get("request_id")
	return c.NoContent(http.StatusOK)
})
ctx := webtest.NewContext(nil, nil, "/", nil)
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("X-Request-ID") != "")
// true
// true

webmiddleware.RequestIDWithConfig

RequestIDWithConfig returns RequestID middleware with config.

mw := webmiddleware.RequestIDWithConfig(webmiddleware.RequestIDConfig{
	Generator: func() string { return "fixed-id" },
})
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusOK) })
ctx := webtest.NewContext(nil, nil, "/", nil)
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("X-Request-ID"))
// fixed-id

webmiddleware.RequestLoggerWithConfig

RequestLoggerWithConfig returns request logger middleware with config.

var loggedURI string
mw := webmiddleware.RequestLoggerWithConfig(webmiddleware.RequestLoggerConfig{
	LogValuesFunc: func(c web.Context, values webmiddleware.RequestLoggerValues) error {
		loggedURI = values.URI
		return nil
	},
})
req := httptest.NewRequest(http.MethodGet, "/users/42", nil)
ctx := webtest.NewContext(req, nil, "/users/:id", webtest.PathParams{"id": "42"})
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusAccepted) })
_ = handler(ctx)
fmt.Println(loggedURI, ctx.StatusCode())
// /users/42 202

webmiddleware.Timeout

Timeout returns a response-timeout middleware.

ctx := webtest.NewContext(nil, nil, "/", nil)
handler := webmiddleware.Timeout()(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(ctx.StatusCode())
// 204

webmiddleware.TimeoutWithConfig

TimeoutWithConfig returns a response-timeout middleware with config.

ctx := webtest.NewContext(nil, nil, "/", nil)
handler := webmiddleware.TimeoutWithConfig(webmiddleware.TimeoutConfig{Timeout: time.Second})(func(c web.Context) error {
	return c.NoContent(http.StatusAccepted)
})
_ = handler(ctx)
fmt.Println(ctx.StatusCode())
// 202

Middleware - Security

webmiddleware.CORS

CORS returns Cross-Origin Resource Sharing middleware.

req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Origin", "https://example.com")
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.CORS()(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("Access-Control-Allow-Origin"))
// *

webmiddleware.CORSWithConfig

CORSWithConfig returns CORS middleware with config.

mw := webmiddleware.CORSWithConfig(webmiddleware.CORSConfig{AllowOrigins: []string{"https://example.com"}})
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Origin", "https://example.com")
ctx := webtest.NewContext(req, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("Access-Control-Allow-Origin"))
// https://example.com

webmiddleware.Secure

Secure sets security-oriented response headers.

ctx := webtest.NewContext(nil, nil, "/", nil)
handler := webmiddleware.Secure()(func(c web.Context) error { return c.NoContent(http.StatusOK) })
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("X-Frame-Options"))
// SAMEORIGIN

webmiddleware.SecureWithConfig

SecureWithConfig sets security-oriented response headers with config.

ctx := webtest.NewContext(nil, nil, "/", nil)
handler := webmiddleware.SecureWithConfig(webmiddleware.SecureConfig{ReferrerPolicy: "same-origin"})(func(c web.Context) error {
	return c.NoContent(http.StatusOK)
})
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("Referrer-Policy"))
// same-origin

Middleware - Static Files

webmiddleware.Static

Static serves static content from the provided root.

dir, _ := os.MkdirTemp("", "web-static-*")
defer os.RemoveAll(dir)
_ = os.WriteFile(filepath.Join(dir, "hello.txt"), []byte("hello"), 0o644)
req := httptest.NewRequest(http.MethodGet, "/hello.txt", nil)
ctx := webtest.NewContext(req, nil, "/hello.txt", nil)
_ = webmiddleware.Static(dir)(func(c web.Context) error { return c.NoContent(http.StatusNotFound) })(ctx)
fmt.Println(strings.TrimSpace(ctx.ResponseWriter().(*httptest.ResponseRecorder).Body.String()))
// hello

webmiddleware.StaticWithConfig

StaticWithConfig serves static content using config.

dir, _ := os.MkdirTemp("", "web-static-*")
defer os.RemoveAll(dir)
_ = os.WriteFile(filepath.Join(dir, "index.html"), []byte("<h1>home</h1>"), 0o644)
req := httptest.NewRequest(http.MethodGet, "/", nil)
ctx := webtest.NewContext(req, nil, "/", nil)
_ = webmiddleware.StaticWithConfig(webmiddleware.StaticConfig{Root: dir})(func(c web.Context) error { return c.NoContent(http.StatusNotFound) })(ctx)
fmt.Println(strings.TrimSpace(ctx.ResponseWriter().(*httptest.ResponseRecorder).Body.String()))
// <h1>home</h1>

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