How to Build a Nuxt Module: Structure, Options, and Publishing

You copy the same plugin, composables, and config across projects. A Nuxt module lets you package that setup once so every app gets the same behavior with one install. This guide shows a practical module that registers a plugin, composables, and components, passes options to runtime, tests in a playground, and ships clean to npm.
What a Nuxt module is and when to write one
A Nuxt module runs at dev and build time to extend Nuxt. It can register plugins and composables, add components, ship server handlers to Nitro, tweak Vite and Nitro config, and inject runtime config for the host app. Choose a module when you want to:
- Ship a reusable feature like analytics, feature flags, billing UI, or an API client.
- Enforce conventions across a team, for example a UI kit, linting, and default build config.
- Hide integration details behind a single options object so app code stays clean.
Plugins are for one app. A module is for many apps and should install with zero or minimal manual wiring.
Build a Nuxt module step by step
1) Scaffold the module
Use the official starter. It includes TypeScript, unbuild, and a ready playground.
npx nuxi init -t module nuxt-awesome
cd nuxt-awesome
pnpm install # or npm / yarnYou get src/module.ts for the entry, a runtime/ folder for code that runs inside the host app, and a playground/ Nuxt app for local testing.
2) Define metadata and options
Give the module a name, a config key shown in nuxt.config.ts, and sensible defaults. Use defineNuxtModule from @nuxt/kit.
/* src/module.ts */ import { defineNuxtModule, addPlugin, addImportsDir, addComponentsDir, addServerHandler, createResolver } from '@nuxt/kit'export interface ModuleOptions { enabled?: boolean apiBase?: string }
export default defineNuxtModule<ModuleOptions>({ meta: { name: 'nuxt-awesome', configKey: 'awesome' }, defaults: { enabled: true, apiBase: '/api' }, setup (options, nuxt) { const resolver = createResolver(import.meta.url)
// Ensure runtime code is transpiled in host apps nuxt.options.build.transpile.push(resolver.resolve('runtime')) // Pass selected options to runtime config nuxt.options.runtimeConfig.public = { ...nuxt.options.runtimeConfig.public, awesome: { apiBase: options.apiBase } } // Register plugin, composables, and components addPlugin(resolver.resolve('runtime/plugin')) addImportsDir(resolver.resolve('runtime/composables')) addComponentsDir({ path: resolver.resolve('runtime/components'), pathPrefix: false }) // Optional: add a server handler via Nitro addServerHandler({ route: '/awesome/ping', handler: resolver.resolve('runtime/server/handlers/ping') }) // Example: expose compile-time flags to Vite nuxt.hooks.hook('vite:extendConfig', (config) => { config.define ||= {} // @ts-ignore config.define.__AWESOME_ENABLED__ = JSON.stringify(!!options.enabled) })
} })
This makes your module configurable via awesome: { ... } in a host app’s nuxt.config.ts.
3) Add runtime code: plugin, composable, and handler
Runtime code lives under runtime/. Keep the public API small and stable.
/* runtime/plugin.ts */ import { defineNuxtPlugin, useRuntimeConfig } from '#app'export default defineNuxtPlugin(() => { const { public: { awesome } } = useRuntimeConfig()
const client = { base: awesome?.apiBase || '/api', async ping () { const res = await fetch(this.base + '/ping') return res.ok } }
return { provide: { awesome: client } } })
/* runtime/composables/useAwesome.ts */
import { useNuxtApp, useRuntimeConfig } from '#app'
export function useAwesome () {
const { $awesome } = useNuxtApp()
const { public: { awesome } } = useRuntimeConfig()
return { client: $awesome, config: awesome }
}/* runtime/server/handlers/ping.ts */
export default defineEventHandler(() => ({ ok: true }))For client-only or server-only behavior, create plugin.client or plugin.server files and register the right one.
4) Expose components or server routes
If your feature ships UI or extra routes, place them in runtime/components or runtime/server and register as needed.
/* runtime/components/AwesomeBadge.vue */
<template><span class="awesome-badge">Awesome</span></template>
<script setup lang="ts"></script>
<style scoped>.awesome-badge{padding:4px 8px;border:1px solid #ddd;border-radius:4px;}</style>Prefer small, focused components and keep their props stable since host apps may rely on them across versions.
5) Use it in the playground
The scaffold includes a playground/ app wired to the local module. Run both together.
/* playground/nuxt.config.ts */ import MyModule from '../src/module'
export default defineNuxtConfig({ modules: MyModule, awesome: { apiBase: '/api' } })
pnpm -r dev # builds the module and starts the playgroundCreate a page and call your composable.
/* playground/pages/index.vue */ <script setup lang="ts"> const { client } = useAwesome() const ok = await client.ping().catch(() => false) </script>
<template> <div>Ping: </div> <AwesomeBadge /> </template>
If the playground seems stuck on an old build, restart it or run pnpm -r dev again so changes in src/ rebuild.
6) Add types and DX polish
Type your injections so host apps get IntelliSense. Also export your options type.
/* src/types.d.ts */ import type { ModuleOptions } from './module'declare module '#app' { interface NuxtApp { $awesome: { base: string; ping: () => Promise<boolean> } } }
export type { ModuleOptions as AwesomeModuleOptions }
Point types to the built declarations and publish only the build output. Consider an exports map so consumers cannot import sources by accident.
{
"name": "nuxt-awesome",
"version": "0.1.0",
"type": "module",
"main": "dist/module.mjs",
"types": "dist/types.d.ts",
"exports": {
".": {
"types": "./dist/types.d.ts",
"import": "./dist/module.mjs"
},
"./runtime/*": "./dist/runtime/*"
},
"files": ["dist"],
"sideEffects": false,
"peerDependencies": { "nuxt": "^3.0.0" },
"scripts": {
"build": "unbuild",
"dev": "pnpm -r --parallel dev",
"prepublishOnly": "pnpm build"
}
}If your runtime ships CSS, either import it from the host app or set a narrow sideEffects array so tree-shaking does not drop needed styles.
7) Package and publish
Build, version, and publish to npm. Write a README with install, options, examples, and a changelog link.
pnpm build
npm pack # inspect the tarball contents
npm version patch # or minor / major
npm publish --access publicTest installation in a fresh Nuxt app created with npx nuxi init. Verify that module options → runtimeConfig works, server handlers mount, and your types resolve in the editor.
Where modules fit in SaaS and AI projects
For a SaaS, a module is the right place to package shared concerns across properties. Examples:
- Billing and entitlements: wrap your payment provider client, expose helpers like
usePlan()andhasFeature('x'), and register a secure webhook route. - Auth glue: auto-register a plugin that syncs session state to a composable, and add route rules for protected pages.
- i18n defaults: ship a locale detector, default messages, and a directive for number and date formatting.
- Marketing baseline: add analytics, SEO helpers, and consistent components like
<PricingTable />and<CookieBanner />.
If you want to ship fast, pair your module with a Nuxt SaaS starter kit or a Nuxt boilerplate that already includes authentication, protected routes, an admin area, payments and checkout, i18n, transactional emails, analytics, SEO tools, cron jobs, and a landing page. Your module can focus on domain logic while the base handles foundation work.
Building an AI tool for the web follows the same pattern. Wrap your model provider and configuration in a module. Expose a composable like useAiClient() with methods for chat, text, and image generation. Support switching models or providers with a single option. Keep pricing, trials, and usage metering in the app or backend, while the module handles the client, types, and error normalization.
Early feedback helps shape the roadmap. You can wire in a feedback widget via a tiny plugin inside your module. Feedjolt is a good example of the kind of tool you might integrate to capture votes and rank requests while you iterate.
When you buy a Nuxt boilerplate or a Vue Nuxt starter template for a new app, keep your own feature logic inside a module. That lets you swap the base later without a rewrite and keep the same composables and components across products.
Common pitfalls and fixes
- Missing transpile for runtime. If host apps error on syntax or imports, add
nuxt.options.build.transpile.push(resolver.resolve('runtime'))insetup. - Options not available at runtime. Mirror module options into
runtimeConfig.public. Read them withuseRuntimeConfigin your plugin and composables. - Wrong import paths after publish. Publish only built files. Set
files: ["dist"]and add anexportsmap to block deep imports intosrc/. - Plugins run in the wrong environment. Use
plugin.clientorplugin.serverfilenames, or guard logic withprocess.client/process.server. - Playground uses a cached build. Restart the playground or run
pnpm -r devso changes insrc/rebuild the module. - Global types not picked up. Ensure your
dist/types.d.tsis shipped and referenced bytypesinpackage.json. If you generate multiple d.ts files, re-export them from a single entry. - Bundled dependencies break the host app. Keep framework and Nuxt as peer dependencies. Do not bundle Vue or Nuxt types into your dist.
- Server routes collide. Prefix your routes, for example
/awesome/*, to avoid conflicts in host apps.
Key takeaways
- A Nuxt module packages reusable setup for many apps: plugins, composables, components, routes, and build config.
- Scaffold with
nuxi, define options withdefineNuxtModule, and add runtime code underruntime/. - Test in a playground before publishing so you catch path, transpile, and runtime config issues early.
- For SaaS and AI work, keep domain features in a module and let a Nuxt starter kit handle auth, payments, admin, analytics, and SEO so you ship fast.
FAQ
What is the difference between a Nuxt plugin and a Nuxt module?
A plugin runs in one app and sets up runtime behavior. A module runs at build time and can add plugins, composables, components, server routes, and tweak build config across many apps.
How do I test a Nuxt module locally before publishing?
Use the scaffolded playground app, point it at your module’s local entry, and run a workspace dev script so the module rebuilds when you edit code.
Can I make parts of my module client-only or server-only?
Yes. Use separate plugin files like plugin.client.ts or plugin.server.ts, or guard code with process.client and process.server checks.
How should I expose configuration from my module at runtime?
Mirror options into runtimeConfig.public (or private) during setup, then read them from useRuntimeConfig in your plugin and composables.
What Node and Nuxt versions should my module support?
Target the current LTS version of Node and add a Nuxt peerDependency such as ^3.x. Test in a fresh Nuxt app to confirm compatibility.
Ready to ship your SaaS?
Deploy Nuxt to Vercel with Custom Domains, Step by Step
Ship a Nuxt 3 app on Vercel with correct build scripts, env vars, custom domains, previews, caching, redirects, and safe rollbacks. Clear, tested steps.
How to Build a SaaS with AI Coding Agents – Step-by-Step
Learn how to build a SaaS with AI agents in 2026. Discover the exact workflow to use tools like Cursor and ShipAhead to launch your product in days.