ScreenshotNeo

BlogGuides

Nuxt Kit: Complete Guide to Building Nuxt Modules in Nuxt 4

Learn Nuxt Kit in Nuxt 4: define reusable modules, manage dependencies, register local modules, protect runtime config, and troubleshoot common errors.

By the ScreenshotNeo team1 October 20267 min read

Nuxt Kit is Nuxt’s module-authoring utility layer. Use it to define reusable modules, merge configuration, add hooks, register server handlers, declare module dependencies, and extend a Nuxt application during setup. It is not a runtime utility library for components, composables, pages, plugins, or server routes.

This guide targets the Nuxt 4 Kit API documented as v4.5.2 during research. Nuxt’s Nuxt 3 guide states that Nuxt 3 reached end of life on 31 July 2026, so verify support and package versions before publishing or upgrading a production project.

What Nuxt Kit does

Nuxt documentation describes Kit as providing features for module authors. A module runs while Nuxt is being configured and can:

  • Define a typed options object with defaults and a schema.
  • Merge user configuration with those defaults.
  • Install Nuxt lifecycle hooks.
  • Add plugins, components, composables, pages, server handlers, templates, aliases, and build configuration.
  • Declare dependencies on other Nuxt modules.
  • Expose selected, safe options to application runtime code.

Kit utilities belong to the module and build-time boundary. Do not import them into Vue components, composables, pages, plugins, or server routes. Runtime code should use the files and configuration that your module generates or registers.

Reusable module versus local module

Use case Location Registration Typical dependency setup
Reusable module for multiple projects Separate package, commonly src/module.ts Install the package and add it to modules in nuxt.config.ts Keep @nuxt/kit and @nuxt/schema aligned with Nuxt
App-local module modules/*/index.ts or modules/*.ts Nuxt 4 auto-registers these patterns Use the nuxt/kit helper subpath shown in the local-module guide

Install and align versions

For a published module, install Kit explicitly when your package needs it:

pnpm add -D @nuxt/kit @nuxt/schema
# or
npm install -D @nuxt/kit @nuxt/schema

Keep Kit and schema equal to or newer than the Nuxt version used by the project. Check the version-specific Nuxt Kit API reference before copying an example into a different Nuxt release.

Kit is ESM-only. Do not use require('@nuxt/kit'). In a CommonJS tool, load it asynchronously:

async function loadKit() {
  const { defineNuxtModule } = await import('@nuxt/kit')
  return defineNuxtModule
}

Define a reusable module with defineNuxtModule

defineNuxtModule is the core pattern. It merges defaults with user options, installs declared hooks, processes module dependencies, and then runs your setup callback.

// src/module.ts
import {
  addPlugin,
  createResolver,
  defineNuxtModule
} from '@nuxt/kit'

export interface ModuleOptions {
  enabled: boolean
  greeting: string
}

export default defineNuxtModule<ModuleOptions>({
  meta: {
    name: 'nuxt-greeting',
    configKey: 'greetingModule',
    compatibility: {
      nuxt: '^4.0.0'
    }
  },
  defaults: {
    enabled: true,
    greeting: 'Hello from Nuxt Kit'
  },
  schema: {
    enabled: { type: 'boolean', default: true },
    greeting: { type: 'string', default: 'Hello from Nuxt Kit' }
  },
  hooks: {
    'ready'(nuxt) {
      if (nuxt.options.dev) {
        console.log('[nuxt-greeting] module ready')
      }
    }
  },
  setup(options, nuxt) {
    if (!options.enabled) return

    const resolver = createResolver(import.meta.url)
    nuxt.options.runtimeConfig.public.greeting = options.greeting
    addPlugin(resolver.resolve('./runtime/plugin'))
  }
})

The module package would contain a runtime plugin such as:

// src/runtime/plugin.ts
export default defineNuxtPlugin(() => {
  // Runtime code belongs here, outside @nuxt/kit imports.
})

A project can configure the module with the metadata key:

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-greeting'],
  greetingModule: {
    enabled: true,
    greeting: 'Welcome'
  }
})

What each definition field means

Field Purpose
meta.name Stable module name used in diagnostics and generated metadata.
meta.configKey Top-level Nuxt config key for module options.
meta.compatibility Optional Nuxt compatibility constraint.
defaults Values merged with user options before setup runs.
schema Validation and documentation metadata for options.
hooks Nuxt lifecycle listeners installed by the module.
setup Main callback where files, handlers, plugins, and configuration are registered.

Declare module dependencies with moduleDependencies

When one module depends on another, use the declarative moduleDependencies option. It communicates setup order, compatibility constraints, and dependency configuration in one place.

import { defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
  meta: {
    name: 'nuxt-search-ui',
    configKey: 'searchUi'
  },
  moduleDependencies: {
    '@nuxtjs/tailwindcss': {
      version: '^6.0.0',
      defaults: {
        exposeConfig: false
      }
    }
  },
  setup(options, nuxt) {
    // The dependency is available according to its declared setup order.
  }
})

The API reference marks installModule as deprecated in favor of moduleDependencies. Existing projects may still contain the older helper, but new module definitions should use the declarative option and verify the exact dependency shape against the installed Kit version.

Create a Nuxt 4 local module

Nuxt 4 automatically registers modules/*/index.ts and modules/*.ts. You do not need to repeat these files in nuxt.config.ts.

// modules/request-log/index.ts
import {
  addServerHandler,
  defineNuxtModule
} from 'nuxt/kit'

export default defineNuxtModule({
  meta: {
    name: 'request-log'
  },
  setup() {
    addServerHandler({
      route: '/_debug/module-ready',
      handler: '~/modules/request-log/runtime/handler'
    })
  }
})
// modules/request-log/runtime/handler.ts
export default defineEventHandler(() => ({
  module: 'request-log',
  ready: true
}))

Run the application and request /_debug/module-ready. If Nuxt does not discover the module, check the filename pattern, TypeScript syntax, and the terminal output generated during Nuxt startup.

Register common things from a module

Plugins and templates

import {
  addPlugin,
  addTemplate,
  createResolver,
  defineNuxtModule
} from '@nuxt/kit'

export default defineNuxtModule({
  meta: { name: 'nuxt-assets' },
  setup() {
    const resolver = createResolver(import.meta.url)

    addPlugin(resolver.resolve('./runtime/plugin'))
    addTemplate({
      filename: 'generated/module-settings.mjs',
      getContents: () => 'export default { enabled: true }'
    })
  }
})

Hooks

export default defineNuxtModule({
  meta: { name: 'nuxt-build-observer' },
  hooks: {
    'nitro:config'(nitroConfig) {
      nitroConfig.runtimeConfig = nitroConfig.runtimeConfig || {}
    },
    'vite:extendConfig'(config) {
      config.define = {
        ...config.define,
        __MODULE_ENABLED__: 'true'
      }
    }
  },
  setup() {}
})

Server handlers and middleware

import { addServerHandler, defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
  meta: { name: 'nuxt-health-endpoint' },
  setup() {
    addServerHandler({
      route: '/health/module',
      method: 'get',
      handler: '~/modules/health/runtime/health'
    })
  }
})

Keep runtime configuration safe

Module options are available during build and setup. If a runtime feature needs configuration, pass only values that are safe for the browser into runtimeConfig.public. Nuxt warns: “Be careful not to expose any sensitive module configuration on the public runtime config, such as private API keys, as they will end up in the public bundle.”

import { defineNuxtModule } from '@nuxt/kit'
import { defu } from 'defu'

export default defineNuxtModule({
  meta: { name: 'nuxt-service' },
  defaults: {
    endpoint: 'https://example.test',
    publicToken: ''
  },
  setup(options, nuxt) {
    nuxt.options.runtimeConfig = defu(nuxt.options.runtimeConfig, {
      serviceSecret: '',
      public: {
        serviceEndpoint: options.endpoint,
        serviceToken: options.publicToken
      }
    })
  }
})

Keep private keys in the server-only portion of runtime config and read them from server code. Do not copy secrets into runtimeConfig.public, generated client files, templates, or environment values that are bundled for the browser.

Build and publish checklist

  1. Choose a stable module name and configuration key.
  2. Define typed options and safe defaults.
  3. Declare supported Nuxt versions in metadata and package peer dependencies.
  4. Use createResolver(import.meta.url) for package-relative files.
  5. Declare module relationships with moduleDependencies.
  6. Keep Kit imports in module/build-time files.
  7. Separate private runtime configuration from public values.
  8. Document whether the module is local-only or published.
  9. Build the package as ESM and avoid CommonJS require.
  10. Try the module in a minimal Nuxt 4 fixture before releasing it.

Common errors and fixes

Error or symptom Cause Fix
require('@nuxt/kit') fails Kit is ESM-only. Convert the module to ESM or use asynchronous import() from CommonJS.
Kit utility imported in a component Build-time and runtime boundaries are mixed. Move the Kit call into the module setup file and expose generated runtime code instead.
Local module is not loaded The file does not match Nuxt’s local module patterns. Use modules/*.ts or modules/*/index.ts, then restart Nuxt.
Dependency setup runs too late The dependency was installed imperatively or not declared. Declare it with moduleDependencies and specify its version constraint.
Unexpected option values Defaults or schema do not match the intended config key. Check meta.configKey, option names, and the merged defaults.
Private key appears in browser output The key was placed in public runtime config. Move it to server-only runtime config and consume it from server code.
Resolver cannot find a runtime file A relative path depends on the caller’s working directory. Resolve from import.meta.url with createResolver.
Type or schema incompatibility Kit, schema, and Nuxt versions are misaligned. Align package versions and consult the matching versioned Kit API reference.

Performance, reliability, and cost considerations

  • Do expensive work during setup only when necessary. Generate files once instead of repeating work on every request.
  • Prefer declarative dependencies so Nuxt can determine setup order and validate compatibility before runtime.
  • Keep runtime plugins small and avoid adding global hooks when a generated route or narrowly scoped plugin is sufficient.
  • Use caching for generated templates or derived metadata when regeneration is expensive during development.
  • Nuxt Kit itself is a package-level development dependency; your cost is the time and infrastructure used by the module’s generated application code.
  • Do not infer reliability, speed, or adoption from npm download counts. Those metrics change and do not measure module quality.

Or skip the browser setup

If your Nuxt module generates screenshots for documentation, previews, visual tests, or content pipelines, ScreenshotNeo provides a single HTTP request instead of maintaining a browser worker:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://nuxt.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://nuxt.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://nuxt.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the ScreenshotNeo API documentation for request options. Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing result. ScreenshotNeo also includes an MCP server so Claude, Cursor, and other MCP clients can take screenshots. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots.

Create a free ScreenshotNeo account to get started.

FAQ

Is Nuxt Kit a replacement for Vue composables?

No. Kit creates and configures Nuxt modules. Composables are runtime application code and should not import Kit utilities.

Do local Nuxt 4 modules need to be listed in nuxt.config.ts?

No. Nuxt automatically discovers modules/*/index.ts and modules/*.ts.

Should new modules call installModule?

Use moduleDependencies for new code. The current API marks installModule deprecated.

Can a module expose a public API key?

Only if the value is intentionally public. Private keys placed in public runtime configuration are included in the browser bundle.

Where should I check API changes?

Use the versioned Nuxt Kit API reference and the Nuxt Kit guide that match your Nuxt release.