Back to skills
SkillHub ClubRun DevOpsFull StackBackendDevOps

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.

Stars
597
Hot score
99
Updated
March 20, 2026
Overall rating
C4.2
Composite score
4.2
Best-practice grade
B75.6

Install command

npx @skill-hub/cli install onmax-nuxt-skills-nuxt-modules

Repository

onmax/nuxt-skills

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 repository

Best 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

Claude CodeCodex CLIGemini CLIOpenCode

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.

```

nuxt-modules | SkillHub