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.

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

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
StageWhen it runsUse case
pre-authBefore authorizationRate limiting, request logging, IP filtering
post-authAfter authorization, before validationTenant injection, audit logging
pre-executeAfter validation, before the handlerCache checks, request enrichment
post-executeAfter the handler returnsResponse 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

  1. 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).
  2. Runtime: Plugins execute in order: user file plugins first, then community module plugins. Middleware is sorted by order field 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.

Need a Landing Page?

Modern landing pages with optional modules (blog, docs, forms, i18n). Let's discuss your project.

Build Your MVP

Full-stack SaaS development. Expert in database design, multi-tenancy, and scalable architecture.

Deployment Help

Dockerize your backend, set up CI/CD pipelines, deploy to Cloudflare or Hetzner. Early-stage setup.

Suggest a SaaS Tool

Missing a calculator or tool? Suggest what you'd like to see on our site.