[{"data":1,"prerenderedAt":645},["ShallowReactive",2],{"\u002Fblog\u002Fhow-to-build-a-nuxt-module-structure-options-and-publishing-data":3},{"post":4,"surround":634},{"id":5,"title":6,"alternates":7,"authors":8,"badge":14,"body":16,"date":592,"dateModified":593,"description":26,"extension":594,"head":593,"hero_image_url":25,"json_ld":595,"meta":617,"navigation":618,"ogImage":593,"outbound_links":619,"path":620,"primary_keyword":621,"related_articles":622,"robots":593,"schemaOrg":593,"search_intent":623,"seo":624,"sitemap":625,"stem":626,"supporting_keywords":627,"tags":632,"__hash__":633},"blog_en\u002Fblog\u002Fhow-to-build-a-nuxt-module-structure-options-and-publishing.md","How to Build a Nuxt Module: Structure, Options, and Publishing",[],[9],{"name":10,"to":11,"avatar":12},"Tom Han","https:\u002F\u002Fx.com\u002Ftomhan245",{"src":13},"https:\u002F\u002Fcdn.shipahe.ad\u002Ftomhan.webp",{"label":15},"How-to",{"type":17,"value":18,"toc":568},"minimark",[19,28,32,37,40,54,57,61,66,69,76,91,95,110,135,144,148,154,170,175,180,191,195,205,210,213,217,223,238,243,246,261,272,276,279,292,299,304,311,315,318,323,334,338,341,369,372,379,382,385,389,497,501,524],[20,21,22],"figure",{},[23,24],"img",{"src":25,"alt":26,"style":27},"https:\u002F\u002Fshipahe.ad\u002Fimages\u002Fblog\u002Fhow-to-build-a-nuxt-module-structure-options-and-publishing\u002Fpost-680.webp","Learn how to build a Nuxt module from scratch. Scaffold, add options, ship plugins and composables, test in a playground, and publish without headaches.","max-width:100%;border-radius:12px",[29,30,31],"p",{},"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.",[33,34,36],"h2",{"id":35},"what-a-nuxt-module-is-and-when-to-write-one","What a Nuxt module is and when to write one",[29,38,39],{},"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:",[41,42,43,44,43,48,43,51],"ul",{},"\n  ",[45,46,47],"li",{},"Ship a reusable feature like analytics, feature flags, billing UI, or an API client.",[45,49,50],{},"Enforce conventions across a team, for example a UI kit, linting, and default build config.",[45,52,53],{},"Hide integration details behind a single options object so app code stays clean.",[29,55,56],{},"Plugins are for one app. A module is for many apps and should install with zero or minimal manual wiring.",[33,58,60],{"id":59},"build-a-nuxt-module-step-by-step","Build a Nuxt module step by step",[62,63,65],"h3",{"id":64},"_1-scaffold-the-module","1) Scaffold the module",[29,67,68],{},"Use the official starter. It includes TypeScript, unbuild, and a ready playground.",[70,71,72],"pre",{},[73,74,75],"code",{},"npx nuxi init -t module nuxt-awesome\ncd nuxt-awesome\npnpm install   # or npm \u002F yarn",[29,77,78,79,82,83,86,87,90],{},"You get ",[73,80,81],{},"src\u002Fmodule.ts"," for the entry, a ",[73,84,85],{},"runtime\u002F"," folder for code that runs inside the host app, and a ",[73,88,89],{},"playground\u002F"," Nuxt app for local testing.",[62,92,94],{"id":93},"_2-define-metadata-and-options","2) Define metadata and options",[29,96,97,98,101,102,105,106,109],{},"Give the module a name, a config key shown in ",[73,99,100],{},"nuxt.config.ts",", and sensible defaults. Use ",[73,103,104],{},"defineNuxtModule"," from ",[73,107,108],{},"@nuxt\u002Fkit",".",[70,111,112,130],{},[73,113,114,115,118,121],{},"\u002F* src\u002Fmodule.ts *\u002F\nimport {\n  defineNuxtModule,\n  addPlugin,\n  addImportsDir,\n  addComponentsDir,\n  addServerHandler,\n  createResolver\n} from '@nuxt\u002Fkit'\n",[29,116,117],{},"export interface ModuleOptions {\nenabled?: boolean\napiBase?: string\n}",[29,119,120],{},"export default defineNuxtModule\u003CModuleOptions>({\nmeta: {\nname: 'nuxt-awesome',\nconfigKey: 'awesome'\n},\ndefaults: {\nenabled: true,\napiBase: '\u002Fapi'\n},\nsetup (options, nuxt) {\nconst resolver = createResolver(import.meta.url)",[70,122,127],{"className":123,"code":125,"language":126},[124],"language-text","\u002F\u002F Ensure runtime code is transpiled in host apps\nnuxt.options.build.transpile.push(resolver.resolve('runtime'))\n\n\u002F\u002F Pass selected options to runtime config\nnuxt.options.runtimeConfig.public = {\n  ...nuxt.options.runtimeConfig.public,\n  awesome: {\n    apiBase: options.apiBase\n  }\n}\n\n\u002F\u002F Register plugin, composables, and components\naddPlugin(resolver.resolve('runtime\u002Fplugin'))\naddImportsDir(resolver.resolve('runtime\u002Fcomposables'))\naddComponentsDir({\n  path: resolver.resolve('runtime\u002Fcomponents'),\n  pathPrefix: false\n})\n\n\u002F\u002F Optional: add a server handler via Nitro\naddServerHandler({\n  route: '\u002Fawesome\u002Fping',\n  handler: resolver.resolve('runtime\u002Fserver\u002Fhandlers\u002Fping')\n})\n\n\u002F\u002F Example: expose compile-time flags to Vite\nnuxt.hooks.hook('vite:extendConfig', (config) =&gt; {\n  config.define ||= {}\n  \u002F\u002F @ts-ignore\n  config.define.__AWESOME_ENABLED__ = JSON.stringify(!!options.enabled)\n})\n","text",[73,128,125],{"__ignoreMap":129},"",[29,131,132],{},[73,133,134],{},"}\n})",[29,136,137,138,141,142,109],{},"This makes your module configurable via ",[73,139,140],{},"awesome: { ... }"," in a host app’s ",[73,143,100],{},[62,145,147],{"id":146},"_3-add-runtime-code-plugin-composable-and-handler","3) Add runtime code: plugin, composable, and handler",[29,149,150,151,153],{},"Runtime code lives under ",[73,152,85],{},". Keep the public API small and stable.",[70,155,156,165],{},[73,157,158,159,162],{},"\u002F* runtime\u002Fplugin.ts *\u002F\nimport { defineNuxtPlugin, useRuntimeConfig } from '#app'\n",[29,160,161],{},"export default defineNuxtPlugin(() => {\nconst { public: { awesome } } = useRuntimeConfig()",[29,163,164],{},"const client = {\nbase: awesome?.apiBase || '\u002Fapi',\nasync ping () {\nconst res = await fetch(this.base + '\u002Fping')\nreturn res.ok\n}\n}",[29,166,167],{},[73,168,169],{},"return {\nprovide: { awesome: client }\n}\n})",[70,171,172],{},[73,173,174],{},"\u002F* runtime\u002Fcomposables\u002FuseAwesome.ts *\u002F\nimport { useNuxtApp, useRuntimeConfig } from '#app'\n\nexport function useAwesome () {\n  const { $awesome } = useNuxtApp()\n  const { public: { awesome } } = useRuntimeConfig()\n  return { client: $awesome, config: awesome }\n}",[70,176,177],{},[73,178,179],{},"\u002F* runtime\u002Fserver\u002Fhandlers\u002Fping.ts *\u002F\nexport default defineEventHandler(() => ({ ok: true }))",[29,181,182,183,186,187,190],{},"For client-only or server-only behavior, create ",[73,184,185],{},"plugin.client"," or ",[73,188,189],{},"plugin.server"," files and register the right one.",[62,192,194],{"id":193},"_4-expose-components-or-server-routes","4) Expose components or server routes",[29,196,197,198,186,201,204],{},"If your feature ships UI or extra routes, place them in ",[73,199,200],{},"runtime\u002Fcomponents",[73,202,203],{},"runtime\u002Fserver"," and register as needed.",[70,206,207],{},[73,208,209],{},"\u002F* runtime\u002Fcomponents\u002FAwesomeBadge.vue *\u002F\n\u003Ctemplate>\u003Cspan class=\"awesome-badge\">Awesome\u003C\u002Fspan>\u003C\u002Ftemplate>\n\u003Cscript setup lang=\"ts\">\u003C\u002Fscript>\n\u003Cstyle scoped>.awesome-badge{padding:4px 8px;border:1px solid #ddd;border-radius:4px;}\u003C\u002Fstyle>",[29,211,212],{},"Prefer small, focused components and keep their props stable since host apps may rely on them across versions.",[62,214,216],{"id":215},"_5-use-it-in-the-playground","5) Use it in the playground",[29,218,219,220,222],{},"The scaffold includes a ",[73,221,89],{}," app wired to the local module. Run both together.",[70,224,225,228],{},[73,226,227],{},"\u002F* playground\u002Fnuxt.config.ts *\u002F\nimport MyModule from '..\u002Fsrc\u002Fmodule'\n",[29,229,230],{},[73,231,232,233,237],{},"export default defineNuxtConfig({\nmodules: ",[234,235,236],"span",{},"MyModule",",\nawesome: {\napiBase: '\u002Fapi'\n}\n})",[70,239,240],{},[73,241,242],{},"pnpm -r dev    # builds the module and starts the playground",[29,244,245],{},"Create a page and call your composable.",[70,247,248,251],{},[73,249,250],{},"\u002F* playground\u002Fpages\u002Findex.vue *\u002F\n\u003Cscript setup lang=\"ts\">\nconst { client } = useAwesome()\nconst ok = await client.ping().catch(() => false)\n\u003C\u002Fscript>\n",[29,252,253],{},[73,254,255,256,260],{},"\u003Ctemplate>\n\u003Cdiv>Ping: ",[257,258],"binding",{"value":259},"ok ? 'ok' : 'failed'","\u003C\u002Fdiv>\n\u003CAwesomeBadge \u002F>\n\u003C\u002Ftemplate>",[29,262,263,264,267,268,271],{},"If the playground seems stuck on an old build, restart it or run ",[73,265,266],{},"pnpm -r dev"," again so changes in ",[73,269,270],{},"src\u002F"," rebuild.",[62,273,275],{"id":274},"_6-add-types-and-dx-polish","6) Add types and DX polish",[29,277,278],{},"Type your injections so host apps get IntelliSense. Also export your options type.",[70,280,281,287],{},[73,282,283,284],{},"\u002F* src\u002Ftypes.d.ts *\u002F\nimport type { ModuleOptions } from '.\u002Fmodule'\n",[29,285,286],{},"declare module '#app' {\ninterface NuxtApp {\n$awesome: { base: string; ping: () => Promise\u003Cboolean> }\n}\n}",[29,288,289],{},[73,290,291],{},"export type { ModuleOptions as AwesomeModuleOptions }",[29,293,294,295,298],{},"Point ",[73,296,297],{},"types"," to the built declarations and publish only the build output. Consider an exports map so consumers cannot import sources by accident.",[70,300,301],{},[73,302,303],{},"{\n  \"name\": \"nuxt-awesome\",\n  \"version\": \"0.1.0\",\n  \"type\": \"module\",\n  \"main\": \"dist\u002Fmodule.mjs\",\n  \"types\": \"dist\u002Ftypes.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \".\u002Fdist\u002Ftypes.d.ts\",\n      \"import\": \".\u002Fdist\u002Fmodule.mjs\"\n    },\n    \".\u002Fruntime\u002F*\": \".\u002Fdist\u002Fruntime\u002F*\"\n  },\n  \"files\": [\"dist\"],\n  \"sideEffects\": false,\n  \"peerDependencies\": { \"nuxt\": \"^3.0.0\" },\n  \"scripts\": {\n    \"build\": \"unbuild\",\n    \"dev\": \"pnpm -r --parallel dev\",\n    \"prepublishOnly\": \"pnpm build\"\n  }\n}",[29,305,306,307,310],{},"If your runtime ships CSS, either import it from the host app or set a narrow ",[73,308,309],{},"sideEffects"," array so tree-shaking does not drop needed styles.",[62,312,314],{"id":313},"_7-package-and-publish","7) Package and publish",[29,316,317],{},"Build, version, and publish to npm. Write a README with install, options, examples, and a changelog link.",[70,319,320],{},[73,321,322],{},"pnpm build\nnpm pack                   # inspect the tarball contents\nnpm version patch          # or minor \u002F major\nnpm publish --access public",[29,324,325,326,329,330,333],{},"Test installation in a fresh Nuxt app created with ",[73,327,328],{},"npx nuxi init",". Verify that ",[73,331,332],{},"module options → runtimeConfig"," works, server handlers mount, and your types resolve in the editor.",[33,335,337],{"id":336},"where-modules-fit-in-saas-and-ai-projects","Where modules fit in SaaS and AI projects",[29,339,340],{},"For a SaaS, a module is the right place to package shared concerns across properties. Examples:",[41,342,43,343,43,354,43,357,43,360],{},[45,344,345,346,349,350,353],{},"Billing and entitlements: wrap your payment provider client, expose helpers like ",[73,347,348],{},"usePlan()"," and ",[73,351,352],{},"hasFeature('x')",", and register a secure webhook route.",[45,355,356],{},"Auth glue: auto-register a plugin that syncs session state to a composable, and add route rules for protected pages.",[45,358,359],{},"i18n defaults: ship a locale detector, default messages, and a directive for number and date formatting.",[45,361,362,363,349,366,109],{},"Marketing baseline: add analytics, SEO helpers, and consistent components like ",[73,364,365],{},"\u003CPricingTable \u002F>",[73,367,368],{},"\u003CCookieBanner \u002F>",[29,370,371],{},"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.",[29,373,374,375,378],{},"Building an AI tool for the web follows the same pattern. Wrap your model provider and configuration in a module. Expose a composable like ",[73,376,377],{},"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.",[29,380,381],{},"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.",[29,383,384],{},"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.",[33,386,388],{"id":387},"common-pitfalls-and-fixes","Common pitfalls and fixes",[41,390,43,391,43,405,43,419,43,435,43,453,43,465,43,481,43,487],{},[45,392,393,397,398,401,402,109],{},[394,395,396],"strong",{},"Missing transpile for runtime."," If host apps error on syntax or imports, add ",[73,399,400],{},"nuxt.options.build.transpile.push(resolver.resolve('runtime'))"," in ",[73,403,404],{},"setup",[45,406,407,410,411,414,415,418],{},[394,408,409],{},"Options not available at runtime."," Mirror module options into ",[73,412,413],{},"runtimeConfig.public",". Read them with ",[73,416,417],{},"useRuntimeConfig"," in your plugin and composables.",[45,420,421,424,425,428,429,432,433,109],{},[394,422,423],{},"Wrong import paths after publish."," Publish only built files. Set ",[73,426,427],{},"files: [\"dist\"]"," and add an ",[73,430,431],{},"exports"," map to block deep imports into ",[73,434,270],{},[45,436,437,440,441,186,443,445,446,449,450,109],{},[394,438,439],{},"Plugins run in the wrong environment."," Use ",[73,442,185],{},[73,444,189],{}," filenames, or guard logic with ",[73,447,448],{},"process.client","\u002F",[73,451,452],{},"process.server",[45,454,455,458,459,461,462,464],{},[394,456,457],{},"Playground uses a cached build."," Restart the playground or run ",[73,460,266],{}," so changes in ",[73,463,270],{}," rebuild the module.",[45,466,467,470,471,474,475,401,477,480],{},[394,468,469],{},"Global types not picked up."," Ensure your ",[73,472,473],{},"dist\u002Ftypes.d.ts"," is shipped and referenced by ",[73,476,297],{},[73,478,479],{},"package.json",". If you generate multiple d.ts files, re-export them from a single entry.",[45,482,483,486],{},[394,484,485],{},"Bundled dependencies break the host app."," Keep framework and Nuxt as peer dependencies. Do not bundle Vue or Nuxt types into your dist.",[45,488,489,492,493,496],{},[394,490,491],{},"Server routes collide."," Prefix your routes, for example ",[73,494,495],{},"\u002Fawesome\u002F*",", to avoid conflicts in host apps.",[33,498,500],{"id":499},"key-takeaways","Key takeaways",[41,502,43,503,43,506,43,518,43,521],{},[45,504,505],{},"A Nuxt module packages reusable setup for many apps: plugins, composables, components, routes, and build config.",[45,507,508,509,512,513,515,516,109],{},"Scaffold with ",[73,510,511],{},"nuxi",", define options with ",[73,514,104],{},", and add runtime code under ",[73,517,85],{},[45,519,520],{},"Test in a playground before publishing so you catch path, transpile, and runtime config issues early.",[45,522,523],{},"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.",[525,526,529,533,537,540,544,547,551,554,558,561,565],"section",{"className":527},[528],"post-faq",[33,530,532],{"id":531},"faq","FAQ",[62,534,536],{"id":535},"what-is-the-difference-between-a-nuxt-plugin-and-a-nuxt-module","What is the difference between a Nuxt plugin and a Nuxt module?",[29,538,539],{},"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.",[62,541,543],{"id":542},"how-do-i-test-a-nuxt-module-locally-before-publishing","How do I test a Nuxt module locally before publishing?",[29,545,546],{},"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.",[62,548,550],{"id":549},"can-i-make-parts-of-my-module-client-only-or-server-only","Can I make parts of my module client-only or server-only?",[29,552,553],{},"Yes. Use separate plugin files like plugin.client.ts or plugin.server.ts, or guard code with process.client and process.server checks.",[62,555,557],{"id":556},"how-should-i-expose-configuration-from-my-module-at-runtime","How should I expose configuration from my module at runtime?",[29,559,560],{},"Mirror options into runtimeConfig.public (or private) during setup, then read them from useRuntimeConfig in your plugin and composables.",[62,562,564],{"id":563},"what-node-and-nuxt-versions-should-my-module-support","What Node and Nuxt versions should my module support?",[29,566,567],{},"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.",{"title":129,"searchDepth":569,"depth":569,"links":570},2,[571,572,582,583,584,585],{"id":35,"depth":569,"text":36},{"id":59,"depth":569,"text":60,"children":573},[574,576,577,578,579,580,581],{"id":64,"depth":575,"text":65},3,{"id":93,"depth":575,"text":94},{"id":146,"depth":575,"text":147},{"id":193,"depth":575,"text":194},{"id":215,"depth":575,"text":216},{"id":274,"depth":575,"text":275},{"id":313,"depth":575,"text":314},{"id":336,"depth":569,"text":337},{"id":387,"depth":569,"text":388},{"id":499,"depth":569,"text":500},{"id":531,"depth":569,"text":532,"children":586},[587,588,589,590,591],{"id":535,"depth":575,"text":536},{"id":542,"depth":575,"text":543},{"id":549,"depth":575,"text":550},{"id":556,"depth":575,"text":557},{"id":563,"depth":575,"text":564},"2026-09-03",null,"md",{"@context":596,"@graph":597},"https:\u002F\u002Fschema.org",[598,602],{"@type":599,"headline":6,"description":26,"image":25,"inLanguage":600,"datePublished":601},"BlogPosting","en","2026-09-03 03:22:49",{"@type":603,"mainEntity":604},"FAQPage",[605,609,611,613,615],{"@type":606,"name":536,"acceptedAnswer":607},"Question",{"@type":608,"text":539},"Answer",{"@type":606,"name":543,"acceptedAnswer":610},{"@type":608,"text":546},{"@type":606,"name":550,"acceptedAnswer":612},{"@type":608,"text":553},{"@type":606,"name":557,"acceptedAnswer":614},{"@type":608,"text":560},{"@type":606,"name":564,"acceptedAnswer":616},{"@type":608,"text":567},{},true,[],"\u002Fblog\u002Fhow-to-build-a-nuxt-module-structure-options-and-publishing","nuxt",[],"Informational",{"title":6,"description":26},{"loc":620},"blog\u002Fhow-to-build-a-nuxt-module-structure-options-and-publishing",[621,628,629,630,631],"vue","nuxt module","saas","starter kit",[621,628,629,630,631],"g5FiZTkXjgkci8WEWE8Gqg7ZrWsQ37xPC0AG41_TBc8",[635,640],{"title":636,"path":637,"stem":638,"description":639,"children":-1},"Deploy Nuxt to Vercel with Custom Domains, Step by Step","\u002Fblog\u002Fdeploy-nuxt-to-vercel-with-custom-domains-step-by-step","blog\u002Fdeploy-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.",{"title":641,"path":642,"stem":643,"description":644,"children":-1},"How to Build a SaaS with AI Coding Agents – Step-by-Step","\u002Fblog\u002Fhow-to-build-a-saas-with-ai","blog\u002Fhow-to-build-a-saas-with-ai","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.",1788422508246]