Plugins & encapsulation
Plugins package routes, hooks, decorators, and lifecycle callbacks into reusable units. Like Fastify, DaloyJS gives each plugin a local scope: app-level hooks and decorators flow inward, while plugin-level hooks and decorators do not leak sideways to sibling plugins.
Defining a plugin
A plugin can be a descriptor object with optional metadata and a register(app) function, or a plain function that receives the child app. Name plugins that manage shared state so DaloyJS can deduplicate them and validate dependencies.
Registering a plugin
app.register() mounts the plugin in a scoped group. The registration config supplies the inherited prefix, tags, hooks, and route auth metadata for every route the plugin adds.
Await app.ready() before starting the server whenever a plugin does async work. Sync plugins also use the same queue for async install observers, so it is safe to call every time.
Dependencies, seeds, and state
Plugin descriptors can declare operational metadata that DaloyJS checks at registration time:
dependencies: prerequisite plugin names that must already be registered.seed: a differentiator for mounting the same named plugin more than once with different configuration.stateful: production guard for plugins that mutate shared state. In production (withsecureDefaultson), an anonymous stateful plugin is refused until you give it a name.
Decorators
Decorate the app to inject shared resources into every handler's state. Decorations added at the root are visible inside plugins. Decorations added inside a plugin stay scoped to that plugin and never leak sideways to sibling plugins or back up to the root. Reusing a key throws unless you pass { override: true } deliberately.
Decorate before you register the routes that read the value: each route captures its scope's decorations when it is registered. Since 1.5.5, the first decorate() on a scope that already has routes throws instead of leaving those handlers reading undefined. That also catches decorating inside a per-request Worker or Lambda wrapper. For a value that changes per request, set it on ctx.state in a preBody hook.
There is no opt-out of that scoping. A decorate() call inside a registered plugin's register(app) is visible only to routes that same plugin registers; routes defined on the root app read undefined. To share a resource such as a database client with every route, decorate the root app: write the setup as a plain function that takes the root app and call it directly instead of through app.register().
Each route binds to its scope's decorations when it is registered, so decorate before registering the routes that read it (the same ordering Fastify requires). Declare root-level decorations before the plugins and groups that depend on them, and a plugin's own decorations before that plugin's routes.
Extension ordering
Plugins that contribute lifecycle hooks can declare ordered extensions. DaloyJS topologically sorts before and after relationships, rejects duplicate extension names, and refuses two extensions that mutate the same response header without an explicit order.
Why encapsulation matters
- You can mount the same plugin twice under different prefixes without hook bleed-through.
- Third-party plugins cannot accidentally rewrite sibling hooks or error handlers.
- Prefixes, tags, hooks, auth, and decorators stay predictable as a service grows.
- Named stateful plugins are deduplicated, and dependencies fail fast when the registration order is wrong.
Lifecycle events
Observability plugins often need to know when other plugins finish installing or when the process starts shutting down. DaloyJS exposes two event hooks for this without polluting the route registry:
app.onPluginInstalled(listener): fires once perregister()call, after sync plugins return and after async plugins resolve. The listener receives{ name?: string, prefix: string }, whereprefixis the effective mounted prefix after nesting. Awaitingapp.ready()drains async plugins and async listeners.app.onShutdown(listener): fires at the start ofapp.shutdown(timeoutMs, reason), before in-flight requests drain. Use this to flush metrics, publish a draining signal to a load balancer, or close background pollers. For post-drain cleanup such as database pools and file handles, keep usingonClose().
Listener errors are caught and logged via the configured logger so a faulty observer does not crash plugin registration or graceful shutdown. Both shutdown() and the underlying onClose chain remain idempotent.