Plugin System
Plugin System
The plugin system provides a structured architecture for extending @websideproject/nuxt-auto-api. Plugins can hook into both the build-time module setup and the server-side runtime pipeline.
Overview
A plugin has two optional phases:
- Build-time (
buildSetup) - Runs during Nuxt module setup. Register server handlers, add imports, modify options. - Runtime (
runtimeSetup) - Runs when the Nitro server starts. Register middleware, hooks, and context extenders.
Plugin Registration
autoApi.plugins takes a server file, plugins listed inline, or both:
// nuxt.config.ts
import { createExportPlugin } from '@websideproject/nuxt-auto-api/plugins'
export default defineNuxtConfig({
autoApi: {
plugins: [
'~/server/autoapi-plugins', // a file: anything goes
createExportPlugin({ formats: ['csv', 'json'] }), // inline: plugins that add routes
],
},
})
// server/autoapi-plugins.ts
import { createRateLimitPlugin, createRequestMetadataPlugin } from '@websideproject/nuxt-auto-api/plugins'
import { KvRateLimitStore } from './utils/kv-rate-limit-store'
export default [
createRateLimitPlugin({ max: 200, store: new KvRateLimitStore() }),
createRequestMetadataPlugin({ autoPopulateOn: ['create', 'update'], resources: ['users'] }),
]
In a plugins path, ~/ and ~~/ both mean the project root (unlike Nuxt 4's ~, which is app/), so
'~/server/autoapi-plugins' is <root>/server/autoapi-plugins.ts.
| A server file | Listed inline in nuxt.config | |
|---|---|---|
| Options | anything — instances (a KV store), imports, closures | data, and functions that use nothing from nuxt.config |
Build-time phase (buildSetup) | does not run | runs |
| Plugins that add routes | ✗ — their routes are never registered | ✓ — must be listed here |
Secrets (process.env.X) | read when the server runs | written into the server bundle at build time (the build warns) |
Why the difference?
nuxt.config.ts runs at build time; the server runs later, from a bundle. A plugin listed inline cannot be
handed over as a live object, so the module writes code that recreates it: for a built-in factory
(createRateLimitPlugin({ … }) and the rest) it calls the factory again with the same options. Data survives
that; a function survives as its source text, so it must not use variables or imports from nuxt.config:
// Listed inline — works: the function is self-contained
createRateLimitPlugin({ skip: ctx => ctx.user?.role === 'admin' })
// Listed inline — does NOT work: `geoip` is an import of nuxt.config, not available on the server
import { geoip } from 'maxmind'
createRequestMetadataPlugin({ extract: async event => geoip.lookup(getRequestIP(event)) })
An option that can never be recreated — a class instance such as a store — fails the build and points you to the file. A server file is bundled by Nitro as real code, so it has none of these limits; what it lacks is the build phase, which is where routes are registered.
Plugin Sources
Plugins can come from three sources, all merged at build time:
1. App-level (your server file)
// nuxt.config.ts
autoApi: {
plugins: '~/server/autoapi-plugins'
}
// server/autoapi-plugins.ts
import { createRateLimitPlugin, createRevisionPlugin } from '@websideproject/nuxt-auto-api/plugins'
export default [
createRateLimitPlugin({ max: 200 }),
createRevisionPlugin({ resources: ['articles'] }),
]
2. Community Nuxt modules (via hook)
Community modules can register plugins without the user touching their config. (The package names below are examples of what a third-party package would look like — no such package is published.)
// my-nuxt-auto-api-plugin/src/module.ts
export default defineNuxtModule({
setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
nuxt.hook('autoApi:registerPlugins', (ctx) => {
ctx.addFile(resolver.resolve('./runtime/audit-plugin'))
})
}
})
The file must default-export a single AutoApiPlugin or an array of them:
// my-nuxt-auto-api-plugin/src/runtime/audit-plugin.ts
import { defineAutoApiPlugin } from '@websideproject/nuxt-auto-api/plugins'
export default defineAutoApiPlugin({
name: 'my-audit',
runtimeSetup(ctx) {
ctx.addMiddleware({
name: 'my-audit',
stage: 'post-execute',
handler: async (context) => { /* ... */ },
})
},
})
3. Built-in plugins (shipped with @websideproject/nuxt-auto-api)
These are imported from @websideproject/nuxt-auto-api/plugins and used in your server file like any other plugin.
Creating a Plugin
Using a factory function (recommended for configurable plugins)
import { defineAutoApiPlugin } from '@websideproject/nuxt-auto-api/plugins'
import type { AutoApiPlugin } from '@websideproject/nuxt-auto-api/plugins'
export interface MyPluginOptions {
threshold?: number
}
export function createMyPlugin(options: MyPluginOptions = {}): AutoApiPlugin {
const { threshold = 100 } = options
return defineAutoApiPlugin({
name: 'my-plugin',
version: '1.0.0',
runtimeSetup(ctx) {
ctx.addMiddleware({
name: 'my-middleware',
stage: 'pre-auth',
handler: async (context) => {
// `threshold` is available here — closure works!
console.log(`Threshold: ${threshold}`)
},
})
},
})
}
Using defineAutoApiPlugin directly (for simple plugins)
import { defineAutoApiPlugin } from '@websideproject/nuxt-auto-api/plugins'
export default defineAutoApiPlugin({
name: 'simple-plugin',
runtimeSetup(ctx) {
ctx.extendContext(async (context) => {
context.customData = { fromPlugin: true }
})
ctx.addHook('users', {
afterCreate: async (result) => {
console.log('User created:', result.id)
},
})
ctx.addGlobalHook({
afterUpdate: async (result, context) => {
console.log(`${context.resource} updated`)
},
})
},
})
Middleware Stages
Plugins can register middleware at four stages of the request pipeline:
pre-auth → authorize → post-auth → validate → pre-execute → handler → post-execute
| Stage | When it runs | Use case |
|---|---|---|
pre-auth | Before authorization | Rate limiting, request logging, IP filtering |
post-auth | After authorization, before validation | Tenant injection, audit logging |
pre-execute | After validation, before the handler | Cache checks, request enrichment |
post-execute | After the handler returns | Response logging, cache population |
Middleware Options
ctx.addMiddleware({
name: 'audit-log',
stage: 'post-execute',
order: 10, // Lower runs first (default: 0)
resources: ['users', 'posts'], // Only these resources (default: all)
operations: ['create', 'update'], // Only these operations (default: all)
handler: async (context) => {
await logAuditEntry(context)
},
})
Context Extenders
Context extenders run on every request after the initial context is built, before any middleware. They enrich the HandlerContext with additional data.
Extenders run in registration order (the plugins file's array order, then community modules), so an extender
that reads context.user must come after the plugin that sets it.
ctx.extendContext(async (context) => {
if (context.user) {
context.tenant = {
id: context.user.tenantId,
field: 'tenantId',
canAccessAllTenants: context.user.role === 'super-admin',
}
}
})
Build-Time Context
The buildSetup function receives a PluginBuildContext with access to Nuxt Kit utilities:
interface PluginBuildContext {
addServerHandler: typeof addServerHandler
addServerImportsDir: typeof addServerImportsDir
addImportsDir: typeof addImportsDir
addServerPlugin: (path: string) => void
addPlugin: typeof addPlugin
addTemplate: typeof addTemplate
options: AutoApiOptions // Mutable - plugins can modify options
nuxt: Nuxt
resolver: ReturnType<typeof createResolver>
logger: { info, warn, error, debug }
}
Runtime Context
The runtimeSetup function receives a PluginRuntimeContext:
interface PluginRuntimeContext {
addMiddleware: (middleware: AutoApiMiddleware) => void
extendContext: (fn: ContextExtender) => void
addHook: (resource: string, hooks: ResourceHooks) => void
addGlobalHook: (hooks: ResourceHooks) => void
registerPermissionEvaluator: (fn: PermissionEvaluator) => void // custom object permission kinds
runtimeConfig: any
logger: { info, warn, error, debug }
}
Built-in Plugins
Rate Limiting
import { createRateLimitPlugin } from '@websideproject/nuxt-auto-api/plugins'
createRateLimitPlugin({
windowMs: 60000, // 1 minute
max: 100, // 100 requests per window
byIp: true, // Rate limit by IP (default)
byUser: false, // Also rate limit by user ID
skip: (ctx) => ctx.user?.role === 'admin',
message: 'Too many requests',
})
Request Metadata
import { createRequestMetadataPlugin } from '@websideproject/nuxt-auto-api/plugins'
createRequestMetadataPlugin({
autoPopulate: (metadata, data, context) => {
if (context.operation === 'create') {
data.signupCountry = metadata.country
data.signupIp = metadata.ip
}
return data
},
autoPopulateOn: ['create', 'update'],
resources: ['users'],
})
See Request Metadata Plugin for full documentation.
Better Auth Integration
import { createBetterAuthPlugin } from '@websideproject/nuxt-auto-api/plugins'
createBetterAuthPlugin({
getSession: async (event) => {
const session = await auth.api.getSession({ headers: event.headers })
return session
},
mapUser: (user, session) => ({
id: user.id,
email: user.email,
roles: [user.role ?? 'user'],
organizationId: session?.activeOrganizationId ?? null, // a custom mapper must copy the tenant across
}),
getPermissions: user => (user.role === 'admin' ? ['admin'] : ['user']),
})
Plugin Execution Order
- Build-time: Inline plugins execute in array order during module setup. File-based plugins do not have build-time phases (their code runs at runtime) — list plugins that add routes inline.
- Runtime: Plugins execute in order: user file plugins first, then community module plugins. Middleware is sorted by
orderfield across all plugins.
Writing a Community Plugin Package
As a factory function (users add to their plugins file)
// my-nuxt-auto-api-plugin/src/plugin.ts
import { defineAutoApiPlugin } from '@websideproject/nuxt-auto-api/plugins'
import type { AutoApiPlugin } from '@websideproject/nuxt-auto-api/plugins'
export interface MyAuditOptions {
destination: 'db' | 'console' | 'external'
}
export function createMyAuditPlugin(options: MyAuditOptions): AutoApiPlugin {
return defineAutoApiPlugin({
name: 'my-audit',
runtimeSetup(ctx) {
ctx.addMiddleware({
name: 'audit-log',
stage: 'post-execute',
handler: async (context) => {
// Log the operation ...
},
})
},
})
}
Users add it to their server file:
// server/autoapi-plugins.ts
import { createMyAuditPlugin } from 'my-nuxt-auto-api-plugin'
export default [
createMyAuditPlugin({ destination: 'db' }),
]
As a Nuxt module (zero-config for users)
// my-nuxt-auto-api-plugin/src/module.ts
import { defineNuxtModule, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
meta: { name: 'my-nuxt-auto-api-plugin' },
setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
nuxt.hook('autoApi:registerPlugins', (ctx) => {
ctx.addFile(resolver.resolve('./runtime/audit-plugin'))
})
},
})
Users just add the module to nuxt.config.ts:
modules: ['@websideproject/nuxt-auto-api', 'my-nuxt-auto-api-plugin']
Migration from Extensions
The legacy extensions field is no longer read — anything listed there is silently ignored. Move the plugins
to plugins (createRateLimitExtension is now createRateLimitPlugin, same options):
// Before (ignored)
autoApi: {
extensions: [
createRateLimitExtension({ max: 100 })
]
}
// After
autoApi: {
plugins: '~/server/autoapi-plugins'
}
Many-to-Many (M2M) Relationships
This guide covers how to work with many-to-many relationships in @websideproject/nuxt-auto-api.
Database Adapters
@websideproject/nuxt-auto-api supports multiple database engines through a unified adapter layer. Each adapter normalizes engine-specific behavior like transactions, batch operations, and mutation count parsing.