For AI agents: the complete documentation index is available at https://dotpress.dev/llms.txt, the full documentation bundle is available at https://dotpress.dev/llms-full.txt, and this page is available as Markdown at https://dotpress.dev/routing/plugins.md.

Plugins

A plugin extends an app with a reusable feature: routes, global middlewares, response filters, context keys and lifecycle hooks. The packages of Dotpress are plugins (authPlugin(), openapiPlugin(), mailerPlugin()…), and your app can have its own.

Using a plugin

Plugins are given to createApp():

const app = await createApp({
  adapter,
  plugins: [authPlugin(auth), openapiPlugin({ info: { title: 'My API', version: '1.0.0' } })],
})

They are set up in order, after the plugins they depend on.

Writing a plugin

A plugin is an object declared with definePlugin(): a unique name, and a setup() function that receives the plugin API. A function that takes options and returns the plugin keeps it configurable:

import { 
const declareContextKey: (key: DecoratableContextKey, pluginName: string) => void

Declares a context key provided by a plugin, so that accessing it without the plugin installed throws an explicit error instead of returning undefined. Call it next to the module augmentation, at the top level of the package.

@exampledeclare module '@dotpress/core' { interface RequestContext { mailer: Mailer } } declareContextKey('mailer', 'mailerPlugin')
declareContextKey
,
const definePlugin: <const T extends Plugin>(plugin: T) => T

Declares a plugin. Extra properties are allowed, and exposed to other plugins through api.getPlugin().

@exampleexport const healthPlugin = (path = '/health') => definePlugin({ name: 'health', setup(api) { api.addRoute({ path, handler: () => ({ status: 'ok' }) }) }, })
definePlugin
} from '@dotpress/core'
export interface FeatureFlags {
FeatureFlags.isEnabled(flag: string): boolean
isEnabled
(
flag: string
flag
: string): boolean
FeatureFlags.all(): Record<string, boolean>
all
():
type Record<K extends keyof any, T> = { [P in K]: T; }

Construct a type with a set of properties K of type T

Record
<string, boolean>
} export interface FlagSource {
FlagSource.forTenant(tenantId: string | undefined): Promise<FeatureFlags>
forTenant
(
tenantId: string | undefined
tenantId
: string | undefined):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<FeatureFlags>
FlagSource.close(): Promise<void>
close
():
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<void>
} // The new context key, typed in every handler of the program. declare module '@dotpress/core' { interface RequestContext {
RequestContext.flags: Promise<FeatureFlags>
flags
:
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<FeatureFlags>
} } // Using ctx.flags without the plugin throws an explicit error, which names // the function to install: featureFlagsPlugin.
function declareContextKey(key: DecoratableContextKey, pluginName: string): void

Declares a context key provided by a plugin, so that accessing it without the plugin installed throws an explicit error instead of returning undefined. Call it next to the module augmentation, at the top level of the package.

@exampledeclare module '@dotpress/core' { interface RequestContext { mailer: Mailer } } declareContextKey('mailer', 'mailerPlugin')
declareContextKey
('flags', 'featureFlagsPlugin')
export const
const featureFlagsPlugin: (source: FlagSource) => {
    readonly name: "featureFlags";
    readonly setup: (api: PluginAPI) => void;
}
featureFlagsPlugin
= (
source: FlagSource
source
: FlagSource) =>
definePlugin<{
    readonly name: "featureFlags";
    readonly setup: (api: PluginAPI) => void;
}>(plugin: {
    readonly name: "featureFlags";
    readonly setup: (api: PluginAPI) => void;
}): {
    readonly name: "featureFlags";
    readonly setup: (api: PluginAPI) => void;
}

Declares a plugin. Extra properties are allowed, and exposed to other plugins through api.getPlugin().

@exampleexport const healthPlugin = (path = '/health') => definePlugin({ name: 'health', setup(api) { api.addRoute({ path, handler: () => ({ status: 'ok' }) }) }, })
definePlugin
({
name: "featureFlags"
name
: 'featureFlags',
function setup(api: PluginAPI): void
setup
(
api: PluginAPI
api
) {
// Computed on first access, once per request.
api: PluginAPI
api
.
PluginAPI.decorateContext<"flags">(key: "flags", factory: ContextFactory<"flags">): void

Provides a context key, declared on RequestContext through module augmentation. The factory runs on first access, once per request.

@exampledeclare module '@dotpress/core' { interface RequestContext { mailer: Mailer } } api.decorateContext('mailer', () => mailer)
decorateContext
('flags', (
ctx: RequestContext
ctx
) =>
source: FlagSource
source
.
FlagSource.forTenant(tenantId: string | undefined): Promise<FeatureFlags>
forTenant
(
ctx: RequestContext
ctx
.
RequestContext.req: DotpressRequest<Record<string, string>, QueryValues, unknown, UploadedFiles>
req
.
DotpressRequest<Record<string, string>, QueryValues, unknown, UploadedFiles>.header(name: string): string | undefined

Returns a header value (case-insensitive).

header
('x-tenant-id')))
api: PluginAPI
api
.
PluginAPI.addRoute: <"/flags", EmptyObject, false, unknown[]>(route: RouteConfig<"/flags", EmptyObject, EmptyObject, false, unknown[], unknown>) => void
addRoute
({
RouteConfig<"/flags", EmptyObject, EmptyObject, false, unknown[], unknown>.path: "/flags"
path
: '/flags',
RouteConfig<"/flags", EmptyObject, EmptyObject, false, unknown[], unknown>.handler: RouteHandler<EmptyObject, {}, false, unknown>
handler
: async ({
flags: Promise<FeatureFlags>
flags
}) => (await
flags: Promise<FeatureFlags>
flags
).
FeatureFlags.all(): Record<string, boolean>
all
(),
})
api: PluginAPI
api
.
PluginAPI.hooks: PluginHooks
hooks
.
PluginHooks.onClose(hook: LifecycleHook): void

When app.close() is called, after the server is stopped.

onClose
(() =>
source: FlagSource
source
.
FlagSource.close(): Promise<void>
close
())
}, })

Handlers of the app then use ctx.flags, typed:

handler: async ({ flags }) => ((await flags).isEnabled('new-checkout') ? newCheckout() : checkout())

The plugin API

MethodDescription
addRoute(route)Adds a route, like defineRoute()
addRouteGroup(prefix, middlewares)Adds a group of routes
addGlobalMiddleware(middleware)Runs on every route, before the middlewares of createApp()
addResponseFilter(filter)Filters every JSON response, after the filters of createApp()
decorateContext(key, factory)Provides a context key declared by module augmentation, computed on first access
onError(hook)Receives the unexpected errors, like the onError option of createApp()
hooks.onInit, onReady, onRequest, wrapRequest, onResponse, onCloseLifecycle hooks, see below
getRoutes()The routes of the app, from onInit
getPlugin(name), getPlugins()The other plugins
loggerA logger bound to the plugin
basePath, publicBasePath, publicPath(path)The paths of the app, see Base path
corsThe CORS settings of the app (credentials, isOriginAllowed(req))

Registration methods are only available during setup(), which can be async: they throw once it has returned.

Hooks

HookWhen
onInitAfter every plugin is set up and the routes are compiled: inspect the routes
onReadyAfter the routes are mounted on the server, before listen()
onRequest(ctx)For each request matching a route, before the middlewares. Can stop the request, like a middleware.
wrapRequest(ctx, next)Runs the pipeline of each request inside a function: tracing, request-scoped storage
onResponse(event)For every response, 404 included: logging, metrics. Can replace the response.
onCloseWhen app.close() is called, after the server has stopped, in reverse order of the plugins: close connections

See Logging and error hooks for examples.

Dependencies between plugins

A plugin that needs another one declares it with dependsOn: it is set up after it, whatever the order of plugins. A missing or circular dependency makes createApp() throw.

definePlugin({
  name: 'auditLog',
  dependsOn: ['auth'], // the name of authPlugin()
  setup(api) {
    // …
  },
})

The other properties of a plugin object are visible to other plugins through api.getPlugin(name): a plugin can expose a service to the plugins that depend on it.

Plugin or middleware?

NeedUse
A check on some routesa middleware on the routes or a group
A check on every routea global middleware of createApp()
A feature reused across apps, with routes, context keys or hooksa plugin
Services of your app in ctxa small plugin, see Project structure
Coming from v1

Plugins are objects declared with definePlugin(), no longer functions. addGroup() is renamed addRouteGroup(), and useBeforeRoutes() / useAfterRoutes() are removed: plugins do not depend on a server, native middlewares go to the adapter options. Plugins gain dependencies, lifecycle hooks and context keys.