-
Notifications
You must be signed in to change notification settings - Fork 0
The Rex Interface
This is everything an extension can do to the framework. It is deliberately
smaller than the framework's own surface — an extension has no Run, no Stop,
no access to the configuration.
type Rex interface {
Logger() Logger
Container() Container
EventBus() event.EventBus
Use(mw Middleware)
UseOnRouter(routerName string, mw Middleware, priority int)
UsePerRoute(f PerRouteMiddleware, priority int)
UsePerRouter(f PerRouterMiddleware, priority int)
RegisterRoute(rt Route) error
RegisterRouteToRouter(rt Route, routerName string) error
CreateRouter(name string, cfg RouterConfig) error
}The rex.Rex value the framework passes to your hooks satisfies this directly —
there is no adapter or wrapper in between.
The application's logger, which is whatever the application configured. Take a field-scoped child rather than logging bare, so operators can filter your extension's output:
log := r.Logger().WithField("extension", "myext")
log.Info("watching %d dependencies", len(deps))The Logger interface is declared in this module, so an extension never imports
rex/logger.
The root DI container, typed as rextension.Container — not dix.Container.
That typing is the reason an extension does not need dix in its go.mod at
all.
Publish things other extensions or handlers should resolve:
r.Container().Singleton(func() *myext.Registry { return newRegistry() })Most extension code wants Resolver, not Container.
Container is for the narrow case of an extension that publishes.
Subscribe from OnInitialize; see Events.
r.EventBus().Subscribe(event.EventTypeRouterRequestHandled, func(e event.Event) {
ev, ok := event.As[event.RouterRequestHandledEvent](e)
if !ok {
return
}
log.Debug("%s %s → %d", ev.Request.Method, ev.RoutePattern, ev.Status)
})Events are dispatched on worker goroutines and may be dropped under saturation. Anything that must be exact belongs in middleware, not in a subscriber.
| Call | Consulted | Attaches to | Use when |
|---|---|---|---|
Use(mw) |
never — the middleware is the value | every router, PriorityDefault
|
the middleware is unconditional and needs no configuration |
UseOnRouter(name, mw, prio) |
never | one named router | operational middleware for a dedicated listener |
UsePerRouter(f, prio) |
once per router, at build | routers where f returns non-nil |
the configuration depends on which router |
UsePerRoute(f, prio) |
once per route, at build | routes where f returns non-nil |
the middleware applies to some routes, or is configured per route |
UsePerRoute is the one most extensions want. See Middleware for the
priority scale and the factory contract.
The router named in UseOnRouter need not exist yet — the name is resolved
when the route tables are built, and an unknown name is reported from Run
rather than silently ignored.
if err := r.RegisterRouteToRouter(
route.New("GET", "/healthz", handler), "health",
); err != nil {
return err
}Both return an error, and it is worth checking: a route with no handler, or one registered after the tables are frozen, is rejected there and would otherwise silently never appear.
The router name is resolved at build time, so registering onto a router another
extension creates later is fine. An unknown name surfaces from Run as
ErrRouterUnknown, wrapped with the method, path and call site of
the registration that named it.
err := r.CreateRouter("metrics", rextension.RouterConfig{Addr: ":9090"})
if err != nil && !errors.Is(err, rextension.ErrRouterExists) {
return err
}Always allow for ErrRouterExists. Two extensions may both want a router
called metrics, and whichever runs second should reuse it rather than abort
startup. This is the standard shape; the sentinel exists so extensions no longer
have to write strings.Contains(err.Error(), "already exists").
See Router Configuration for the config fields — in
particular, a dedicated operational listener usually wants a different
ReadTimeout and body limit than the public one.
-
No
Config(). An extension configures itself through its own options, not by reading the application's configuration. -
No
Routers(). An extension has no business reaching into a built router. If you need to know about routes, take them fromPerRouteMiddlewareorRouteValidator, both of which hand the route table to you at the one moment it is complete. -
No
Run/Stop. Lifecycle belongs to the application.
The full-framework additions — WithOptions, WithExtensions, WithLogger,
Run, RunContext, Stop, Config, Routers, UsePerRouteOn — live on
rex.Rex, which embeds this interface.
rextension — the Rex extension contract · MIT · © 2026 Kryovyx · pre-1.0, interfaces may change
Building an extension
Contracts
Reference
Ecosystem