Writing a module
Setup checklist
Declare a module's dependencies and write its setup() with the module-kit helpers, in order.
This page is for module authors with a package from Scaffold a module. It
covers the two parts of src/module.ts that every module has: the modules it depends on, and the setup() that
registers its pages, routes, translations and tables.
Declare dependencies
List every module yours needs in moduleDependencies. Nuxt then installs and orders them, so the
host app lists only your module.
export default defineNuxtModule<ModuleOptions>({
meta: { name: '@frigear-nu/my-module', configKey: 'myModule' },
moduleDependencies: {
'@nuxthub/core': { version: '>=0.10' },
'@frigear-nu/module-kit': { version: '>=0.0.1' },
'@frigear-nu/auth': { version: '>=0.0.1' },
'@frigear-nu/menu': { version: '>=0.0.1', optional: true },
},
// ...
})
| Kind | In moduleDependencies | In package.json |
|---|---|---|
| Required | { version: '>=0.0.1' } | dependencies with workspace:^ (not workspace:*, which publishes an exact version). |
| Optional | { version: '>=0.0.1', optional: true } | An optional peerDependencies entry (with peerDependenciesMeta) and a devDependencies entry, so tests can use it. |
Nuxt toolchain (nuxt, @nuxt/kit, @nuxt/ui, @nuxthub/core, drizzle-orm, vitest, ...) | - | catalog:, from the catalog of the root pnpm-workspace.yaml. |
- The catalog. Every package shares one copy of each toolchain package. A version of its own per package multiplies the installed Nuxt copies and slows every boot.
- Module Kit always. Its entry provides the
useModuleTauto-import. Add@nuxthub/coreonly when the module has tables. - Optional modules. An installed optional module is set up before yours, so
hasNuxtModule('@frigear-nu/menu')insetup()does not depend on the order of the host app'smodules. Guard every use of it. Load its helpers with a dynamicimport()inside the guard, for exampleawait import('@frigear-nu/widget/utils'). - Options.
moduleDependenciesmay be a function ofnuxtwhen a dependency depends on options, as@frigear-nu/usersdoes for MCP.
The setup() checklist
Most modules call the same helpers, in this order:
import { addServerImportsDir, createResolver, defineNuxtModule } from '@nuxt/kit'
import {
addModulePages,
addModuleRoutes,
inlineModuleRuntime,
registerDatabaseTable,
registerLocales,
registerModule,
registerTailwindSource,
resolvePagePath,
} from '@frigear-nu/module-kit/utils'
const MODULE_NAME = '@frigear-nu/my-module'
export default defineNuxtModule<ModuleOptions>({
// meta, defaults, moduleDependencies ...
setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
// 0. Bundling and server auto-imports of ./runtime, also when the module is installed from npm.
inlineModuleRuntime(nuxt, resolver)
// 1. Make the module known to the rest of the app.
registerModule(MODULE_NAME, nuxt)
// 2. Classes used in the module's components must be generated by Tailwind.
registerTailwindSource(nuxt, resolver.resolve('./runtime/app'))
// 3. Translations: da.json and en.json, namespaced by module.
registerLocales(nuxt, resolver.resolve('./runtime/i18n/lang'))
// 4. Pages are opt-in or overridable through settings.pages.*.
addModulePages(MODULE_NAME, resolver, [
{
name: 'my-module',
path: resolvePagePath(options.settings?.pages?.index, '/my-module'),
file: './runtime/app/pages/index.vue',
},
])
// 5. Server routes and server auto-imports.
addServerImportsDir(resolver.resolve('./runtime/server/utils'))
addModuleRoutes(resolver, [
{ method: 'get', path: 'things', file: 'things.get' },
{ method: 'get', path: '/api/admin/my-module/things', file: 'admin/things.get' },
], { base: '/api/my-module' })
// 6. Tables: a plain drizzle file per dialect. No `name`, nothing to await.
registerDatabaseTable(nuxt, {
resolver,
schema: dialect => `./runtime/server/db/schema.${dialect}`,
})
},
})
Why each call is there:
- Names.
registerModuletakes the full package name.moduleUrlandmoduleApiUrltake the short name ('my-module') and kebab-case it into the path. inlineModuleRuntime. Call it in every module with aruntimefolder. Nitro auto-imports only into thenode_modulesfiles of layers. Without it, an installed module's server code that uses auto-imports, such asuseNitroApp, fails in the host app, and its virtual modules do not resolve.- App imports. App code in
runtime(components, composables, plugins, pages) imports from#imports:import { computed, useModuleT } from '#imports'. Nuxt does not auto-import into a module innode_modules. Theno-undefrule of ESLint config fails the lint when an import is missing. Names from optional modules come from your ownaddOptionalImportsvirtual module instead. - Access control. Register what your routes check with
addPermission(plusaddScope,addRole,addRoleRequirement,addPoliciesas needed) and check it withauthorize, not the session helpers. List@frigear-nu/authinmoduleDependencies. Access control covers it. - Routes. Register every route through one
addModuleRoutescall.fileis relative to./runtime/server/routes/, andpathis joined ontobaseunless it starts with/. Every module's API follows the same URL, error and response rules, in HTTP routes. - Optional areas. Menus, widgets and admin pages come from optional modules. Use
addOptionalMenuSourceand the guarded imports in Module Kit.
Next steps
- Database tables – add the module's tables.
- HTTP routes – shape the routes you just registered.
- Access control – register the module's permissions.
- Testing – boot the module in a test fixture.