Skip to content

Lifecycle Hooks

wiki edited this page Sep 4, 2026 · 1 revision

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.

Where each hook sits

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.

OnInitialize

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.

OnStart

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.

OnReady

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.

OnStop

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.

OnShutdown

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.

Guarantees you can rely on

  • The hooks never overlap on one instance. A Stop() arriving mid-startup waits for startup to finish before any stop hook runs, so OnReady and OnStop cannot 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/SIGTERM and 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 RouteValidator for cross-extension checks precisely so that neither depends on which extension was passed to New first.

Embedding a no-op base

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 { ... }

Clone this wiki locally