Skip to content

Application Init (@MoostInit) ​

@MoostInit() marks a controller method to run once at boot — after Moost has bound every controller (so the complete getControllersOverview() is available) and before any adapter starts serving. It is the decorator-idiomatic seam for one-time, post-wiring setup: warm a cache, validate configuration, or derive a value from the final route table.

This is application init, not the per-event lifecycle. There is no request, no headers, no route params — just the wired app and dependency injection.

Quick start ​

ts
import { Moost, Controller, MoostInit, InjectMoost } from 'moost'

@Controller('api/auth')
class AuthController {
  @MoostInit()
  init(@InjectMoost() moost: Moost) {
    // The overview is COMPLETE here — every controller and its mounted routes.
    const overview = moost.getControllersOverview()
    moost.getLogger('auth').log(`wired ${overview.length} controllers`)
  }
}

Why not the constructor? ​

A singleton controller's constructor runs during bind, before its own entry is pushed to the overview. So a constructor that calls getControllersOverview() sees an incomplete table (missing its own controller and everything bound after it). @MoostInit runs after all controllers are bound, so the overview is always complete.

The classic case: an integration must scope something to a controller's actual mounted path. Mount prefixes compose at bind time (registerControllers(['api/auth', AuthController]) → /api/auth/refresh), so the path can only be read from the final, composed route table. Use the @HandlerPaths param decorator instead of walking the overview by hand:

ts
@Controller('auth')
class AuthController {
  constructor(private readonly holder: RefreshPathHolder) {}

  @Get('refresh')
  refresh() { /* ... */ }

  @MoostInit()
  resolveRefreshPath(@HandlerPaths('refresh') paths: string[], @InjectMoost() moost: Moost) {
    if (paths.length) this.holder.value = paths[0]    // '/api/auth/refresh', composed
    else moost.getLogger('auth').warn('refresh route not found') // warns at BOOT, not first request
  }
}

Accessing the app ​

Both inject the running Moost instance:

  • @InjectMoost() — a parameter decorator on the init method (shown above).
  • Constructor injection — Moost is in the provide registry, so constructor(private moost: Moost) {} works on any singleton controller.

Programmatic hooks (app.addInitHook) ​

app.addInitHook(fn, { priority? }) is the programmatic counterpart of @MoostInit, for code that cannot decorate a controller method — a FOR_EVENT controller, a class that is only injected, or library code that receives the app through DI.

ts
@Injectable()
class Publisher {
  constructor(app: Moost) {
    app.addInitHook((moost) => {
      moost.getLogger('publisher').log(`wired ${moost.getControllersOverview().length} controllers`)
    }, { priority: -10 })
  }
}
  • Same pass as @MoostInit. Hooks run after all controllers are bound and before adapter.onInit, in ascending priority (default 0), then registration order, in the same init context (controller context is the app; no request composables). fn receives the app. A throwing hook rejects init().
  • Before the pass (including from a controller constructor during bind) the hook is queued.
  • During the pass (from another hook) it runs later in the same pass, ordered among the hooks that have not run yet.
  • After the pass it runs once, immediately and asynchronously, and is not stored. A failure is logged through the app logger; there is no init() left to reject. This is the case for FOR_EVENT controllers (constructed per request) and lazily created singletons.
  • No de-duplication. Registering the same function twice runs it twice. A per-request caller (e.g. a FOR_EVENT constructor) runs its hook on every construction, so guard non-idempotent work with your own flag.
  • Re-init. Hooks registered before or during a pass run again on every init() of the same app, like @MoostInit hooks. Late hooks do not: to have a hook run on every future init, register it before init().

Resolving a handler path ​

Deriving a handler's actual mounted path is common enough that Moost ships a helper for it, so you don't navigate getControllersOverview() by hand (and don't trip over the multiplicities below). Three forms, smallest first:

ts
import { getHandlerPaths, useHandlerPaths, HandlerPaths } from 'moost'

// 1. Param decorator — inject directly into a @MoostInit method
@MoostInit()
init(@HandlerPaths('refresh') paths: string[]) { /* paths = ['/api/auth/refresh'] */ }

// 2. Composable — inside the method body; defaults the controller to the current one
@MoostInit()
async init() { const paths = await useHandlerPaths('refresh') }

// 3. Pure function — explicit controller + method, callable anywhere with a Moost ref
@MoostInit()
init(@InjectMoost() moost: Moost) { const paths = getHandlerPaths(moost, AuthController, 'refresh') }

All three return all distinct mounted paths (string[]), because a single method can resolve to more than one path:

  • the controller is mounted at multiple prefixes (@ImportController in several places);
  • the method carries multiple verbs (@Get + @Post);
  • a handler registers under multiple paths.

Pass opts.predicate to narrow by event-specific criteria without coupling core to a transport — e.g. only the GET registration: getHandlerPaths(moost, AuthController, 'refresh', { predicate: (h) => (h.handler as { method: string }).method === 'GET' }). Use opts.type (e.g. 'HTTP') to restrict by event type. Returns [] when nothing matches — check it and warn at boot.

Semantics ​

ConcernBehavior
WhenAfter all controllers are bound, before the adapter.onInit loop. The full overview is guaranteed complete.
How oftenExactly once per init() — per app, not per instance. A singleton kept across a dev-server reload runs its hooks again on the same instance for the new app (what a reload keeps).
ScopeSINGLETON controllers only — classes registered with registerControllers or @ImportController. A hook on a class that is only injected somewhere never runs. @MoostInit on a FOR_EVENT controller throws at bind (there is no init-time instance).
OrderingAscending priority (default 0), then registration order — same mental model as interceptor priority. Lower runs first.
ArgumentsResolved through the resolve pipe only — @InjectMoost, @Const, and other @Resolve-based params work. @Inject does not (it resolves constructor parameters only; on a method param it yields undefined) — inject into the constructor instead.
InterceptorsNot applied. Guards/auth/error interceptors are request concerns; init hooks call the method directly.
Transform/Validate pipesNot applied — only the resolve pipe runs. Injected values are not validated or transformed.
AsyncEach hook is awaited.
ErrorsFail-fast: a throwing hook rejects init(). A broken one-time setup is a boot-time misconfiguration you want loud.
vs adapter.onInit@MoostInit and addInitHook hooks run before adapter onInit, so an adapter's onInit can observe state an init hook produced.
vs addInitHookaddInitHook is the programmatic form: works from any code holding the app (no decorated controller needed) and also accepts late registration (runs once immediately, not stored, so not re-run on re-init).
ts
@MoostInit({ priority: -10 }) // runs before default-priority hooks
earlySetup() {}

DOs and DON'Ts ​

  • DO use it for one-time setup that depends on the complete overview — deriving paths, warming caches, validating config.
  • DO put the result somewhere request handlers can read it (an injectable holder), rather than recomputing per request.
  • DO make a hook idempotent when its singleton can outlive a dev-server reload (it has no Moost dependency) — it runs again for every new app.
  • DO use app.addInitHook in library code that cannot decorate a controller (FOR_EVENT controllers, injected-only classes).
  • DON'T put @MoostInit on a class that is only injected — hooks are collected from registered controllers; register the class or move the hook to one.
  • DON'T call request-scoped composables (useRequest, useHeaders, useRouteParams, useCookies) — there is no event at init; they will fail.
  • DON'T put @MoostInit on a FOR_EVENT controller — it throws at bind. Use a SINGLETON.
  • DON'T rely on it for per-request logic — it runs once at boot, not per event.

See also ​

  • Application Dispose — @MoostDispose, the teardown counterpart: release what a singleton owns on shutdown or dev-server reload.
  • Event Lifecycle — the per-event flow (@MoostInit is the boot-time counterpart).
  • Controllers — getControllersOverview() and how controllers are registered.
  • Dependency Injection — how @InjectMoost and constructor injection resolve.

Released under the MIT License.