Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Nuxt 3.x reference covering SSR, file-based routing, auto-imports, server routes, and Nitro deployment.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/advanced-module-authoring.md
1---2name: module-authoring3description: Complete guide to creating publishable Nuxt modules with best practices4---56# Module Authoring78This guide covers creating publishable Nuxt modules with proper structure, type safety, and best practices.910## Module Structure1112Recommended structure for a publishable module:1314```15my-nuxt-module/16├── src/17│ ├── module.ts # Module entry18│ └── runtime/19│ ├── components/ # Vue components20│ ├── composables/ # Composables21│ ├── plugins/ # Nuxt plugins22│ └── server/ # Server handlers23├── playground/ # Development app24├── package.json25└── tsconfig.json26```2728## Module Definition2930### Basic Module with Type-safe Options3132```ts33// src/module.ts34import { defineNuxtModule, createResolver, addPlugin, addComponent, addImports } from '@nuxt/kit'3536export interface ModuleOptions {37prefix?: string38apiKey: string39enabled?: boolean40}4142export default defineNuxtModule<ModuleOptions>({43meta: {44name: 'my-module',45configKey: 'myModule',46compatibility: {47nuxt: '>=3.0.0',48},49},50defaults: {51prefix: 'My',52enabled: true,53},54setup(options, nuxt) {55if (!options.enabled) return5657const { resolve } = createResolver(import.meta.url)5859// Module setup logic here60},61})62```6364### Using `.with()` for Strict Type Inference6566When you need TypeScript to infer that default values are always present:6768```ts69import { defineNuxtModule } from '@nuxt/kit'7071interface ModuleOptions {72apiKey: string73baseURL: string74timeout?: number75}7677export default defineNuxtModule<ModuleOptions>().with({78meta: {79name: '@nuxtjs/my-api',80configKey: 'myApi',81},82defaults: {83baseURL: 'https://api.example.com',84timeout: 5000,85},86setup(resolvedOptions, nuxt) {87// resolvedOptions.baseURL is guaranteed to be string (not undefined)88// resolvedOptions.timeout is guaranteed to be number (not undefined)89},90})91```9293## Adding Runtime Assets9495### Components9697```ts98import { addComponent, addComponentsDir, createResolver } from '@nuxt/kit'99100export default defineNuxtModule({101setup() {102const { resolve } = createResolver(import.meta.url)103104// Single component105addComponent({106name: 'MyButton',107filePath: resolve('./runtime/components/MyButton.vue'),108})109110// Component directory with prefix111addComponentsDir({112path: resolve('./runtime/components'),113prefix: 'My',114pathPrefix: false,115})116},117})118```119120### Composables and Auto-imports121122```ts123import { addImports, addImportsDir, createResolver } from '@nuxt/kit'124125export default defineNuxtModule({126setup() {127const { resolve } = createResolver(import.meta.url)128129// Single import130addImports({131name: 'useMyUtil',132from: resolve('./runtime/composables/useMyUtil'),133})134135// Directory of composables136addImportsDir(resolve('./runtime/composables'))137},138})139```140141### Plugins142143```ts144import { addPlugin, addPluginTemplate, createResolver } from '@nuxt/kit'145146export default defineNuxtModule({147setup(options) {148const { resolve } = createResolver(import.meta.url)149150// Static plugin file151addPlugin({152src: resolve('./runtime/plugins/myPlugin'),153mode: 'client', // 'client', 'server', or 'all'154})155156// Dynamic plugin with generated code157addPluginTemplate({158filename: 'my-module-plugin.mjs',159getContents: () => `160import { defineNuxtPlugin } from '#app/nuxt'161162export default defineNuxtPlugin({163name: 'my-module',164setup() {165const config = ${JSON.stringify(options)}166// Plugin logic167}168})`,169})170},171})172```173174## Server Extensions175176### Server Handlers177178```ts179import { addServerHandler, addServerScanDir, createResolver } from '@nuxt/kit'180181export default defineNuxtModule({182setup() {183const { resolve } = createResolver(import.meta.url)184185// Single handler186addServerHandler({187route: '/api/my-endpoint',188handler: resolve('./runtime/server/api/my-endpoint'),189})190191// Scan entire server directory (api/, routes/, middleware/, utils/)192addServerScanDir(resolve('./runtime/server'))193},194})195```196197### Server Composables198199```ts200import { addServerImports, addServerImportsDir, createResolver } from '@nuxt/kit'201202export default defineNuxtModule({203setup() {204const { resolve } = createResolver(import.meta.url)205206// Single server import207addServerImports({208name: 'useServerUtil',209from: resolve('./runtime/server/utils/useServerUtil'),210})211212// Server composables directory213addServerImportsDir(resolve('./runtime/server/composables'))214},215})216```217218### Nitro Plugin219220```ts221import { addServerPlugin, createResolver } from '@nuxt/kit'222223export default defineNuxtModule({224setup() {225const { resolve } = createResolver(import.meta.url)226addServerPlugin(resolve('./runtime/server/plugin'))227},228})229```230231```ts232// runtime/server/plugin.ts233import { defineNitroPlugin } from 'nitropack/runtime'234235export default defineNitroPlugin((nitroApp) => {236nitroApp.hooks.hook('request', (event) => {237console.log('Request:', event.path)238})239})240```241242## Templates and Virtual Files243244### Generate Virtual Files245246```ts247import { addTemplate, addTypeTemplate, addServerTemplate, createResolver } from '@nuxt/kit'248249export default defineNuxtModule({250setup(options, nuxt) {251const { resolve } = createResolver(import.meta.url)252253// Client/build virtual file (accessible via #build/my-config.mjs)254addTemplate({255filename: 'my-config.mjs',256getContents: () => `export default ${JSON.stringify(options)}`,257})258259// Type declarations260addTypeTemplate({261filename: 'types/my-module.d.ts',262getContents: () => `263declare module '#my-module' {264export interface Config {265apiKey: string266}267}`,268})269270// Nitro virtual file (accessible in server routes)271addServerTemplate({272filename: '#my-module/config.mjs',273getContents: () => `export const config = ${JSON.stringify(options)}`,274})275},276})277```278279### Access Virtual Files280281```ts282// In runtime plugin283// @ts-expect-error - virtual file284import config from '#build/my-config.mjs'285286// In server routes287import { config } from '#my-module/config.js'288```289290## Extending Pages and Routes291292```ts293import { extendPages, extendRouteRules, addRouteMiddleware, createResolver } from '@nuxt/kit'294295export default defineNuxtModule({296setup() {297const { resolve } = createResolver(import.meta.url)298299// Add pages300extendPages((pages) => {301pages.push({302name: 'my-page',303path: '/my-route',304file: resolve('./runtime/pages/MyPage.vue'),305})306})307308// Add route rules (caching, redirects, etc.)309extendRouteRules('/api/**', {310cache: { maxAge: 60 },311})312313// Add middleware314addRouteMiddleware({315name: 'my-middleware',316path: resolve('./runtime/middleware/myMiddleware'),317global: true,318})319},320})321```322323## Module Dependencies324325Declare dependencies on other modules with version constraints:326327```ts328export default defineNuxtModule({329meta: {330name: 'my-module',331},332moduleDependencies: {333'@nuxtjs/tailwindcss': {334version: '>=6.0.0',335// Set defaults (user can override)336defaults: {337exposeConfig: true,338},339// Force specific options340overrides: {341viewer: false,342},343},344'@nuxtjs/i18n': {345optional: true, // Won't fail if not installed346defaults: {347defaultLocale: 'en',348},349},350},351setup() {352// Dependencies are guaranteed to be set up before this runs353},354})355```356357### Dynamic Dependencies358359```ts360moduleDependencies(nuxt) {361const deps: Record<string, any> = {362'@nuxtjs/tailwindcss': { version: '>=6.0.0' },363}364365if (nuxt.options.ssr) {366deps['@nuxtjs/html-validator'] = { optional: true }367}368369return deps370}371```372373## Lifecycle Hooks374375Requires `meta.name` and `meta.version`:376377```ts378export default defineNuxtModule({379meta: {380name: 'my-module',381version: '1.2.0',382},383onInstall(nuxt) {384// First-time setup385console.log('Module installed for the first time')386},387onUpgrade(nuxt, options, previousVersion) {388// Version upgrade migrations389console.log(`Upgrading from ${previousVersion}`)390},391setup(options, nuxt) {392// Regular setup runs every build393},394})395```396397## Extending Configuration398399```ts400export default defineNuxtModule({401setup(options, nuxt) {402// Add CSS403nuxt.options.css.push('my-module/styles.css')404405// Add runtime config406nuxt.options.runtimeConfig.public.myModule = {407apiUrl: options.apiUrl,408}409410// Extend Vite config411nuxt.options.vite.optimizeDeps ||= {}412nuxt.options.vite.optimizeDeps.include ||= []413nuxt.options.vite.optimizeDeps.include.push('some-package')414415// Add build transpile416nuxt.options.build.transpile.push('my-package')417},418})419```420421## Using Hooks422423```ts424export default defineNuxtModule({425// Declarative hooks426hooks: {427'components:dirs': (dirs) => {428dirs.push({ path: '~/extra' })429},430},431432setup(options, nuxt) {433// Programmatic hooks434nuxt.hook('pages:extend', (pages) => {435// Modify pages436})437438nuxt.hook('imports:extend', (imports) => {439imports.push({ name: 'myHelper', from: 'my-package' })440})441442nuxt.hook('nitro:config', (config) => {443// Modify Nitro config444})445446nuxt.hook('vite:extendConfig', (config) => {447// Modify Vite config448})449},450})451```452453## Path Resolution454455```ts456import { createResolver, resolvePath, findPath } from '@nuxt/kit'457458export default defineNuxtModule({459async setup(options, nuxt) {460// Resolver relative to module461const { resolve } = createResolver(import.meta.url)462463const pluginPath = resolve('./runtime/plugin')464465// Resolve with extensions and aliases466const entrypoint = await resolvePath('@some/package')467468// Find first existing file469const configPath = await findPath([470resolve('./config.ts'),471resolve('./config.js'),472])473},474})475```476477## Module Package.json478479```json480{481"name": "my-nuxt-module",482"version": "1.0.0",483"type": "module",484"exports": {485".": {486"import": "./dist/module.mjs",487"require": "./dist/module.cjs"488}489},490"main": "./dist/module.cjs",491"module": "./dist/module.mjs",492"types": "./dist/types.d.ts",493"files": ["dist"],494"scripts": {495"dev": "nuxi dev playground",496"build": "nuxt-module-build build",497"prepare": "nuxt-module-build build --stub"498},499"dependencies": {500"@nuxt/kit": "^3.0.0"501},502"devDependencies": {503"@nuxt/module-builder": "latest",504"nuxt": "^3.0.0"505}506}507```508509## Disabling Modules510511Users can disable a module via config key:512513```ts514// nuxt.config.ts515export default defineNuxtConfig({516// Disable entirely517myModule: false,518519// Or with options520myModule: {521enabled: false,522},523})524```525526## Development Workflow5275281. **Create module**: `npx nuxi init -t module my-module`5292. **Develop**: `npm run dev` (runs playground)5303. **Build**: `npm run build`5314. **Test**: `npm run test`532533## Best Practices534535- Use `createResolver(import.meta.url)` for all path resolution536- Prefix components to avoid naming conflicts537- Make options type-safe with `ModuleOptions` interface538- Use `moduleDependencies` instead of `installModule`539- Provide sensible defaults for all options540- Add compatibility requirements in `meta.compatibility`541- Use virtual files for dynamic configuration542- Separate client/server plugins appropriately543544<!--545Source references:546- https://nuxt.com/docs/api/kit/modules547- https://nuxt.com/docs/api/kit/components548- https://nuxt.com/docs/api/kit/autoimports549- https://nuxt.com/docs/api/kit/plugins550- https://nuxt.com/docs/api/kit/templates551- https://nuxt.com/docs/api/kit/nitro552- https://nuxt.com/docs/api/kit/pages553- https://nuxt.com/docs/api/kit/resolving554-->555