Search docs

Jump between documentation pages.

Plugins & encapsulation

Plugins package routes, hooks, and decorators into reusable units. Like Fastify, plugins are encapsulated — what happens inside a plugin stays inside, unless you opt out.

Defining a plugin

A plugin is any object with an optional name and a register(app) function — or a plain function with the same shape. No imports required.

ts
import type { App } from "@daloyjs/core";

export const usersPlugin = {
  name: "users",
  register(app: App) {
    app.use(/* plugin-scoped middleware */);

    app.route({
      method: "GET",
      path: "/me",
      operationId: "me",
      responses: { 200: { description: "current user" } },
      handler: async () => ({ status: 200, body: { user: "alice" } }),
    });
  },
};

Registering a plugin

ts
app.register(usersPlugin, {
  prefix: "/users",
  tags: ["Users"],
  hooks: bearerAuth({ validate: t => t === process.env.TOKEN }),
});

await app.ready();

Decorators

Decorate your app to inject shared resources into every handler's state:

ts
app.decorate("db", await openDatabase());
app.decorate("logger", createLogger({ level: "info" }));

app.route({
  method: "GET",
  path: "/items/:id",
  operationId: "getItem",
  responses: { 200: { description: "ok" } },
  handler: async ({ params, state }) => {
    const row = await state.db.findOne("items", { id: params.id });
    state.logger.info({ id: params.id }, "item fetched");
    return { status: 200, body: row };
  },
});

Why encapsulation matters

  • You can mount the same plugin twice under different prefixes without bleed-through.
  • Third-party plugins can't accidentally rewrite your error handler or hooks.
  • Plugin-internal middleware doesn't apply to sibling routes — predictable order.

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 per register() call, after sync plugins return and after async plugins resolve. The listener receives { name?: string, prefix: string }, where prefix is the effective mounted prefix after nesting. Awaiting app.ready() drains both async plugins and async listeners.
  • app.onShutdown(listener) — fires at the start of app.shutdown(timeoutMs, reason), beforein-flight requests drain. Use this to flush metrics, publish a “draining” signal to a load balancer, or close background pollers. For post-drain cleanup (database pools, file handles), keep using onClose().
ts
app.onPluginInstalled((info) => {
  metrics.counter("plugin.installed", { name: info.name ?? "anonymous", prefix: info.prefix });
});

app.onShutdown(async ({ reason, timeoutMs }) => {
  await loadBalancer.drain({ timeoutMs });
  metrics.counter("app.shutdown", { reason: reason ?? "unknown" });
});

app.onClose(async () => {
  await db.close();
});

app.register(usersPlugin, { prefix: "/users" });
await app.ready();

// later, on SIGTERM
await app.shutdown(10_000, "SIGTERM");

Listener errors are caught and logged via the configured logger so a faulty observer can never block plugin registration or graceful shutdown. Both shutdown() and the underlying onClose chain remain idempotent.