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 injectionMoost is in the provide registry, so constructor(private moost: Moost) {} works on any singleton controller.

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().
ScopeSINGLETON controllers only. @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, @Inject, @Const, and other @Resolve-based params work.
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 runs before adapter onInit, so an adapter's onInit can observe state an init hook produced.
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.
  • 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

  • Event Lifecycle — the per-event flow (@MoostInit is the boot-time counterpart).
  • ControllersgetControllersOverview() and how controllers are registered.
  • Dependency Injection — how @InjectMoost and constructor injection resolve.

Released under the MIT License.