nuxt-modules
Use when creating Nuxt modules: (1) Published npm modules (@nuxtjs/, nuxt-), (2) Local project modules (modules/ directory), (3) Runtime extensions (components, composables, plugins), (4) Server extensions (API routes, middleware), (5) Releasing/publishing modules to npm, (6) Setting up CI/CD workflows for modules. Provides defineNuxtModule patterns, Kit utilities, hooks, E2E testing, and release automation.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install onmax-nuxt-skills-nuxt-modules
Repository
Skill path: skills/nuxt-modules
Use when creating Nuxt modules: (1) Published npm modules (@nuxtjs/, nuxt-), (2) Local project modules (modules/ directory), (3) Runtime extensions (components, composables, plugins), (4) Server extensions (API routes, middleware), (5) Releasing/publishing modules to npm, (6) Setting up CI/CD workflows for modules. Provides defineNuxtModule patterns, Kit utilities, hooks, E2E testing, and release automation.
Open repositoryBest for
Primary workflow: Run DevOps.
Technical facets: Full Stack, Backend, DevOps, Testing.
Target audience: everyone.
License: MIT.
Original source
Catalog source: SkillHub Club.
Repository owner: onmax.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install nuxt-modules into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/onmax/nuxt-skills before adding nuxt-modules to shared team environments
- Use nuxt-modules for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: nuxt-modules
description: "Use when creating Nuxt modules: (1) Published npm modules (@nuxtjs/, nuxt-), (2) Local project modules (modules/ directory), (3) Runtime extensions (components, composables, plugins), (4) Server extensions (API routes, middleware), (5) Releasing/publishing modules to npm, (6) Setting up CI/CD workflows for modules. Provides defineNuxtModule patterns, Kit utilities, hooks, E2E testing, and release automation."
license: MIT
---
# Nuxt Module Development
Guide for creating Nuxt modules that extend framework functionality.
**Related skills:** `nuxt` (basics), `vue` (runtime patterns)
## Quick Start
```bash
npx nuxi init -t module my-module
cd my-module && npm install
npm run dev # Start playground
npm run dev:build # Build in watch mode
npm run test # Run tests
```
## Available Guidance
- **[references/development.md](references/development.md)** - Module anatomy, defineNuxtModule, Kit utilities, hooks
- **[references/testing-and-publishing.md](references/testing-and-publishing.md)** - E2E testing, best practices, releasing, publishing
- **[references/ci-workflows.md](references/ci-workflows.md)** - Copy-paste CI/CD workflow templates
## Loading Files
**Consider loading these reference files based on your task:**
- [ ] [references/development.md](references/development.md) - if building module features, using defineNuxtModule, or working with Kit utilities
- [ ] [references/testing-and-publishing.md](references/testing-and-publishing.md) - if writing E2E tests, publishing to npm, or following best practices
- [ ] [references/ci-workflows.md](references/ci-workflows.md) - if setting up CI/CD workflows for your module
**DO NOT load all files at once.** Load only what's relevant to your current task.
## Module Types
| Type | Location | Use Case |
| --------- | ---------------- | -------------------------------- |
| Published | npm package | `@nuxtjs/`, `nuxt-` distribution |
| Local | `modules/` dir | Project-specific extensions |
| Inline | `nuxt.config.ts` | Simple one-off hooks |
## Project Structure
```
my-module/
├── src/
│ ├── module.ts # Entry point
│ └── runtime/ # Injected into user's app
│ ├── components/
│ ├── composables/
│ ├── plugins/
│ └── server/
├── playground/ # Dev testing
└── test/fixtures/ # E2E tests
```
## Resources
- [Module Guide](https://nuxt.com/docs/guide/going-further/modules)
- [Nuxt Kit](https://nuxt.com/docs/api/kit)
- [Module Starter](https://github.com/nuxt/starter/tree/module)
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/development.md
```markdown
# Module Development
Module anatomy, Kit utilities, and common patterns.
## defineNuxtModule
```ts
import { addPlugin, createResolver, defineNuxtModule } from '@nuxt/kit'
export interface ModuleOptions {
apiKey?: string
prefix?: string
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name: '@nuxtjs/example',
configKey: 'example',
compatibility: { nuxt: '>=3.0.0' }
},
defaults: {
apiKey: '',
prefix: 'My'
},
hooks: {
'app:error': err => console.error(err)
},
moduleDependencies: {
'@nuxtjs/tailwindcss': { version: '>=6.0.0', optional: true }
},
// Or as async function (Nuxt 4.3+)
async moduleDependencies(nuxt) {
const needsSupport = nuxt.options.runtimeConfig.public?.feature
return {
'@nuxtjs/tailwindcss': needsSupport ? {} : { optional: true }
}
},
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
addPlugin(resolve('./runtime/plugin'))
}
})
```
User configures via `configKey`:
```ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/example'],
example: { apiKey: 'xxx' }
})
```
## Critical: #imports in Published Modules
Auto-imports don't work in `node_modules`. Runtime files must explicitly import:
```ts
// src/runtime/composables/useMyFeature.ts
// Wrong - won't work in published module
// Right - explicit import
import { useRoute } from '#imports'
const route = useRoute()
const route = useRoute()
```
## Adding Plugins
```ts
import { addPlugin, createResolver, defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
addPlugin(resolve('./runtime/plugin'))
}
})
```
```ts
// src/runtime/plugin.ts
import { defineNuxtPlugin } from '#imports'
export default defineNuxtPlugin((nuxtApp) => {
return {
provide: { myHelper: (msg: string) => console.log(msg) }
}
})
```
**Async plugins (Nuxt 4.3+):** Lazy-load build plugins:
```ts
import { addVitePlugin, addWebpackPlugin } from '@nuxt/kit'
export default defineNuxtModule({
async setup() {
// Lazy-load only the bundler plugin needed
addVitePlugin(() => import('my-plugin/vite').then(r => r.default()))
addWebpackPlugin(() => import('my-plugin/webpack').then(r => r.default()))
}
})
```
## Adding Components
```ts
import { addComponent, addComponentsDir, createResolver, defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
// Single component
addComponent({
name: 'MyButton',
filePath: resolve('./runtime/components/MyButton.vue')
})
// Or entire directory with prefix
addComponentsDir({
path: resolve('./runtime/components'),
prefix: 'My' // <MyButton>, <MyCard>
})
}
})
```
## Adding Composables
```ts
import { addImports, addImportsDir, createResolver, defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
// Single or multiple
addImports([
{ name: 'useAuth', from: resolve('./runtime/composables/useAuth') },
{ name: 'useUser', from: resolve('./runtime/composables/useUser') }
])
// Or entire directory
addImportsDir(resolve('./runtime/composables'))
}
})
```
## Adding Server Routes
```ts
import { addServerHandler, createResolver, defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
addServerHandler({
route: '/api/_my-module/status',
handler: resolve('./runtime/server/api/status.get')
})
}
})
```
**Always prefix routes:** `/api/_my-module/` avoids conflicts.
**Server imports (Nuxt 4.3+):** Use `#server` alias in server files:
```ts
// runtime/server/api/users.ts
import { helper } from '#server/utils/helper' // Clean imports
```
## Runtime Config
```ts
export default defineNuxtModule({
setup(options, nuxt) {
// Public (client + server)
nuxt.options.runtimeConfig.public.myModule = { apiUrl: options.apiUrl }
// Private (server only)
nuxt.options.runtimeConfig.myModule = { apiKey: options.apiKey }
}
})
```
## Lifecycle Hooks
```ts
export default defineNuxtModule({
hooks: {
'pages:extend': (pages) => {
pages.push({ name: 'custom', path: '/custom', file: resolve('./runtime/pages/custom.vue') })
}
},
setup(options, nuxt) {
nuxt.hook('nitro:config', (nitroConfig) => {
nitroConfig.prerender ||= {}
nitroConfig.prerender.routes ||= []
nitroConfig.prerender.routes.push('/my-route')
})
// Cleanup on close
nuxt.hook('close', async () => {
await cleanup()
})
}
})
```
| Hook | When |
| -------------- | ------------------ |
| `ready` | Nuxt initialized |
| `modules:done` | All modules loaded |
| `pages:extend` | Modify pages array |
| `nitro:config` | Configure Nitro |
| `close` | Nuxt shutting down |
## Custom Hooks
```ts
export interface ModuleHooks {
'my-module:init': (config: MyConfig) => void
}
declare module '#app' {
interface RuntimeNuxtHooks extends ModuleHooks {}
}
export default defineNuxtModule({
setup(options, nuxt) {
nuxt.hook('modules:done', async () => {
await nuxt.callHook('my-module:init', { foo: 'bar' })
})
}
})
```
## Virtual Files (Templates)
```ts
import { addTemplate, defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
setup(options, nuxt) {
addTemplate({
filename: 'my-module/config.mjs',
getContents: () => `export const config = ${JSON.stringify(options)}`
})
}
})
```
Import: `import { config } from '#build/my-module/config.mjs'`
## Type Declarations
```ts
import { addTypeTemplate, defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
setup(options, nuxt) {
addTypeTemplate({
filename: 'types/my-module.d.ts',
getContents: () => `
declare module '#app' {
interface NuxtApp { $myHelper: (msg: string) => void }
}
export {}
`
})
}
})
```
## Logging & Errors
Use `consola.withTag` for consistent module logging:
```ts
import { consola } from 'consola'
const logger = consola.withTag('my-module')
export default defineNuxtModule({
setup(options, nuxt) {
logger.info('Initializing...')
logger.warn('Deprecated option used')
// Errors must include tag manually - consola doesn't add it
if (!options.apiKey) {
throw new Error('[my-module] `apiKey` option is required')
}
}
})
```
## Disabling Modules
**Set to `false` to disable (Nuxt 4.3+):**
```ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/tailwindcss'],
tailwindcss: false // Disable the module
})
```
**Disable inherited layer modules:**
```ts
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['../base-layer'],
disabledModules: ['@nuxt/image', '@sentry/nuxt/module']
})
```
Only works for modules from layers, not root project modules.
## Local Modules
For project-specific modules:
```ts
// modules/my-local-module/index.ts
import { defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
meta: { name: 'my-local-module' },
setup(options, nuxt) {
// Module logic
}
})
```
```ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['./modules/my-local-module']
})
```
## Quick Reference
| Task | Kit Function |
| ---------------- | --------------------------------------- |
| Add plugin | `addPlugin()` |
| Add component | `addComponent()` / `addComponentsDir()` |
| Add composable | `addImports()` / `addImportsDir()` |
| Add server route | `addServerHandler()` |
| Add server utils | `addServerImports()` |
| Virtual file | `addTemplate()` / `addServerTemplate()` |
| Add types | `addTypeTemplate()` |
| Add CSS | `nuxt.options.css.push()` |
## Resources
- [Nuxt Kit](https://nuxt.com/docs/api/kit)
- [Hooks](https://nuxt.com/docs/api/advanced/hooks)
```
### references/testing-and-publishing.md
```markdown
# Testing & Publishing
E2E testing, best practices, and publishing modules.
## E2E Testing Setup
```bash
npm install -D @nuxt/test-utils vitest
```
```ts
// vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'
export default defineVitestConfig({
test: { environment: 'nuxt' }
})
```
## Test Fixtures
Create a minimal Nuxt app that uses your module:
```ts
// test/fixtures/basic/nuxt.config.ts
import MyModule from '../../../src/module'
export default defineNuxtConfig({
modules: [MyModule],
myModule: { enabled: true }
})
```
```vue
<!-- test/fixtures/basic/pages/index.vue -->
<template>
<MyButton>Click me</MyButton>
</template>
```
## Writing Tests
```ts
import { fileURLToPath } from 'node:url'
import { $fetch, setup } from '@nuxt/test-utils/e2e'
// test/basic.test.ts
import { describe, expect, it } from 'vitest'
describe('basic', async () => {
await setup({
rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url))
})
it('renders component', async () => {
const html = await $fetch('/')
expect(html).toContain('Click me')
})
it('api works', async () => {
const data = await $fetch('/api/_my-module/status')
expect(data).toEqual({ status: 'ok' })
})
})
```
## Manual Testing
```bash
# In module directory
npm pack
# In test project
npm install /path/to/my-module-1.0.0.tgz
```
---
## Best Practices
### Async Setup
Keep setup fast. Nuxt warns if setup exceeds 1 second.
```ts
// Wrong - blocking
async setup(options, nuxt) {
const data = await fetchRemoteConfig() // Slow!
}
// Right - defer to hooks
setup(options, nuxt) {
nuxt.hook('ready', async () => {
const data = await fetchRemoteConfig()
})
}
```
### Prefix All Exports
Avoid naming conflicts:
| Type | Wrong | Right |
| ------------- | ------------ | ----------------- |
| Components | `<Button>` | `<FooButton>` |
| Composables | `useData()` | `useFooData()` |
| Server routes | `/api/track` | `/api/_foo/track` |
| Plugins | `$helper` | `$fooHelper` |
### Lifecycle Hooks
For one-time setup tasks:
```ts
export default defineNuxtModule({
meta: { name: 'my-module', version: '2.0.0' },
async onInstall(nuxt) {
await generateInitialConfig(nuxt.options.rootDir)
},
async onUpgrade(options, nuxt, previousVersion) {
if (semver.lt(previousVersion, '2.0.0')) {
await migrateFromV1()
}
}
})
```
### TypeScript + ESM Only
```ts
// Always export typed options
// ESM only - no CommonJS
import { something } from 'package'
export interface ModuleOptions {
apiKey: string
debug?: boolean
} // Right
const { something } = require('package') // Wrong
```
### Error Messages
```ts
setup(options, nuxt) {
if (!options.apiKey) {
throw new Error('[my-module] `apiKey` option is required')
}
}
```
---
## Releasing
Two-step: local bump → CI publish. CI must pass before tag push.
### Setup
```bash
pnpm add -D bumpp
```
```json
{
"scripts": {
"release": "bumpp && git push --follow-tags"
}
}
```
### Flow
```bash
pnpm release # Prompts version, commits, tags, pushes
# → CI release.yml triggers on v* tag → npm publish + GitHub release
```
### Commit Conventions
| Prefix | Bump |
| ------------------------------ | ----- |
| `feat:` | minor |
| `fix:`, `chore:`, `docs:` | patch |
| `feat!:` or `BREAKING CHANGE:` | major |
### CI Workflows
Three workflows for complete CI/CD:
| File | Trigger | Purpose |
| ------------- | -------- | ------------------------------- |
| `ci.yml` | push/PR | lint, typecheck, test |
| `pkg.yml` | push/PR | preview packages via pkg-pr-new |
| `release.yml` | tag `v*` | npm publish + GitHub release |
**Copy templates from:** [references/ci-workflows.md](references/ci-workflows.md)
---
## Publishing
### Naming Conventions
| Scope | Example | Description |
| ---------- | --------------------- | ------------------------------------ |
| `@nuxtjs/` | `@nuxtjs/tailwindcss` | Community modules (nuxt-modules org) |
| `nuxt-` | `nuxt-my-module` | Third-party modules |
| `@org/` | `@myorg/nuxt-auth` | Organization scoped |
### Documentation Checklist
- [ ] **Why** - What problem does this solve?
- [ ] **Installation** - How to install and configure?
- [ ] **Usage** - Basic examples
- [ ] **Options** - All config options with types
- [ ] **Demo** - StackBlitz link
### Version Compatibility
```ts
meta: {
compatibility: { nuxt: '>=3.0.0' }
}
```
Use "X for Nuxt" naming, not "X for Nuxt 3" — let `meta.compatibility` handle versions.
## Resources
- [@nuxt/test-utils](https://nuxt.com/docs/getting-started/testing)
- [Publishing Modules](https://nuxt.com/docs/guide/going-further/modules#publishing)
- [Nuxt Modules Directory](https://nuxt.com/modules)
```
### references/ci-workflows.md
```markdown
# CI Workflow Templates
Copy-paste templates for GitHub Actions.
## Contents
- [ci.yml](#ciyml) - Lint, typecheck, test
- [pkg.yml](#pkgyml) - Preview packages via pkg-pr-new
- [release.yml](#releaseyml) - npm publish + GitHub release
- [npm Trusted Publishing Setup](#npm-trusted-publishing-setup)
---
## ci.yml
Runs lint, typecheck, and tests on every push/PR/tag.
```yaml
name: ci
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install
- run: pnpm dev:prepare
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm test
```
## pkg.yml
Publishes preview packages for every PR via pkg-pr-new.
```yaml
name: pkg.new
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
pkg:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install
- run: pnpm dev:prepare
- run: pnpm prepack
- run: pnpm dlx pkg-pr-new publish
```
## release.yml
Triggered by tag push. Waits for CI, then publishes to npm via OIDC + creates GitHub release.
```yaml
name: release
permissions:
id-token: write
contents: write
actions: read
on:
push:
tags:
- 'v*'
jobs:
wait-for-ci:
runs-on: ubuntu-latest
steps:
- name: Wait for CI to complete
uses: lewagon/[email protected]
with:
ref: ${{ github.sha }}
check-name: ci
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
release:
needs: wait-for-ci
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
registry-url: 'https://registry.npmjs.org'
- run: pnpm install
- run: pnpm dev:prepare
- run: pnpm prepack
- name: GitHub Release
run: pnpm dlx changelogithub
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Publish to npm
run: npm publish --provenance --access public
```
## npm Trusted Publishing Setup (OIDC)
**Preferred method** - No `NPM_TOKEN` secret needed. Uses OIDC for secure, tokenless authentication.
**See also:** [ts-library/ci-workflows.md](../../ts-library/references/ci-workflows.md) for general TypeScript library CI patterns.
### Requirements
1. **Node.js 24+** (npm 11.5.1+ required for OIDC - Node 22 has npm 10.x which fails)
2. **Workflow permissions**: `id-token: write`
3. **Publish command**: must include `--provenance` flag
4. **package.json**: must have `repository` field for provenance verification
5. **npm 2FA setting**: "Require 2FA or granular access token" (first option, allows tokens)
### package.json Requirements
Each package must have a `repository` field or provenance verification fails:
```json
{
"name": "my-package",
"repository": { "type": "git", "url": "git+https://github.com/org/repo.git" }
}
```
### Setup Steps
1. **Open package settings**: `https://www.npmjs.com/package/<PACKAGE_NAME>/access`
2. **Scroll to "Publishing access"** section
3. **Click "Add GitHub Actions"** under Trusted Publishers
4. **Fill in the form**:
- Owner: `<github-org-or-username>`
- Repository: `<repo-name>`
- Workflow file: `release.yml`
- Environment: _(leave empty)_
5. **Click "Add"**
Repeat for each package in your monorepo.
### Troubleshooting
| Error | Cause | Fix |
| --------------------------------------------- | ------------------------ | -------------------------------------------------------------- |
| "Access token expired or revoked" E404 | npm version too old | Use Node.js 24 (npm 11.5.1+) |
| ENEEDAUTH | Missing registry-url | Add `registry-url: 'https://registry.npmjs.org'` to setup-node |
| "repository.url is empty" E422 | Missing repository field | Add `repository` to package.json |
| "npm/xyz not configured as trusted publisher" | Mismatch in config | Check owner, repo, workflow filename match exactly |
### Verify Setup
The workflow uses OIDC when:
- `id-token: write` permission is set
- `--provenance` flag is used
- No `NODE_AUTH_TOKEN` env var is set
npm automatically detects GitHub Actions and authenticates via OIDC.
```