Plugins
Extend RouteCraft with cross-cutting behavior using plugins.
What is a plugin?
A plugin can augment the CraftContext by:
- Registering event listeners
- Adding shared stores
- Exposing utilities to routes/adapters
Plugins are either:
- A function
(context) => void | Promise<void> - An object with
register(context)and optional lifecycle hooks
File-based auto-loading
When using the CLI, plugins are auto-loaded if present in a plugins/ directory at your project root (or within src/plugins if you use a src directory). You can also pass plugins explicitly via CraftConfig.plugins.
// craft.config.ts
import routes from './routes'
import metrics from './plugins/metrics'
export default {
routes,
plugins: [metrics],
}
Structure
Function style:
// plugins/logger.ts
import { type CraftContext } from '@routecraft/routecraft'
export default async function loggerPlugin(context: CraftContext) {
context.on('routeStarted', ({ details: { route } }) => {
context.logger.info(`Started: ${route.definition.id}`)
})
}
Object style:
// plugins/metrics.ts
import { type CraftContext } from '@routecraft/routecraft'
export default {
async register(context: CraftContext) {
context.setStore('metrics.counters', { started: 0 })
context.on('routeStarted', ({ context }) => {
const counters = context.getStore('metrics.counters') as { started: number }
counters.started += 1
})
},
}
Lifecycle hooks
Plugins can subscribe to the Events API and handle:
- Context lifecycle:
contextStarting,contextStarted,contextStopping,contextStopped - Route lifecycle:
routeRegistered,routeStarting,routeStarted,routeStopping,routeStopped - System:
error
See /docs/reference/events for full signatures.
Setting adapter defaults via the context store
Plugins are a great place to set configuration defaults that adapters can consume, such as database connection details or API auth.
// plugins/defaults.ts
import { type CraftContext } from '@routecraft/routecraft'
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}` },
})
}
Adapters can read these defaults via the context. If an adapter implements a merged options pattern, it can combine global defaults with local options.
// Example adapter merging store defaults
import { type CraftContext, type MergedOptions } from '@routecraft/routecraft'
interface DbOptions { connectionString: string; poolSize?: number }
class DbAdapter implements MergedOptions<DbOptions> {
constructor(public options: Partial<DbOptions> = {}) {}
mergedOptions(context: CraftContext): DbOptions {
const defaults = (context.getStore('db.config') as Partial<DbOptions>) || {}
return { connectionString: '', poolSize: 10, ...defaults, ...this.options }
}
}
Routes can also access defaults in processing steps:
craft()
.from(simple({ path: '/resource' }))
.process((ex) => {
const api = ex.context.getStore('api.defaults') as { headers?: Record<string,string> }
return { ...ex.body, headers: { ...api?.headers } }
})
Best practices
- Keep plugins focused (logging, metrics, tracing)
- Avoid hidden side effects in routes; use events instead
- Use the context store to share state between routes/adapters
Example: simple logging plugin
// plugins/simple-logger.ts
import { type CraftContext } from '@routecraft/routecraft'
export default function simpleLogger(context: CraftContext) {
context.on('error', ({ details: { error } }) => {
context.logger.error(error, 'RouteCraft error')
})
}