Use Hooks & Extend Types

Master lifecycle hooks, virtual files, TypeScript declarations and error handling in your modules.

Here are some advanced patterns for authoring modules, including hooks, templates, type augmentation and structured error handling.

Use Lifecycle Hooks

Lifecycle hooks allow you to expand almost every aspect of Nuxt. Modules can hook to them programmatically or through the hooks map in their definition.

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

export default defineNuxtModule({
  // Hook to the `app:error` hook through the `hooks` map
  hooks: {
    'app:error': (err) => {
      console.info(`This error happened: ${err}`)
    },
  },
  setup (options, nuxt) {
    // Programmatically hook to the `pages:extend` hook
    nuxt.hook('pages:extend', (pages) => {
      console.info(`Discovered ${pages.length} pages`)
    })
  },
})
Read more in Docs > 4 X > API > Advanced > Hooks.
Watch Vue School video about using Nuxt lifecycle hooks in modules.
Module cleanup

If your module opens, handles, or starts a watcher, you should close it when the Nuxt lifecycle is done. The close hook is available for this.
import { defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
  setup (options, nuxt) {
    nuxt.hook('close', async (nuxt) => {
      // Your custom code here
    })
  },
})

Create Custom Hooks

Modules can also define and call their own hooks, which is a powerful pattern for making your module extensible.

If you expect other modules to be able to subscribe to your module's hooks, you should call them in the modules:done hook. This ensures that all other modules have had a chance to be set up and register their listeners to your hook during their own setup function.

// my-module/module.ts
import { defineNuxtModule } from '@nuxt/kit'

export interface ModuleHooks {
  'my-module:custom-hook': (payload: { foo: string }) => void
}

export default defineNuxtModule({
  setup (options, nuxt) {
    // Call your hook in `modules:done`
    nuxt.hook('modules:done', async () => {
      const payload = { foo: 'bar' }
      await nuxt.callHook('my-module:custom-hook', payload)
    })
  },
})

Add Virtual Files

If you need to add a virtual file that can be imported into the user's app, you can use the addTemplate utility.

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

export default defineNuxtModule({
  setup (options, nuxt) {
    // The file is added to Nuxt's internal virtual file system and can be imported from '#build/my-module-feature.mjs'
    addTemplate({
      filename: 'my-module-feature.mjs',
      getContents: () => 'export const myModuleFeature = () => "hello world !"',
    })
  },
})

For the server, you should use the addServerTemplate utility instead.

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

export default defineNuxtModule({
  setup (options, nuxt) {
    // The file is added to Nitro's virtual file system and can be imported in the server code from 'my-server-module.mjs'
    addServerTemplate({
      filename: 'my-server-module.mjs',
      getContents: () => 'export const myServerModule = () => "hello world !"',
    })
  },
})

Update Virtual Files

If you need to update your templates/virtual files, you can leverage the updateTemplates utility like this:

nuxt.hook('builder:watch', (event, path) => {
  if (path.includes('my-module-feature.config')) {
    // This will reload the template that you registered
    updateTemplates({ filter: t => t.filename === 'my-module-feature.mjs' })
  }
})

Add Type Declarations

You might also want to add a type declaration to the user's project (for example, to augment a Nuxt interface or provide a global type of your own). For this, Nuxt provides the addTypeTemplate utility that both writes a template to the disk and adds a reference to it in the generated nuxt.d.ts file.

If your module should augment types handled by Nuxt, you can use addTypeTemplate to perform this operation:

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

export default defineNuxtModule({
  setup (options, nuxt) {
    addTypeTemplate({
      filename: 'types/my-module.d.ts',
      getContents: () => `// Generated by my-module
        interface MyModuleNitroRules {
          myModule?: { foo: 'bar' }
        }
        declare module 'nitro/types' {
          interface NitroRouteRules extends MyModuleNitroRules {}
          interface NitroRouteConfig extends MyModuleNitroRules {}
        }
        export {}`,
    })
  },
})

If you need more granular control, you can use the prepare:types hook to register a callback that will inject your types.

const template = addTemplate({ /* template options */ })
nuxt.hook('prepare:types', ({ references }) => {
  references.push({ path: template.dst })
})

Extend TypeScript Config

There are multiple ways to extend the TypeScript configuration of the user's project from your module.

The simplest way is to modify the Nuxt configuration directly like this:

// extend tsconfig.app.json
nuxt.options.typescript.tsConfig.include ??= []
nuxt.options.typescript.tsConfig.include.push(resolve('./augments.d.ts'))

// extend tsconfig.shared.json
nuxt.options.typescript.sharedTsConfig.include ??= []
nuxt.options.typescript.sharedTsConfig.include.push(resolve('./augments.d.ts'))

// extend tsconfig.node.json
nuxt.options.typescript.nodeTsConfig.include ??= []
nuxt.options.typescript.nodeTsConfig.include.push(resolve('./augments.d.ts'))

// extend tsconfig.server.json
nuxt.options.nitro.typescript ??= {}
nuxt.options.nitro.typescript.tsConfig ??= {}
nuxt.options.nitro.typescript.tsConfig.include ??= []
nuxt.options.nitro.typescript.tsConfig.include.push(resolve('./augments.d.ts'))

Alternatively, you can use the prepare:types and nitro:prepare:types hooks to extend the TypeScript references for specific type contexts, or modify the TypeScript configuration similar to the example above.

nuxt.hook('prepare:types', ({ references, sharedReferences, nodeReferences }) => {
  // extend app context
  references.push({ path: resolve('./augments.d.ts') })
  // extend shared context
  sharedReferences.push({ path: resolve('./augments.d.ts') })
  // extend node context
  nodeReferences.push({ path: resolve('./augments.d.ts') })
})

nuxt.hook('nitro:prepare:types', ({ references }) => {
  // extend server context
  references.push({ path: resolve('./augments.d.ts') })
})
TypeScript references add files to the type context without being affected by the exclude option in tsconfig.json.

Augment Types

Nuxt automatically includes your module's directories in the appropriate type contexts. To augment types from your module, all you need to do is place the type declaration file in the appropriate directory based on the augmented type context. Alternatively, you can extend the TypeScript configuration to augment from an arbitrary location.

  • my-module/runtime/ - app type context (except for the runtime/server directory)
  • my-module/runtime/server/ - server type context
  • my-module/ - node type context (except for the runtime/ and runtime/server directories)
Directory Structure
-| my-module/   # node type context
---| runtime/   # app type context
------| augments.app.d.ts
------| server/ # server type context
---------| augments.server.d.ts
---| module.ts
---| augments.node.d.ts

Known Limitations

Type-Checking Server Routes in App Context

Server routes are also type-checked using tsconfig.app.json in addition to tsconfig.server.json.

This is required because Nuxt infers the return types of your server endpoints to provide response types in $fetch and useFetch.

This can cause issues when using server-only types in your route files. For example, if a module creates a server-only virtual file using addServerTemplate and you declare types for it in tsconfig.server.json, those type declarations will only be available in the server context. When the app context type-checks your server routes, it won't recognize these server-only types and will report errors. To resolve this, you unfortunately need to declare such types in the app context as well.

Structured Error Handling

Nuxt Kit provides createBuildErrorUtils to give your module consistent, structured error and warning messages. Each message is tagged with an error code, can include a fix suggestion and a docs link, and automatically surfaces extra diagnostic context when an AI coding agent is detected.

Setup

Create a file in your module (e.g. errors.ts) that initializes the utilities:

my-module/src/errors.ts
import { createBuildErrorUtils } from '@nuxt/kit'

export const { throwBuildError, warnBuild, errorBuild, formatBuildError } = createBuildErrorUtils({
  module: 'my-module',
  docsBase: 'https://my-module.dev/errors',
})

The module value is uppercased and used as the error tag prefix (e.g. [MY-MODULE_001]). The optional docsBase is combined with each error code to auto-generate a documentation URL.

Returned Utilities

createBuildErrorUtils returns four functions, all sharing the same signature (message, opts):

FunctionBehavior
throwBuildErrorThrows an Error with the formatted message. Return type is never.
warnBuildLogs a warning via consola.
errorBuildLogs an error via consola (without throwing).
formatBuildErrorReturns the formatted string without logging or throwing.

Options

The second argument to each function accepts:

interface NuxtErrorOptions {
  /** Error code, e.g. '001'. Combined with the module prefix for the tag. */
  code: string
  /** Why the error occurred. */
  why?: string
  /** A concrete suggestion for how to fix the issue. */
  fix?: string
  /** A documentation URL (overrides the auto-generated one from docsBase). */
  docs?: string
  /** The underlying error that caused this one. */
  cause?: unknown
  /** Extra key-value context (only shown when an AI agent is detected). */
  context?: Record<string, unknown>
}

Usage in a Module

my-module/src/module.ts
import { defineNuxtModule } from '@nuxt/kit'
import { throwBuildError, warnBuild } from './errors'

export default defineNuxtModule({
  setup (options, nuxt) {
    if (!options.apiKey) {
      return throwBuildError('Missing required `apiKey` option.', {
        code: '001',
        fix: 'Set `myModule.apiKey` in your `nuxt.config`.',
      })
    }

    if (options.deprecated) {
      warnBuild('The `deprecated` option will be removed in the next major version.', {
        code: '002',
        fix: 'Use the `replacement` option instead.',
      })
    }
  },
})

The output in the terminal looks like:

[MY-MODULE_001] Missing required `apiKey` option.
├▶ see: https://my-module.dev/errors/001
╰▶ fix: Set `myModule.apiKey` in your `nuxt.config`.
Use return throwBuildError(...) instead of just throwBuildError(...) when the call is at the end of a function or inside a narrowing guard. Although throwBuildError always throws, TypeScript cannot track the never return type through the destructured factory result for control-flow analysis.