-
Notifications
You must be signed in to change notification settings - Fork 0
Lifecycle Hooks
type Extension interface {
OnInitialize(ctx context.Context, r Rex) error
OnStart(ctx context.Context, r Rex) error
OnReady(ctx context.Context, r Rex) error
OnStop(ctx context.Context, r Rex) error
OnShutdown(ctx context.Context, r Rex) error
}All five are required. A hook you have nothing to do in returns nil.
rex.New() collects: it builds the container, the event bus and the logger,
applies the options it was given, and returns. It creates no routers, runs no
hooks, binds nothing, and cannot fail.
Run() builds, in this order:
1. freeze the configuration
2. create the default router and any routers declared before Run
3. ── OnInitialize ── extensions declare routes, routers, middleware
4. ── OnStart ── the remaining declarations
5. create routers declared by the hooks
6. attach middleware: global → per-router → per-route
7. register every queued route on its router
8. RouteValidator.ValidateRoutes over the complete table
9. freeze every router's route table
10. bind the listeners, synchronously
11. ── OnReady ── everything is serving
… serving …
12. stop the listeners
13. ── OnStop ──
14. ── OnShutdown ── runs even if OnStop failed
15. close the event bus
Everything that can fail happens inside Run, where the error can be returned.
A hook returning an error aborts startup and Run returns it wrapped with the
hook's name.
Declare. This is where an extension does the bulk of its work:
-
r.CreateRouter(name, cfg)for a dedicated listener -
r.RegisterRoute/r.RegisterRouteToRouter -
r.Use,r.UseOnRouter,r.UsePerRoute,r.UsePerRouter -
r.Container().Singleton(...)to publish something other extensions resolve r.EventBus().Subscribe(...)
The default router and any routers declared before Run already exist, so a
route may be registered onto one by name. Routers created by another
extension's OnInitialize may not exist yet — which is fine: route
registration is queued and the router name is resolved at step 7, so ordering
between extensions does not matter. An unknown name is reported from Run, with
the call site of the registration that named it.
Declare the rest. Same permissions as OnInitialize. It exists as a second
declaration pass for extensions that must observe what everyone else declared
first — OpenAPI and Swagger are the motivating cases: the generator needs the
complete route set, and it must still register its own /openapi.json route
before the tables freeze.
If your extension does not need to run after the others, use OnInitialize.
Observe. Do not declare. By OnReady the tables are frozen and the
listeners are accepting connections. Registering a route or middleware here
returns ErrRouterFrozen — the table is published by atomic swap and
read without a lock, so there is nowhere safe to add to it.
OnReady is for work that must not begin before the application is actually
serving: announcing to a service registry, starting a background poller, logging
the bound addresses.
This is the single most common upgrade break. Extensions used to register routes from
OnReady, which mutated a trie that in-flight requests were already reading. That is now refused rather than raced.
An error from OnReady aborts startup: the application is stopped and Run
returns the error.
The application is shutting down. The listeners are already closed when this runs, so nothing new is arriving — drain, flush, and cancel background work here.
ctx carries the configured ShutdownTimeout (10s by default). Respect it: a
hook that blocks past the deadline delays every hook after it.
The last hook. Release what is left — connections, files, buffers.
It runs even if OnStop returned an error. Skipping it would leak whatever
the remaining hooks were going to release. Errors from both hooks are joined and
returned from Run.
-
The hooks never overlap on one instance. A
Stop()arriving mid-startup waits for startup to finish before any stop hook runs, soOnReadyandOnStopcannot execute concurrently. The wait is bounded by the shutdown timeout, so a hanging startup cannot hang shutdown with it. -
Shutdown runs exactly once.
Stop(),SIGINT/SIGTERMand a cancelled context can all arrive together; the shutdown path is guarded, so hooks run once whichever combination fires. -
Extension order is registration order, and it should not matter. Anything
order-sensitive between two extensions is a design smell — the framework
provides priorities for middleware order and
RouteValidatorfor cross-extension checks precisely so that neither depends on which extension was passed toNewfirst.
rex ships rex.DefaultExtension, which implements all five as no-ops. It is
convenient for an application-local extension, but note that embedding it costs
you the compiler error when the interface grows — an extension published as a
module is better off writing all five out.
type MyExt struct{ rex.DefaultExtension }
func (e *MyExt) OnInitialize(ctx context.Context, r rex.Rex) error { ... }rextension — the Rex extension contract · MIT · © 2026 Kryovyx · pre-1.0, interfaces may change
Building an extension
Contracts
Reference
Ecosystem