Creating a Nuxt module

Jamie Curnow
Jul 19 · 17 min read
Photo by Daniel Chen on Unsplash

For our most recent project we are using a mono-repo setup with Lerna and Yarn workspaces which gives us an excellent opportunity to create our own local Node packages, writing more maintainable and useable code. All of our front end apps run on Nuxt.js and I quite quickly found that I needed to install Vue plugins, and add to the Vuex store from within a module, while still keeping the context of the Nuxt app.

In this post, I will explain with code examples how to create a Nuxt module as a local Node package. This could be easily extended to publish the package on NPM if desired. I’ll cover the basic setup of a Nuxt module that can be easily extended, including features such as passing options to the module, creating a new Vuex store module, creating middleware, and registering components.

TLDR;
Code repo here:
https://github.com/carepenny/nuxt-module

Let’s get started…

For the purposes of this tutorial, we’ll call our Nuxt module `customCounter`. You can call it whatever you like.

The first thing to do is scaffold out your new package. In a new directory, run

yarn init

In our case, we’re using Yarn workspaces and Lerna to manage local packages, so for the name of the package, I’ll use `@carepenny/custom-counter` you can use any name you want, following your repo standards or NPM publishing standards.

The version and description are up to you.

Since we’re going to roughly follow Nuxt’s module naming standards, we’ll use `module.js` as our entry point.

Repo URL, author, license and private options are up to you.

You should end up with a lonely `package.json` in your directory, which is where you would add all of your dependencies for the module. In this case, to keep things simple, we won’t need any other external packages for our module to work, but you could `yarn add` any external or internal package in your module.

In the Nuxt examples for modules, they recommend having `lib` folder in your module directory including a `module.js` and `plugin.js file`. We’re going to take it a step further and create the following directories and files:

  • ./module.js (our entry point)
  • ./debug.js (used for debugging)
  • ./components/index.js (component registerring plugin)
  • ./components/lib/index.js (exposing all components in a single place)
  • ./components/lib/counterAdjuster.vue (a custom component)
  • ./middleware/index.js (some custom middleware)
  • ./plugins/index.js (injecting a custom function object into Nuxt)
  • ./plugins/helpers/index.js (exposing all plugin functions)
  • ./plugins/helpers/message.js (a function to console log a message)
  • ./plugins/helpers/counter.js (a function to adjust the counter in the store)
  • ./store/index.js (store registering plugin)
  • ./store/modules/counter.js (the vuex store holding the counter data, mutations and getters)

Create all of those files in your package:

mkdir -p {components/lib,middleware,plugins/helpers,store/modules} \
&& touch module.js debug.js \
{components,components/lib,middleware,plugins,plugins/helpers,store}/index.js \
components/lib/counterAdjuster.vue \
plugins/helpers/{message,counter}.js \
store/modules/counter.js

This file structure is a little over-the-top for a simple module such as this, but we’ll use it to demonstrate how a Nuxt module could be extensive and still easily managed.

Setting up the entry point

The entry point of the module `module.js` is where we’ll scaffold out the module, tell Nuxt what files to create and where to create them, manage module options and set off all of the plugins for the module.

There are two things a Nuxt module must do. The first is to export a default function where all of our code will be held. This function has access to a `moduleOptions` object that contains the options passed through to the module directly when declaring it in an array format in your `nuxt.config.js` (more on that in a minute).

// module.js
export default function(moduleOptions) {}

The second thing we must do is add our modules `package.json` to a meta key on the module export… Sounds confusing but it’s a one-liner-and-forget that Nuxt uses under the hood to avoid registering the same module twice.

// module.js
module.exports.meta = require('./package.json')

Our `module.js` should currently look like this:

// module.js
export default function(moduleOptions) {}
module.exports.meta = require('./package.json')

Let’s get the options for the module and spread them into a single options object. The options for the module can come from two places depending on how you define the module in your `nuxt.config.js`. If you define the module options like this:

// nuxt.config.js
export default {
// ...
// register modules:
modules: [
// ...
[@carepenny/custom-counter, { option1: 'something' }]
]
}

Then the `moduleOptions` variable in `module.js` will be:

{ option1: 'something' }

However, you can also define the module options independently in your Nuxt config like this:

// nuxt.config.js
export default {
// ...
// register modules:
modules: [
// ...
'@carepenny/custom-counter'
],
// custom-counter options:
customCounter: { option1: 'something' }
}

In this case, the `moduleOptions` variable in `module.js` will be empty, however, we can access any of the `nuxt.config.js` through `this.options` (which is a little confusing). eg:

// module.js
export default function(moduleOptions) {
console.log(this.options.customCounter) // { option1: 'something' }
}
module.exports.meta = require('./package.json')

It’s obviously important here to document what the object in the nuxt config should be called and to make it unique enough so that it doesn’t clash with other modules or Nuxt options.

You can combine these methods of passing options and of course if this is a public package, the end users have the options to use either of these methods to pass options to your module. So we need to handle both of these scenarios and for our sanity, we’ll create a new `options` object that we will use everywhere else in the module side of our code.

// module.js
export default function(moduleOptions) {
// get all options for the module
const options = {
...moduleOptions,
...this.options.customCounter
}
}
module.exports.meta = require('./package.json')

The options that we’re going to pass through for our `customCounter` example module are:

  • namespace
    Providing an example of customising the naming of the store/plugin etc and allowing the user to avoid conflicts with their own code.
  • initialValue
    Providing an example of setting up state for our store module and allowing the user to set an initial value for the counter.
  • debug
    Provides an easy way to debug the module and see what’s going on. For now, this will be simply console logging the options that we pass through to the module.

Let’s update the nuxt config to pass through these options using both methods:

// nuxt.config.js
export default {
// ...
// register modules:
modules: [
// ...
['@carepenny/custom-counter', { namespace: 'counter' }]
],
// custom-counter options:
customCounter: { initialValue: 6, debug: true }
}

That’s all we need to do in the nuxt config. From here on, we’ll be building out the module.

Building out the module

In Nuxt modules, there are a few methods available on the `this` object. One of those is the `addPlugin()` method which takes an object for its params. This method allows the module to add new plugins to Nuxt.

(Docs: https://nuxtjs.org/api/internals-module-container/#addplugin-template-)

The `addPlugin` method is used like this:

// module.js
const { resolve, join } = require('path')
export default function(moduleOptions) {
// get all options for the module
const options = {
...moduleOptions,
...this.options.customCounter
}

// add the debug plugin
this.addPlugin({
src: resolve(__dirname, 'debug.js'),
fileName: join('customCounter', 'debug.js'),
options: {
...options,
somethingElse: false
}
})
}
module.exports.meta = require('./package.json')

The params for the `addplugin` method are:

  • src
    The path to the file containing plugin logic. We use the path module here and its resolve function to resolve the path when the module is being built by Nuxt.
  • filename
    This string is the name of the file that Nuxt will end up building. It can, however, include a path and Nuxt will create the directories and file at build time. We use the path module’s join function to create the path string.
  • options
    This object is used to pass variables through to the plugin file. Under the hood, Nuxt uses Lodash templates to pass these through. We use a little snippet to just get the options back out into a javascript object in the plugin file. More on that in a minute…

One other thing we need to do right from the start is to handle the `namespace` variable. We’re going to use this to:

  • name the global nuxt methods that we will inject through a plugin,
  • name the directory that nuxt builds our plugin in,
  • add a prefix to our store module,
  • add a prefix to the custom middleware.

Doing this will negate the risk of naming conflicts in our Nuxt app and it’s a great example of how powerful nuxt modules are. Remember that we set the `namespace` variable for the module in our `nuxt.config.js` to `counter`. We’ll expose the namespace variable right after we create the options object in our module and set a default for it in case it is undefined.

// module.js
const { resolve, join } = require('path')
export default function(moduleOptions) {
// get all options for the module
const options = {
...moduleOptions,
...this.options.customCounter
}

// expose the namespace / set a default
if (!options.namespace) options.namespace = 'customCounter'
const { namespace } = options

// add the debug plugin
this.addPlugin({
src: resolve(__dirname, 'debug.js'),
// ** NOTE: we now use namespace here: **
fileName: join(namespace, 'debug.js'),
options // just pass through the options object
})
}
module.exports.meta = require('./package.json')

Creating the plugin files

Let’s open up the `debug.js` plugin file and demonstrate how to retrieve the options passed through from the `addPlugin` method in the module file.

// debug.js
const options = JSON.parse(`<%= JSON.stringify(options) %>`)
const { debug, namespace } = options
if (debug) {
console.log(`${namespace} options: `, options)
}

Here we pluck out all the options using Lodash templates and JSON.stringify().

If you run `nuxt dev` or `yarn run dev` back in your Nuxt project, you should see the options passed all the way through from `nuxt.config.js` to the `debug.js` file.

{
namespace: 'counter',
initialValue: 6,
debug: true
}

We can use this pattern for all plugins that we need to register through our module.

Other than the Lodash templates, the plugin files in the module is a normal Nuxt plugin file and you can write it exactly the same as a normal nuxt plugin. It has access to Vue for registering components etc. and if you export a default function from the plugin file, you have access to the nuxt context and the `inject` method.

The rest of our plugins will live under the `plugins/helpers` directory that we created. The next thing that we’ll do is write the functions for `plugins/helpers/message.js` and `plugins/helpers/counter.js`. These functions are pretty self-explanatory and the comments on them should give you enough info to see what they are doing.

// plugins/helpers/message.js
// funtion to allow a user to console.log() a value,
// prefixing that value with the namespace option of our module.
export const message = ({ namespace, string }) => {
return console.log(namespace, string)
}
// plugins/helpers/counter.js
// functions to get, adjust and log the counter in the store
// the store module in question will be created with the namespace
// module option as it's name
// mini function to handle if no store, or no store module
// with our namespace exists
const storeModuleExists = ({ state, namespace }) => {
if (!state || !state[namespace]) {
console.error(`${namespace} nuxt module error: store not initialized`)
return false
} else {
return true
}
}
// function to return the current value of the count
export const value = ({ state, namespace }) => {
// handle no store:
if (!storeModuleExists({ state, namespace })) return undefined
// return the counter vale from the store
return state[namespace].count
}
// functions to adjust the counter
export const adjust = ({ state, store, namespace, adjustment }) => {
// handle no store:
if (!storeModuleExists({ state, namespace })) return undefined
// the adjustment shoud be of type number, err if not
if (typeof adjustment !== 'number') {
return console.error(`${namespace} nuxt module error: adjustment should be of type 'number'.`)
}
// adjust the value of the counter using a store mutation
return store.commit(`${namespace}/adjust`, adjustment)
}
// function to console log the current value of the count
export const log = ({ state, namespace }) => {
// handle no store:
if (!storeModuleExists({ state, namespace })) return undefined
// get the current count from the store
const { count } = state[namespace]
// console log it
return console.log(count)
}

We will then export these methods from the `plugins/helpers/index.js` file, which is not really necessary with such a small module, but will set us up for easily extending in the future.

// plugins/helpers/index.js
export * from './counter'
export * from './message'

This simply allows us to import all the helpers at once in the `plugins/index.js` file which we will now build out…

The `plugins/index.js` file will apply a new global set of methods to the Vue instance of our Nuxt app using Nuxt’s `inject()` function which is available along with the context of the app in Nuxt plugin files when you export a default function. (Nuxt inject docs). Inject is used like this:

// some/nuxt/plugin/file.js
export default (context, inject) => {
inject('myFunction', (string) => console.log('Hello', string))
}

Which adds `$myFunction` to the Vue prototype so you can use it in any Vue instance like this:

// some/vue/file.vue
export default {
// ...
mounted () {
this.$myFunction('World')
}
}

You also have access to it in the Vuex store modules:

// some/vuex/store/module.js
// ...
export const actions = {
someAction () {
this.$myFunction('Store')
}
}

And it is also attached to the Nuxt context on the `app` object so you can use it in other plugin files:

// some/other/nuxt/plugin/file.js
export default ({ app }) => {
app.$myFunction('This is awesome')
}

As you can see, Nuxt’s inject function is very powerful and easily makes code reusable throughout the app.

Our plugin will be using the functions that we exported in `plugins/helpers/index.js` and injecting them into the app under an object with the name that was provided as the `namespace` variable. It will look like this:

// plugins/index.js
import * as helpers from './helpers/index.js'
// get the options out using lodash templates
const options = JSON.parse(`<%= JSON.stringify(options) %>`)
// extract the namespace from the options
const { namespace } = options
// create the plugin
export default ({ store }, inject) => {
// get a reference to the vuex store's state
const { state } = store
// inject an object of functions into the app
inject(namespace, {
value() {
return helpers.value({ state, namespace })
},
adjust(adjustment) {
return helpers.adjust({ state, store, namespace, adjustment })
},
log() {
return helpers.log({ state, namespace })
},
message(string) {
return helpers.message({ namespace, string })
}
})
}

Now we need to create the Vuex store module through our plugin so that the functions inside `plugins/helpers` can do their thing.

Creating Vuex store modules

We will structure our code for registering the Vuex store module by putting all of the module logic in the `store/modules` directory and we’ll register the store modules in `store/index.js`. This may be a little over-the-top for this instance, but it provides a good example of easily extendable code. We’ll start with `store/modules/counter.js` which will hold our state/mutation/getters for the counter. We will pass through the plugin options when calling this function so that we can set an initial value and expose the options in the Vuex state for use in components etc. If you’re familiar with Vuex, this should be pretty straight forward (If not, you have some further reading to do):

// store/modules/counter.js
export default options => ({
namespaced: true,
state: () => ({
options,
count: options.initialValue
}),
mutations: {
adjust(state, data) {
state.count += data
}
},
getters: {
count: state => state.count
}
})

Then we’ll add that module to Vuex in `store/index.js`. This is done using the Vuex store’s `registerModule()` function (Vuex Docs). The important thing to note here is the use of the `preserveState` option which will preserve the previous state when registering a new module. We set this to `true` if a store module with the same name already exists. This saves conflicts with the apps store modules and doesn’t override the state if it’s mutated server-side.

// store/index.js
import counterModule from './modules/counter'
// get the options out using lodash templates
const options = JSON.parse(`<%= JSON.stringify(options) %>`)
// extract the namespace var
const { namespace } = options
// create the plugin
export default ({ store }, inject) => {
// register the module using namespace as the name.
// counter module takes the options object and returns an object that is a
// vuex store defenition
store.registerModule(namespace, counterModule(options), {
preserveState: Boolean(store.state[namespace]) // if the store module already exists, preserve it
})
}

That’s all we need to do to register store modules!

Defining and registering components

We can define components in our module that will be registered globally and available for use throughout the app. The component templates will be defined in `components/lib/*` and can be any flavour of component that you like (SFC/Render function) as Nuxt will compile and build them like any other component. We will expose all of our component files in `components/lib/index.js` and then import and register them all in `components/index.js`.

Lets start with a really simple component in `components/lib/counterAdjuster.vue`:

<template lang="html">
<div class="custom-counter">
<!-- Basic html to render the current count and provide adjustment buttons -->
<h1>Count is: {{ count }}</h1>
<button class="custom-counter--button" @click="adjust(-1)">Minus</button>
<button class="custom-counter--button" @click="adjust(+1)">Add</button>
</div>
</template>
<script>
// components/lib/counterAdjuster.vue
export default {
computed: {
pluginOptions() {
// _customCounterOptions will be added as a prop on component registration.
// it will be the plugin's options object
return this._customCounterOptions || {}
},
// helper to get the name of our injected plugin using the namespace option
injectedPluginName() {
const { pluginOptions } = this
return pluginOptions.namespace ? '$' + pluginOptions.namespace : undefined
},
// helper to return the current value of the counter using our injected plugin function
count() {
const { injectedPluginName } = this
return injectedPluginName
? this[injectedPluginName].value() // same as this.$count.value()
: undefined
}
},
methods: {
// method to adjust the counter using our injected plugin function
adjust(adjustment) {
const { injectedPluginName } = this
this[injectedPluginName].adjust(adjustment)
}
}
}
</script>
<style lang="scss" scoped></style>

This component is very straight forward, but one thing to note is the `_customCounterOptions` variable in the `pluginOptions()` computed prop. This var is actually a prop passed into the component on registration which allows us to access the options of the module from within the component — importantly the `namespace` option. We’ll set this up in a minute, but next, we’ll quickly create the `components/lib/index.js` which simply exposes any components from the `components/lib` folder for easy registration:

// components/lib/index.js
import counterAdjuster from './counterAdjuster.vue'
export default {
counterAdjuster
}

Now let’s register our component globally using `Vue.component()` (Vue docs). We will use the `extends` option to extend our imported components, adding the `_customCounterOptions` prop that is needed to access our module options from within the component files.

// components/index.js
import Vue from 'vue'
import components from './lib'
// get options passed from module.js
const options = JSON.parse(`<%= JSON.stringify(options) %>`)
// loop through components and register them
for (const name in components) {
Vue.component(name, {
// extend the original component
extends: components[name],
// add a _customCounterOptions prop to gain access to the options in the component
props: {
_customCounterOptions: {
type: Object,
default: () => ({ ...options })
}
}
})
}

The for loop there simply loops over every component exported from `components/lib/index.js` and registers them using their export name as the component name. We can then use the component in any layout/page/component by it’s name eg.:

// some/random/page.vue
<template lang="html">
<counterAdjuster></counterAdjuster>
</template>
<script>
export default {}
</script>

That’s it for components, now onto the final type of registration — middleware.

Adding Nuxt Middleware

Adding middleware to Nuxt through a module is not very well documented (as far as I could find), but it is very simple indeed. All you need to do is import Nuxt’s middleware object from the nuxt build folder. Remember that all of our files will be put into the nuxt build folder at build time, so we should import the middleware object as if we were in our build destination. For example, the nuxt middleware can be imported from `.nuxt/middleware.js` (assuming your build folder is `.nuxt`). We will be replicating our module directory structure in the build folder under a directory with the same name as the `namespace` option passed to the module (more on this in the next step). So the relative path from our `middleware/index.js` file to the nuxt `middleware.js` file is `../../middleware.js`. After we have access to the nuxt middleware object, we can simply add a new function to the object, giving us access to the context of the app, just like adding a default export from a nuxt middleware file. From there, you can do anything that is achievable in a standard middleware file. (Nuxt middleware docs):

// middleware/index.js
import Middleware from '../../middleware'
const options = JSON.parse(`<%= JSON.stringify(options) %>`)
const { namespace } = options
Middleware[namespace] = context => {
const { route, store, app } = context
// simply console logging here to demonstrate access to app context.
console.log('Counter middleware route', route.path)
console.log('Counter middleware store', store.state[namespace].count)
console.log('Counter middleware app', app[`$${namespace}`].value())
}

It’s important to note that you still need to register the middleware by name in the `nuxt.config.js`

The middleware will not be registered and used by Nuxt automatically — just like any other middleware you’ll need to register it on a page-by-page basis, or globally in the `nuxt.config.js`. As you can see above we used the `namespace` variable from the module options to add the middleware function to the Nuxt middleware object, so the name of the middleware in a page or `nuxt.config.js` will be the same as the module’s `options.namespace` var, which in our case defaults to `customCounter` but was set as `counter` when we registered the module.

// nuxt.config.js
{
// ...
router: {
middleware: ['counter']
},
// ...
modules: [
['@carepenny/custom-counter', { namespace: 'counter' }]
]
}

Registering all of our plugins and adding files to the nuxt build folder

The final step for our module is to make sure all of the work in all of the files above gets built into our nuxt build folder and all of the plugins get registered. This will all happen in the `module.js file`. We’ve already partially set up `module.js` and talked about nuxt’s `addPlugin()` function that is available in the default export function. Our `module.js` currently looks like this:

// module.js
const { resolve, join } = require('path')
export default function(moduleOptions) {
// get all options for the module
const options = {
...moduleOptions,
...this.options.customCounter
}

// expose the namespace / set a default
if (!options.namespace) options.namespace = 'customCounter'
const { namespace } = options

// add the debug plugin
this.addPlugin({
src: resolve(__dirname, 'debug.js'),
// ** NOTE: we now use namespace here: **
fileName: join(namespace, 'debug.js'),
options // just pass through the options object
})
}
module.exports.meta = require('./package.json')

We’ll ditch the single `addPlugin()` function that registers the debug plugin, and instead define an array of file-path strings to loop over and register all of the plugins dynamically:

// module.js
const { resolve, join } = require('path')
export default function(moduleOptions) {
// get all options for the module
const options = {
...moduleOptions,
...this.options.customCounter
}

// expose the namespace / set a default
if (!options.namespace) options.namespace = 'customCounter'
const { namespace } = options

// add all of the initial plugins
const pluginsToSync = [
'components/index.js',
'store/index.js',
'plugins/index.js',
'debug.js',
'middleware/index.js'
]
for (const pathString of pluginsToSync) {
this.addPlugin({
src: resolve(__dirname, pathString),
fileName: join(namespace, pathString),
options
})
}
}
module.exports.meta = require('./package.json')

That was easy!

We’ll do the same kind of thing to get all of our plugin dependencies into the build folder in the right location, but this time we’ll use another method that nuxt makes available in the module’s default export function — `addTemplate()`. This function takes an object where we can define a few options:

  • src
    A path to a source file
  • fileName
    The name of the file once built in the nuxt build folder. This is a strange name for this option because it can also be a path string like `/some/folder/file.js`.
  • optoins
    The options to be passed through using lodash templates, just like in plugins.

The function by it’s self would look a little like this:

this.addTemplate({
src: resolve(__dirname, 'something.js'),
fileName: 'customCounter/something.js',
options
})

But we’re going to loop over another array of paths to directories and use the `readdirSync` function availble in the fs module to read each file in the directories, loop over them, and call the `addTemplate` function for each of them. Our complete `module.js` looks a little like this:

// module.js
const { resolve, join } = require('path')
const { readdirSync } = require('fs')
export default function(moduleOptions) {
// get all options for the module
const options = {
...moduleOptions,
...this.options.customCounter
}
// expose the namespace / set a default
if (!options.namespace) options.namespace = 'customCounter'
const { namespace } = options
// add all of the initial plugins
const pluginsToSync = [
'components/index.js',
'store/index.js',
'plugins/index.js',
'debug.js',
'middleware/index.js'
]
for (const pathString of pluginsToSync) {
this.addPlugin({
src: resolve(__dirname, pathString),
fileName: join(namespace, pathString),
options
})
}
// sync all of the files and folders to revelant places in the nuxt build dir (.nuxt/)
const foldersToSync = ['plugins/helpers', 'store/modules', 'components/lib']
for (const pathString of foldersToSync) {
const path = resolve(__dirname, pathString)
for (const file of readdirSync(path)) {
this.addTemplate({
src: resolve(path, file),
fileName: join(namespace, pathString, file),
options
})
}
}
}
module.exports.meta = require('./package.json')

To conclude

Nuxt modules are a great way to share code and separate concerns in your applications. They provide a comprehensive way to extend Nuxt functionality and are highly customizable. Hopefully, this tutorial clears up some questions around modules and illuminates some undocumented parts of module building.

Let me know if you have any suggestions on how to make this better, or if you have any questions or issues following the tutorial, so let me know

Carepenny

Next-generation healthcare software. Our team is working to solve some of the biggest challenges in healthcare software.

Jamie Curnow

Written by

I am a javascript developer living in beautiful Cornwall, UK. Ever curious, always learning.

Carepenny

Carepenny

Next-generation healthcare software. Our team is working to solve some of the biggest challenges in healthcare software.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade