@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.
| Group | Helpers | Under the hood |
|---|---|---|
| Inject runtime code | addPlugin, addPluginTemplate, addImports, addImportsDir, addImportsSources, addComponent, addComponentsDir, addComponentExports, addRouteMiddleware, addLayout, extendPages, extendRouteRules, addPrerenderRoutes | addPlugin 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 side | addServerHandler, addDevServerHandler, addNitroPlugin (addServerPlugin is a deprecated alias), addServerImports, addServerImportsDir, addServerScanDir, addServerTemplate, useNitro, tryUseNitro | Handlers 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 templates | addTemplate, addTypeTemplate, addServerTemplate, updateTemplates, writeTypes | Templates 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 config | addVitePlugin, addWebpackPlugin, addRspackPlugin, addBuildPlugin, extendViteConfig (deprecated), extendWebpackConfig, extendRspackConfig | addBuildPlugin 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. |
| Config | updateRuntimeConfig, useRuntimeConfig (build-time), updateAppConfig, extendNuxtSchema, setGlobalHead | Merge-aware writes into nuxt.options.runtimeConfig / appConfig; extendNuxtSchema registers a schema:extend hook. |
| Composition and compatibility | installModule (deprecated), hasNuxtModule, hasNuxtModuleCompatibility, getNuxtModuleVersion, checkNuxtCompatibility, assertNuxtCompatibility, hasNuxtCompatibility, isNuxtMajorVersion, getNuxtVersion, getNitroVersion | installModule 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 resolution | useNuxt, tryUseNuxt, getNuxtCtx, runWithNuxtContext, createResolver, resolvePath, resolveAlias, findPath, resolveFiles, useLogger, getLayerDirectories, ensureDependencyInstalled | useNuxt 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 Nuxt | loadNuxt, buildNuxt, loadNuxtConfig, diffNuxtConfig | What 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.@nuxt/kit may be imported in src/runtime/**; see Rules of the runtime directory.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.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:
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)
}
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.
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.
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.
NOTES.md, write one sentence per helper about what it did internally (open the source file to confirm).useNuxt() from a runtime file and read the error. Explain to yourself why it cannot work.addPlugin(src) and one with addPlugin(src, { append: true }), and log the order in which they run relative to the playground's own plugin."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."
The mental model
What defineNuxtModule does with your definition, the three worlds a line of module code can live in, how Nuxt orders and deduplicates modules, and when onInstall and onUpgrade fire.
Crossing the boundary
The four channels that carry a module option from the build world into the browser — public and private runtime config, appConfig and generated templates — and how to choose between them.