7 Tips For Building A Large Nuxt App

Nuxt’s very opinionated when it comes to code structure. Its conventions can save you a lot of time making decisions. After one year using it on large codebases though, I’m glad there’s considerable wiggle room for customization. In this article, I lay out a few tips that have either simplified common code patterns or helped me better manage my large Nuxt codebases.

Bear in mind this article concerns Nuxt’s 1.4.x version. At the time of writing, work on a substantially revamped 2.0 version is already underway. Also, Nuxt is mostly known as a SSR toolkit, but it’s perfectly capable of building SPA apps as well. I like the fact that Nuxt offers a codebase organization standard for all Vue applications.

Use a custom routes index file

Nuxt’s latest release includes extendRoutes(), a way to add custom routes to Nuxt’s automatic route setup based on the pages/ directory. You can also bypass Nuxt’s setup entirely by using a routes index file. While you still need to use pages as the directory, you can add a index.js to it:

module.exports = [
   {
     name: 'my-route',
     path: '/my-route',
     component: 'src/pages/foobar.vue'
   }
]

In nuxt.config.js, use this as your extendRoutes():

extendRoutes (nuxtRoutes, resolve) {
  nuxtRoutes.splice(0, nuxtRoutes.length, ...routes.map((route) => {
    return { ...route, component: resolve(__dirname, route.component) }
  }))
}

Stateful loading components

You can change Nuxt’s default loader component by setting the loading property in nuxt.config.js. What isn’t immediately obvious is the fact that you can access Nuxt’s Vuex store from it. This can help extend the loader’s running time while there are any app-triggered HTTP requests active. One common pattern I use for this is setting a setActive mutation, that takes either 1 or -1 to determine the start and end of an HTTP request. Then I can check for active in my Vuex state before clearing the loader:

<template>
  <div class="loader" />
</template>

<script>
import { mapState } from 'vuex'
export default {
  data: () => ({
    loader: null,
    watching: false
  }),
  computed: mapState({
    active: (state) => state.active
  }),
  watch: {
    active (isActive) {
      if (this.watching && !isActive) {
        // clear loader
        this.watching = false
      }
    }
  },
  methods: {
    start () {
      // start loader
      this.watching = true
    },
    finish () {
      if (this.active) {
        this.watching = true
      } else {
        // clear loader
      }
    }
  }
}
</script>

Depending on your application’s rendering speed, you can tweak the loader behavior with delayed setTimeout calls, or even add extra loader methods that disable the original start() and finish() methods. In my apps, I have added a startNow() method that instantly opens the loader before any route transition actually happens, and a finishNow() method that will only clear the loader when API requests have finished, similar to what is shown above.

Passing data from Koa’s context

When adding CSRF protection to an app, I had to pass the CSRF token generated by koa-csrf down to nuxtServerInit(). The problem is that nuxtServerInit() gives you req and res references, but no references to Koa’s own context. The solution I found was to copy any context variables I needed to the res object that is passed to Nuxt, as shown below:

ctx.res.csrf = ctx.csrf 
return new Promise((resolve, reject) => {
  ctx.res.on('close', resolve)
  ctx.res.on('finish', resolve)
  nuxt.render(ctx.req, ctx.res, (promise) => {
    promise.then(resolve).catch(reject)
  })
})

Use a Vuex store factory function

Nuxt has a very practical way to set up a Vuex store, by automatically picking up submodules under the store/ directory. You can also go a step further and use a class or a function to build your global store.

One pattern I like to use is having a main.js file in the Nuxt root that defines my global Vuex store as a class. For that I use a little helper class I call apistore. With it, my store/index.js looks like this:

import { MyAppVuexStore } from '@/main'

const store = MyAppVuexStore.makeStore()

export const state = store.state
export const getters = store.getters
export const mutations = store.mutations
export const actions = store.actions

In MyAppVuexStore, I basically define Vuex actions as instance methods, the init instance method becomes nuxtServerInit() and the global state is also defined as an instance method. I can also use APIStore.use() to stack store dispatcher mixins together in the global store, while still using submodule files in store/ as needed. Personally, I keep that location for API-related stores, that is, stores that track data from remote APIs. This way I can keep one store submodule per API resource.

export class MyAppVuexStore {
  state () {
  }
  init () { // nuxtServerInit
  }
  someAction () {
  }
}

You can extend the apistore helper class further to use class methods as mutations, or getter methods as store getters if you like. In my code, I tend to use apistore’s update mutation (which updates all props defined in the payload) for the global store, and regular mutation code for store submodules.

Generating extra files with extend()

If you want to extend Nuxt’s compiler with something of your own and don’t want to go all the way in building a Nuxt plugin, you can add a function to build.extend in nuxt.config.js that will fs.writeFileSync() something into your source directory and it’ll still get picked up by Nuxt’s builder. I used this recently to automatically populate a series of API dispatchers from server methods:

const api = require('../server/api')

const formatAPIMethod = (sig, func) => {
  return func.toString()
    .replace(/__apiMethod__/, `apiMethod: '${sig}'`)
    .replace(/\n {2}/g, '\n')
}

exports.genAPIMethods = function () {
  let notice = `// This file is autogenerated\n\n`
  let file = `${notice}module.exports = (store) => ({`
  const funcs = []
  Object.keys(api).forEach((r) => {
    file += `\n  ${r}: `
    const methodDefs = JSON.stringify(api[r], (k, v) => {
      if (typeof v === 'function') {
        funcs.push(k)
        return '__function__'
      } else {
        return v
      }
    }, 4)
    .replace(/\n}/g, '\n  },')
    file += methodDefs
    .replace(/\n(\s+)"([^"]+)"/g, (_, ws, name) => {
      return `\n${ws}${name}`
    })
    .replace(/"__function__"/g, (m) => {
      // The following is needed so ESLint accepts this
      /* global store __apiMethod__ */
      return formatAPIMethod(`${r}.${funcs.shift()}`, (payload, shouldDispatch = true) => {
        return store.dispatch('api', {
          __apiMethod__,
          payload,
          shouldDispatch
        }, {root: true})
      })
    })
  })
  file = file.slice(0, -1)
  file += '\n})\n'
  return file
}

I then proceed to call genAPIMethods() right at the start of builder.extend(). Thanks to Function.prototype.toString() and JSON.stringify()’s ability to filter out (and tag) unknown JSON types, I was able to generate a file full of API call dispatchers (through Vuex actions) automatically from my server’s API file:

module.exports = (store) => ({
  resource: {
    method: (payload, shouldDispatch = true) => {
      return store.dispatch('api', {
        apiMethod: 'resource.method',
        payload,
        shouldDispatch
      }, {root: true})
    }
  ...

Initializing global client code

Nuxt fires window.onNuxtReady(app) when the Nuxt instance loads, passing it as the first and only parameter. You can use to this to perform global client initialization code, service workers or ad tracking scripts etc. In my apistore helper I use the client static method to define it, so I can have onNuxtReady() code defined in my main.js file.

export class MyAppVuexStore {
  static client (app) {
    app.$store.dispatch('someInitAction', 'from client init code')
  }
}

Axios Request Interceptors

I’ve been using axios as my HTTP networking library for as long as I’ve used Nuxt. It never failed me. My favorite feature is its request and response interceptors. Fortunately for Nuxt there’s nuxt-axios, which lets you define them as plugins if needed:

export default function ({ $axios }) {
  $axios.onRequest((config) => {
    // ...
    // Refresh JWT token if needed
    // ...
    config.headers['Authorization'] = `Bearer ${token}`
    return config
  })
}

With nuxt-axios, you’ll have an $axios instance available on both server and client code, which you can seamlessly use oh the same networking methods. Remember you can also create your API proxy on the server, bypassing such API wrangling complexities in the client. For more on that, check out my Nuxt and Koa boilerplate.

Jonas Galvez's Picture

About Jonas Galvez

Jonas Galvez started his carrer as an ActionScript developer 18 years ago. Since then, he has had a 10-year long affair with Python and has now returned to ECMAScript land. Works as a senior software engineer at Brazilian STORED e-commerce. He also created and maintains Plainbudget. Hardcore minimalist and pool player.

Comments