Plugins

Extend the RouteCraft runtime with cross-cutting behaviour.

What is a plugin?

A plugin is code that runs once when the context starts, before any capabilities are registered. It has access to the full CraftContext and can:

  • Subscribe to lifecycle events (capability started, error occurred, context stopped)
  • Write shared state to the context store for adapters to read
  • Register additional capabilities dynamically

Plugins vs capabilities: a capability defines what your system does. A plugin extends how the runtime behaves. Logging, metrics, tracing, auth headers, and connection pooling are all plugin concerns, not capability concerns.

Writing a plugin

A plugin is a function that receives the context:

// plugins/logger.ts
import { type CraftContext } from '@routecraft/routecraft'

export default function loggerPlugin(context: CraftContext) {
  context.on('route:started', ({ details: { route } }) => {
    context.logger.info(`Started: ${route.definition.id}`)
  })

  context.on('error', ({ details: { error, route } }) => {
    context.logger.error(error, `Error in ${route?.definition.id ?? 'context'}`)
  })
}

Or as an object if you need a register step:

// plugins/metrics.ts
export default {
  async register(context: CraftContext) {
    context.setStore('metrics.counters', { started: 0, errors: 0 })

    context.on('route:started', ({ context }) => {
      const counters = context.getStore('metrics.counters') as any
      counters.started += 1
    })
  },
}

Registering a plugin

Pass plugins in craft.config.ts:

// craft.config.ts
import type { CraftConfig } from '@routecraft/routecraft'
import logger from './plugins/logger'
import metrics from './plugins/metrics'

const config: CraftConfig = {
  plugins: [logger, metrics],
}

export default config

Setting global adapter defaults

The most common plugin pattern is writing to the context store so adapters can read global configuration instead of requiring it per-capability.

// plugins/defaults.ts
export default function defaults(context: CraftContext) {
  context.setStore('db.config', {
    connectionString: process.env.DB_URL,
    poolSize: 10,
  })

  context.setStore('api.defaults', {
    headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
  })
}

An adapter reads it at call time:

class DbAdapter implements Destination<any, void> {
  async send(exchange) {
    const config = exchange.context.getStore('db.config') as { connectionString: string }
    await db(config.connectionString).insert(exchange.body)
  }
}

This keeps connection strings and tokens out of every capability file.

Lifecycle events

Plugins subscribe to events using context.on(eventName, handler). Common events include route:started, route:stopped, context:started, context:stopped, and error. See the Events reference for the full list.

Dynamically registering capabilities

Because plugins run before capabilities are registered, they can add capabilities to the context at startup:

// plugins/admin.ts
export default function adminPlugin(context: CraftContext) {
  if (process.env.ENABLE_ADMIN === 'true') {
    context.registerRoutes(
      craft()
        .id('admin-health')
        .from(simple({ ok: true }))
        .to(log())
        .build()[0]
    )
  }
}

Plugins reference

Full API for plugin interfaces and context methods.

Monitoring

Observability patterns built on plugins and events.

Previous
Adapters
Next
Events