Module Development
Charlie discovered that MegaShop's tracking logic, error reporting, and performance monitoring were scattered across various locations. Alice and Bob's other projects also needed the same capabilities. By encapsulating these common features into Nuxt modules, they could develop them once and reuse them everywhere, and even publish them to npm for the community to use.
1. What You'll Learn
- Module Architecture: defineNuxtModule() + installModule() + Lifecycle Hooks
- Module Capabilities: Component Injection / Composables / Plugins / Middleware / Server Routing / Configuration
- Module Release: npm Package Bundling + TypeScript Types
- Module testing: @nuxt/test-utils + fixtures
- Hands-On Guide to the MegaShop @megashop/analytics Module
2. A True Story of an Architect
(1) Pain Point: Duplicate Development of Generic Features
Charlie's MegaShop needs tracking—every page view, every item added to the cart, and every click must be logged. He manually calls the API in five components. Alice's other project also needs tracking, so Bob rewrote the code from scratch. The code is repetitive and inconsistent.
(2) Solution for Nuxt Modules
Once packaged as a module, it's ready to use right out of the box—with automatic injection of Composable and the server API:
// nuxt.config.ts
modules: ['@megashop/analytics']
(3) Benefits: Develop Once, Reuse Everywhere
All projects gain tracking capabilities simply by installing the module. With Alice, you can enable tracking in any project by calling useAnalytics()—no configuration required.
3. Module Architecture
(1) Nuxt Module Lifecycle
graph TB
A[defineNuxtModule] --> B[Setup Function]
B --> C[installModule - Dependencies]
B --> D[addPlugin - Register Plugins]
B --> E[addComposable - Inject Composables]
B --> F[addServerHandler - API Routes]
B --> G[addLayout - Custom Layouts]
B --> H[addComponent - Auto Components]
B --> I[extendConfig - Modify Config]
J[Nuxt Hooks] --> K[modules:before]
J --> L[modules:done]
J --> M[build:before]
J --> N[build:done]
(1) ▶ Example: Minimal Module Skeleton
// src/module.ts
import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
meta: {
name: '@megashop/analytics',
configKey: 'analytics',
compatibility: {
nuxt: '^3.0.0'
}
},
defaults: {
enabled: true,
endpoint: '/api/analytics',
debug: false
},
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
// Register plugin
addPlugin(resolve('./runtime/plugin'))
// Expose options to runtime
nuxt.options.runtimeConfig.public.analytics = {
enabled: options.enabled,
endpoint: options.endpoint,
debug: options.debug
}
}
})
Output:
// Execution Successful
(2) Quick Reference for Module Capabilities
| Capability | API | Description |
|---|---|---|
| Register Plugin | addPlugin() | Automatically execute initialization logic |
| Inject a component | addComponent() | Automatically import Vue components |
| Inject into Composable | addImports() | Automatically import functions |
| Add API Route | addServerHandler() | Automatically Register Server Routes |
| Add Layout | addLayout() | Register Custom Layout |
| Add middleware | addRouteMiddleware() | Register route middleware |
| Modify Configuration | extendConfig() | Modify Nuxt Configuration |
| Install a dependency module | installModule() | Install other modules |
4. Hands-On Module: @megashop/analytics
(1) ▶ Example: Module Entry Point
// src/module.ts
import { defineNuxtModule, addPlugin, addImports, addServerHandler, createResolver } from '@nuxt/kit'
export interface ModuleOptions {
enabled: boolean
endpoint: string
debug: boolean
trackPageViews: boolean
trackClicks: boolean
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name: '@megashop/analytics',
configKey: 'analytics',
compatibility: { nuxt: '^3.0.0' }
},
defaults: {
enabled: true,
endpoint: '/api/analytics',
debug: false,
trackPageViews: true,
trackClicks: true
},
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
// 1. Register client plugin for auto-tracking
if (options.trackPageViews || options.trackClicks) {
addPlugin(resolve('./runtime/plugin.client'))
}
// 2. Auto-import useAnalytics composable
addImports({
name: 'useAnalytics',
from: resolve('./runtime/composables/useAnalytics')
})
// 3. Add server API for receiving events
addServerHandler({
method: 'post',
route: options.endpoint,
handler: resolve('./runtime/server/api/analytics')
})
// 4. Expose config to runtime
nuxt.options.runtimeConfig.public.analytics = {
enabled: options.enabled,
endpoint: options.endpoint,
debug: options.debug
}
}
})
Output:
// Execution Successful
(2) ▶ Example: Runtime Plugin
// src/runtime/plugin.client.ts
import { defineNuxtPlugin } from '#app'
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig().public.analytics
if (!config.enabled) return
// Auto-track page views
if (config.trackPageViews) {
nuxtApp.hook('page:finish', () => {
useAnalytics().trackPageView(window.location.pathname)
})
}
// Auto-track clicks on data-track elements
if (config.trackClicks) {
document.addEventListener('click', (e) => {
const target = (e.target as HTMLElement).closest('[data-track]')
if (target) {
const event = target.getAttribute('data-track') || 'click'
useAnalytics().track(event, { element: target.tagName })
}
})
}
})
Output:
// Execution Successful
(3) ▶ Example: Runtime Composable
// src/runtime/composables/useAnalytics.ts
export function useAnalytics() {
const config = useRuntimeConfig().public.analytics
async function track(event: string, data?: Record<string, any>) {
if (!config.enabled) return
if (config.debug) console.log('[Analytics]', event, data)
await $fetch(config.endpoint, {
method: 'POST',
body: { event, data, timestamp: Date.now(), url: import.meta.client ? window.location.href : '' }
})
}
function trackPageView(path: string) {
track('page_view', { path })
}
function trackAddToCart(productId: number, productName: string, price: number) {
track('add_to_cart', { productId, productName, price, currency: 'USD' })
}
function trackPurchase(orderId: string, total: number) {
track('purchase', { orderId, total, currency: 'USD' })
}
return { track, trackPageView, trackAddToCart, trackPurchase }
}
Output:
// Execution Successful
(4) ▶ Example: Runtime Server API
// src/runtime/server/api/analytics.ts
import { defineEventHandler, readBody, setHeader } from 'h3'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Validate required fields
if (!body.event) {
throw createError({ statusCode: 400, message: 'Event name required' })
}
// Store event (in production: send to analytics service)
const storage = useStorage('analytics')
const key = `event:${Date.now()}:${Math.random().toString(36).slice(2)}`
await storage.setItem(key, {
event: body.event,
data: body.data || {},
timestamp: body.timestamp || Date.now(),
url: body.url,
userAgent: getHeader(event, 'user-agent')
})
setHeader(event, 'cache-control', 'no-store')
return { success: true }
})
Output:
// Execution Successful
5. Module Release
(1) ▶ Example: package.json configuration
{
"name": "@megashop/analytics",
"version": "1.0.0",
"type": "module",
"main": "./dist/module.mjs",
"types": "./dist/types.d.ts",
"exports": {
".": {
"import": "./dist/module.mjs",
"require": "./dist/module.cjs",
"types": "./dist/types.d.ts"
},
"./runtime/*": "./dist/runtime/*"
},
"files": ["dist"],
"scripts": {
"build": "nuxt-module-build",
"dev": "nuxt-module-build --stub",
"test": "vitest run",
"prepublishOnly": "npm run build"
},
"peerDependencies": {
"nuxt": "^3.0.0"
},
"devDependencies": {
"@nuxt/module-builder": "^0.6.0",
"@nuxt/test-utils": "^3.0.0",
"nuxt": "^3.12.0"
}
}
Output:
{
"name": "@megashop/analytics",
"version": "1.0.0",
"type": "module",
"main": "./dist/module.mjs",
"types": "./dist/types.d.ts",
"exports": {
".": {
"import": "./dist/module.mjs",
"require": "./dist/module.cjs",
"types": "./dist/types.d.ts"
},
"./runtime/*": "./dist/runtime/*"
},
"files": [
"dist"
],
"scripts": {
"build": "nuxt-module-build",
"dev": "nuxt-module-build --stub",
"test": "vitest run",
"prepublishOnly": "npm run
(2) ▶ Example: Using it in a project
// nuxt.config.ts of MegaShop
export default defineNuxtConfig({
modules: [
// Local module during development
'~/modules/analytics',
// Published module in production
// '@megashop/analytics'
],
analytics: {
enabled: true,
endpoint: '/api/analytics',
debug: process.env.NODE_ENV === 'development',
trackPageViews: true,
trackClicks: true
}
})
Output:
// Execution Successful
6. Module Testing
(1) ▶ Example: Module Test Fixture
// test/module.test.ts
import { setupTest } from '@nuxt/test-utils'
describe('@megashop/analytics module', () => {
setupTest({
fixture: './test/fixtures/basic',
build: true
})
test('registers analytics plugin', () => {
// Plugin auto-registers, check Nuxt plugins
const nuxt = useNuxt()
const hasPlugin = nuxt.options.plugins.some(p => p.src?.includes('analytics'))
expect(hasPlugin).toBe(true)
})
test('exposes useAnalytics composable', async () => {
// Auto-import available
const { data } = await useFetch('/api/analytics', {
method: 'POST',
body: { event: 'test', data: {} }
})
expect(data.value).toBeDefined()
})
test('analytics endpoint accepts events', async () => {
const response = await $fetch('/api/analytics', {
method: 'POST',
body: { event: 'page_view', data: { path: '/' } }
})
expect(response.success).toBe(true)
})
})
Output:
// Execution Successful
7. Comprehensive Example: Using @megashop/analytics
<!-- pages/products/[id].vue - Using analytics module -->
<template>
<div v-if="product">
<h1>{{ product.name }}</h1>
<button
@click="handleAddToCart"
data-track="add_to_cart"
>
Add to Cart
</button>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const { data: product } = await useFetch(`/api/products/${route.params.id}`)
// useAnalytics auto-imported by module
const { trackAddToCart, trackPageView } = useAnalytics()
// Manual tracking
onMounted(() => {
trackPageView(`/products/${route.params.id}`)
})
async function handleAddToCart() {
if (!product.value) return
trackAddToCart(product.value.id, product.value.name, product.value.price)
// ... add to cart logic
}
</script>
❓ FAQ
runtime directory in the module?runtime contains runtime code—code that is executed only when the application is running and is not included in the build configuration. Plugins, Composables, and server handlers are all placed under runtime/ and are not processed by Nuxt Kit.modules/ directory and reference it in nuxt.config.ts ~/modules/xxx. During development, use nuxt-module-build --stub to generate symbolic links; code changes will take effect in real time.installModule('@pinia/nuxt') in the setup to install dependent modules. Nuxt will automatically remove duplicates.exports and types in package.json, build using @nuxt/module-builder, and run npm publish. We recommend using GitHub Actions for automated publishing.exports.types in package.json to point to dist/types.d.ts, and users will automatically receive type hints after installation.📖 Summary
- defineNuxtModule: Define a module using
meta,defaults, and thesetupfunction - Module capabilities: inject everything using addPlugin, addImports, addServerHandler, addComponent, etc.
- Runtime code is located in the
runtime/directory and is not included in the build configuration phase. - @megashop/analytics module: Automatic tracking + useAnalytics Composable + server API
- Modules are built using @nuxt/module-builder and published with
npm publish
📝 Exercises
- Basic Exercise (Difficulty: ⭐): Create a minimal module skeleton, register a plugin, and output "Module loaded"
- Advanced Exercise (Difficulty: ⭐⭐): Develop the @megashop/analytics module to implement the useAnalytics Composable and server API
- Challenge (Difficulty: ⭐⭐⭐): Add automatic page view tracking and
data-trackclick tracking, then write unit tests to verify the functionality
---|



