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
Plugins are registered via a server file that exports an array. This approach gives full closure, import, and TypeScript support with zero serialization issues.
Step 1: Point to the file in nuxt.config.ts
export default defineNuxtConfig({
autoApi: {
plugins: '~/server/autoapi-plugins',
},
})
Step 2: Create the server file
// server/autoapi-plugins.ts
import { createRateLimitPlugin, createRequestMetadataPlugin } from '@websideproject/nuxt-auto-api/plugins'
export default [
createRateLimitPlugin({
windowMs: 60000,
max: 200,
skip: (ctx) => ctx.user?.role === 'admin',
}),
createRequestMetadataPlugin({
autoPopulateOn: ['create', 'update'],
resources: ['users'],
}),
]
That's it. The module handles initialization, context wiring, and lifecycle automatically.
Why a file instead of inline config?
You might wonder why plugins aren't defined inline in nuxt.config.ts like this:
// THIS DOES NOT WORK
import { createRateLimitPlugin } from '@websideproject/nuxt-auto-api/plugins'
export default defineNuxtConfig({
autoApi: {
plugins: [
createRateLimitPlugin({ windowMs: 60000, max: 200 })
]
}
})
The reason is the serialization boundary between build time and runtime.
nuxt.config.ts runs at build time during Nuxt module setup. The module needs to pass plugin runtime logic to the Nitro server, which runs separately. To bridge this gap, it generates a virtual module (.nuxt/@websideproject/nuxt-auto-api-plugins.mjs). The only way to get inline functions into that file is Function.prototype.toString() — which captures the function text but loses all closure variables.
For example, createRateLimitPlugin() creates an in-memory Map and a setInterval timer inside the factory. The returned runtimeSetup function closes over these variables. When serialized via .toString(), the function text references store and cleanupTimer, but those variables don't exist in the generated file — causing store is not defined at runtime.
The same problem applies to any callback that uses imports from the calling scope:
// THIS ALSO DOES NOT WORK — geoip is a closure variable
import { geoip } from 'maxmind'
export default defineNuxtConfig({
autoApi: {
plugins: [
createRequestMetadataPlugin({
extract: async (event) => geoip.lookup(getRequestIP(event))
// ^^^^^^ lost during serialization
})
]
}
})
A dedicated server file solves this completely. Nitro bundles it as a real module — imports, closures, timers, Maps, and everything else work naturally because the code runs as-is, never serialized.
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 } from '@websideproject/nuxt-auto-api/plugins'
import { createAuditLogPlugin } from '@websideproject/nuxt-auto-api-audit-log'
export default [
createRateLimitPlugin({ max: 200 }),
createAuditLogPlugin({ destination: 'db' }),
]
2. Community Nuxt modules (via hook)
Community modules can register plugins without the user touching their config:
// @websideproject/nuxt-auto-api-audit-log/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:
// @websideproject/nuxt-auto-api-audit-log/src/runtime/audit-plugin.ts
import { defineAutoApiPlugin } from '@websideproject/nuxt-auto-api/plugins'
export default defineAutoApiPlugin({
name: 'audit-log',
runtimeSetup(ctx) {
ctx.addMiddleware({
name: 'audit-log',
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.
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
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: (session) => ({
id: session.user.id,
email: session.user.email,
permissions: session.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).
- 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)
// @websideproject/nuxt-auto-api-audit-log/src/plugin.ts
import { defineAutoApiPlugin } from '@websideproject/nuxt-auto-api/plugins'
import type { AutoApiPlugin } from '@websideproject/nuxt-auto-api/plugins'
export interface AuditLogOptions {
destination: 'db' | 'console' | 'external'
}
export function createAuditLogPlugin(options: AuditLogOptions): AutoApiPlugin {
return defineAutoApiPlugin({
name: 'audit-log',
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 { createAuditLogPlugin } from '@websideproject/nuxt-auto-api-audit-log'
export default [
createAuditLogPlugin({ destination: 'db' }),
]
As a Nuxt module (zero-config for users)
// @websideproject/nuxt-auto-api-audit-log/src/module.ts
import { defineNuxtModule, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
meta: { name: '@websideproject/nuxt-auto-api-audit-log' },
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', '@websideproject/nuxt-auto-api-audit-log']
Migration from Extensions
If you were using the legacy extensions field, migrate to the file-based approach:
// Before (deprecated)
autoApi: {
extensions: [
createRateLimitExtension({ max: 100 })
]
}
// After
autoApi: {
plugins: '~/server/autoapi-plugins'
}
The extensions field still works for backward compatibility but is deprecated.
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.