Skip to content

The Rex Interface

wiki edited this page Sep 4, 2026 · 1 revision

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.

Logger()

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.

Container()

The root DI container, typed as rextension.Containernot 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.

EventBus()

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.

The four middleware registrations

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.

RegisterRoute / RegisterRouteToRouter

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.

CreateRouter

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.

What is deliberately absent

  • 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 from PerRouteMiddleware or RouteValidator, 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.

Clone this wiki locally