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

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 fileListed inline in nuxt.config
Optionsanything — instances (a KV store), imports, closuresdata, and functions that use nothing from nuxt.config
Build-time phase (buildSetup)does not runruns
Plugins that add routes✗ — their routes are never registered✓ — must be listed here
Secrets (process.env.X)read when the server runswritten 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

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.

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

  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) — list plugins that add routes inline.
  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)

// 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'
}

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.