Skip to content

Extensions

wiki edited this page Sep 4, 2026 · 1 revision

Extensions

An extension is a module implementing five lifecycle hooks. It depends on rextension — a contract module with no dependencies — not on rex, so adding one never pulls the framework in twice.

app := rex.New(
	rex.WithConfig(cfg),
	cors.WithCORS(corsCfg),
	security.WithSecurity(secCfg),
	ratelimit.WithRateLimit(ratelimit.WithGlobalLimit(100, time.Minute)),
	validation.WithValidation(nil),
	health.WithHealth(nil),
	metric.WithMetrics(nil),
	openapi.WithOpenAPI(apiCfg),
	swagger.WithSwagger(nil),
)

Argument order does not matter. Middleware order comes from the priority scale.

The ecosystem

Module With… Attaches Wiki
rextension-cors cors.WithCORS(cfg) per-router middleware at PriorityCORS; decorates the router's own OPTIONS answer wiki
rextension-ratelimit ratelimit.WithRateLimit(opts…) per-route middleware at PriorityRateLimit; X-RateLimit-* headers wiki
rextension-security security.WithSecurity(cfg) auth at PriorityAuth, CSRF at PriorityCSRF; publishes the scheme registry; validates the route table wiki
rextension-validation validation.WithValidation(cfg) per-route middleware at PriorityValidation, driven by route body schemas wiki
rextension-health health.WithHealth(cfg) /healthz, /readyz on a dedicated router; dependency gate at PriorityHealthGate wiki
rextension-metric metric.WithMetrics(cfg) /metrics on a dedicated router; instrumentation at PriorityDefault wiki
rextension-openapi openapi.WithOpenAPI(cfg) /openapi.json, generated from the route table in OnStart wiki
rextension-swagger swagger.WithSwagger(cfg) Swagger UI, served from embedded assets wiki

Each also has a New…Extension(cfg) constructor, if you want the extension value rather than an Option. A nil config takes safe defaults everywhere.

How they cooperate without importing each other

Through route interfaces. A route declares a capability; whichever extension cares type-asserts for it. A route that does not implement one is passed through.

type CreateUser struct{ route.Route }

func (r *CreateUser) RequiredSchemes() []string       { return []string{"bearer"} }        // security, openapi, swagger
func (r *CreateUser) RequestBody() rextension.BodySchema { return rextension.Scalar(Req{}) } // validation, openapi
func (r *CreateUser) MaxBodyBytes() int64             { return 1 << 20 }                   // the router

Through the container. The security extension publishes a rextension.SchemeRegistry; OpenAPI and Swagger resolve it, and degrade quietly when it is absent.

Through shared contract types. rextension.OriginPolicy is written once and read by both CORS and CSRF — one allowlist, two consumers.

Never through package-level state. A global registry means two Rex instances in one process clobber each other and state leaks between tests in the same binary.

What extensions may and may not do

They may create routers, register routes, attach middleware, register services in the container, subscribe to events, and validate the complete route table before it freezes.

They may not register routes or middleware from OnReady — the table is frozen by then, and the attempt is refused with ErrRouterFrozen. If you are upgrading and something stopped appearing, this is almost certainly why.

A typical assembled application

func main() {
	cfg := &rex.Config{
		DefaultRouter:   rex.RouterConfig{Addr: ":8080", BaseURL: "/api/v1"},
		ShutdownTimeout: 30 * time.Second,
	}

	origins := rextension.OriginPolicy{
		AllowedOrigins:   []string{"https://app.example.com"},
		AllowCredentials: true,
	}

	app := rex.New(
		rex.WithConfig(cfg),
		cors.WithCORS(cors.NewConfig(cors.WithPolicy(origins))),
		security.WithSecurity(securityConfig(origins)),
		ratelimit.WithRateLimit(ratelimit.WithGlobalLimit(600, time.Minute)),
		validation.WithValidation(nil),
		health.WithHealth(nil),   // :9090
		metric.WithMetrics(nil),  // :9091
		openapi.WithOpenAPI(nil),
		swagger.WithSwagger(nil),
	)

	if err := registerRoutes(app); err != nil {
		log.Fatal(err)
	}
	if err := app.Run(); err != nil {
		log.Fatal(err)
	}
}

The resulting chain on an application route, outermost first:

CORS (200) → rate limit (300) → auth (400) → CSRF (450)
  → validation (500) → health gate (600) → metrics (1000) → handler

Writing your own

See rextension → Writing an extension. The short version:

  • import rextension only — not rex, not dix
  • implement all five hooks, even as no-ops
  • declare in OnInitialize/OnStart, never in OnReady
  • tolerate ErrRouterExists from CreateRouter
  • read route configuration once, at build time, in a PerRouteMiddleware factory
  • answer with problem documents

Clone this wiki locally