Module authoring

The kit API, grouped

The @nuxt/kit helpers a module author should know from memory, grouped by what they do, with what each one does underneath and the source files worth reading.

@nuxt/kit is small: a few dozen functions, each a thin wrapper over "push into nuxt.options" or "register a hook". Knowing what each one does underneath is what lets you predict ordering, explain why two modules interfere, and debug a helper that "did nothing". Interviewers rarely ask for signatures; they ask what addPlugin does internally, or why useNuxt() works without arguments.

Know

GroupHelpersUnder the hood
Inject runtime codeaddPlugin, addPluginTemplate, addImports, addImportsDir, addImportsSources, addComponent, addComponentsDir, addComponentExports, addRouteMiddleware, addLayout, extendPages, extendRouteRules, addPrerenderRoutesaddPlugin normalises src (string to object, alias resolution, extension lookup), removes an existing plugin with the same src, infers mode from a .client / .server suffix and prepends to nuxt.options.plugins unless { append: true }. addImports* feed unimport through the imports:extend / imports:dirs / imports:sources hooks; addComponentsDir pushes into components:dirs; extendPages wraps pages:extend.
Server sideaddServerHandler, addDevServerHandler, addNitroPlugin (addServerPlugin is a deprecated alias), addServerImports, addServerImportsDir, addServerScanDir, addServerTemplate, useNitro, tryUseNitroHandlers go onto nuxt.options.serverHandlers and become Nitro handlers; server imports feed Nitro's own unimport instance, separate from the app's; addServerScanDir adds a directory Nitro scans for api, routes, middleware and utils; addServerTemplate writes into nuxt.options.nitro.virtual.
Files and templatesaddTemplate, addTypeTemplate, addServerTemplate, updateTemplates, writeTypesTemplates land in buildDir (.nuxt): virtual by default and served from memory by the bundler, on disk with write: true, importable as #build/<filename>. Type templates are referenced from .nuxt/nuxt.d.ts via prepare:types. updateTemplates just calls builder:generateApp.
Bundler configaddVitePlugin, addWebpackPlugin, addRspackPlugin, addBuildPlugin, extendViteConfig (deprecated), extendWebpackConfig, extendRspackConfigaddBuildPlugin takes an unplugin-style factory { vite, webpack, rspack } so one plugin works across builders. { dev, build, prepend } gate when it applies; { client, server } are deprecated for Vite in Nuxt 5 Nuxt 5 , where the plugin decides with applyToEnvironment.
ConfigupdateRuntimeConfig, useRuntimeConfig (build-time), updateAppConfig, extendNuxtSchema, setGlobalHeadMerge-aware writes into nuxt.options.runtimeConfig / appConfig; extendNuxtSchema registers a schema:extend hook.
Composition and compatibilityinstallModule (deprecated), hasNuxtModule, hasNuxtModuleCompatibility, getNuxtModuleVersion, checkNuxtCompatibility, assertNuxtCompatibility, hasNuxtCompatibility, isNuxtMajorVersion, getNuxtVersion, getNitroVersioninstallModule runs another module's setup inline, now; moduleDependencies is the declarative replacement. hasNuxtModule checks _installedModules and the modules array, so it also sees modules listed after you.
Context and resolutionuseNuxt, tryUseNuxt, getNuxtCtx, runWithNuxtContext, createResolver, resolvePath, resolveAlias, findPath, resolveFiles, useLogger, getLayerDirectories, ensureDependencyInstalleduseNuxt reads the current instance from an unctx context backed by AsyncLocalStorage (with a global fallback). That is why kit helpers work without passing nuxt around, and why calling one outside a module or hook throws.
Programmatic NuxtloadNuxt, buildNuxt, loadNuxtConfig, diffNuxtConfigWhat the CLI and @nuxt/test-utils use. Useful for build-level tests: load a fixture, inspect nuxt.options.
  • addPlugin prepends by default so module plugins run before the app's own plugins, which is what you want for providers and exactly what you do not want when you depend on something the app sets up. Use { append: true }, an order (lower runs first, user plugins default to 0, stay within -20 to 20) or dependsOn inside the plugin itself.
  • Kit never touches runtime. Nothing from @nuxt/kit may be imported in src/runtime/**; see Rules of the runtime directory.
  • Source to read: packages/kit/src/plugin.ts, template.ts, imports.ts, components.ts, nitro.ts, resolve.ts, module/define.ts, module/install.ts. Each is short. module/define.ts shows exactly how options, defaults, compatibility checks and dedupe are processed; module/install.ts shows moduleDependencies resolution and the lifecycle hooks.

How it works

src/module.ts
import {
  addComponentsDir, addImports, addNitroPlugin, addPlugin, addServerHandler,
  addServerImportsDir, createResolver, defineNuxtModule,
} from '@nuxt/kit'

export default defineNuxtModule({
  meta: { name: 'nuxt-team-toolkit', configKey: 'toolkit' },
  setup (_options, nuxt) {
    const { resolve } = createResolver(import.meta.url)

    // prepended: runs before the app's plugins (a provider other code relies on)
    addPlugin(resolve('./runtime/app/plugins/toolkit'))

    // appended and ordered: runs after the app's plugins (depends on the app's auth plugin)
    addPlugin({ src: resolve('./runtime/app/plugins/analytics.client'), order: 10 }, { append: true })

    // auto-imports and components: registered through imports:extend / components:dirs
    addImports({ name: 'useToolkit', from: resolve('./runtime/app/composables/useToolkit') })
    addComponentsDir({ path: resolve('./runtime/app/components'), prefix: 'Tk' })

    // server side: nuxt.options.serverHandlers, Nitro plugins, Nitro's own auto-imports
    addServerHandler({ route: '/api/_toolkit/health', handler: resolve('./runtime/server/api/health') })
    addNitroPlugin(resolve('./runtime/server/plugins/request-id'))
    addServerImportsDir(resolve('./runtime/server/utils'))

    // the primitive underneath every helper: plain mutation of nuxt.options
    nuxt.options.css.push(resolve('./runtime/app/assets/toolkit.css'))
  },
})

Why useNuxt() works in a build-world helper that never received nuxt:

src/build/registry.ts
import { useNuxt } from '@nuxt/kit'

export function registerToolkitFeature (name: string) {
  // resolved from the async context Nuxt runs module installation in; throws outside of it
  const nuxt = useNuxt()
  const features = (nuxt.options.runtimeConfig.public.toolkitFeatures ||= []) as string[]
  features.push(name)
}
Gotcha· It worked in the playground, then ran twice

addPlugin removes an existing entry with the same resolved src before inserting, so calling it twice is harmless. nuxt.options.css.push() and nuxt.options.plugins.push() are not deduplicated. A module listed by the layer and the project is deduplicated by meta.name, but an anonymous inline module is not, and its raw pushes run twice.

Gotcha· extendViteConfig with { client: true } is a Nuxt 5 problem

Nuxt 5 With the Vite Environment API a single Vite plugin serves both environments, so extendViteConfig, vite:extendConfig and the client / server options of addVitePlugin are deprecated. Write the plugin with applyToEnvironment(env => env.name === 'client') and configEnvironment instead; it works on Nuxt 4 today.

Verify before the interview:

addNitroPlugin (with { nitro2, nitro3 } variants) replaced addServerPlugin, which is kept as a deprecated alias for a deprecation window. Check which name the current kit docs lead with, and whether extendViteConfig is still exported.

docs ↗

Exercise

Exercise
  • Kit tour: use each helper in the Inject runtime code, Server side and Files and templates groups at least once, one commit each. In NOTES.md, write one sentence per helper about what it did internally (open the source file to confirm).
  • Break something deliberately: call useNuxt() from a runtime file and read the error. Explain to yourself why it cannot work.
  • Register two plugins, one with addPlugin(src) and one with addPlugin(src, { append: true }), and log the order in which they run relative to the playground's own plugin.

Be able to say

Be able to say· What does addPlugin do internally, and when is its default the wrong choice?

"It normalises the src (a string becomes an object, aliases are resolved, the extension is looked up), removes any existing plugin with the same path, infers mode from a .client or .server suffix, and prepends to nuxt.options.plugins. Prepending means module plugins run before the app's own plugins, which is usually what you want for providers and exactly what you do not want when you depend on something the app sets up. In that case I pass append: true, give the plugin an order, or declare dependsOn inside the plugin itself so the ordering is explicit rather than positional."