/ nuxt

Configure Nuxt.js to consume ENV variables during run-time

Or how to go against the well established patterns and still win.

In my team we started re-writing the platform that was long in the tooth with a lot of technical debt. It is still serving 50+ million unique viewers, but it well-deserves to be retired.
Performance is naturally of the biggest concern for our new baby as well as SEO.
So we knew we could not rely on traditional SPA patterns. We needed server side rendering and we decided to go with Nuxt.js.

Nuxt.js

Nuxt.js is an opinionated framework for creating universal Vue.js applications in a way that abstracts client/server distribution. It can also be used as a static site generator. It can be wired, for example, to listen for changes from an API and (re-)generate the content making sure it always serves static content for lightning fast performance. Pretty awesome if you ask me.
But this is not something super new. What makes Nuxt.js interesting is the ability to allow developers to spin up an SSR project quickly and start delivering features right away.
Having said that, convention over configuration is rarely something I choose to go with unless there is a really, really good reason. Swimming against an opinionated framework is rarely something that turns out well.
That's where we will go in this post. And it will turn out fine. I promise!

We want to "build once, deploy many"

Our cloud architect approaches me and explains the problem. Every time we change code, the codebase needs to be built specifically for every environment separately. This is often slow. What is the common difference in code across different environments? Environment variables! Although what he asked for makes sense, my initial reaction was:

You want what?
But we have all this artillery of compilers, transpilers, pre- and post-processors at our disposal that take days to set up and that build the code for every environment! Plus, there are so many potential problems for leaking sensitive data to the client.
Build once, deploy many makes perfect sense in the world of software development however. But not necessarily in the Javascript world...

Let's do it!

So I started my research and Google was certainly not my friend. The cynic in me asked "Perhaps for a reason?" 👿
There was some chatter, but no solution. That meant that I actually had to come up with a solution on my own instead of copy-pasting code (that's what we as developers do all the time anyway, right?) 😜

Understanding how Nuxt.js does it

Nuxt.js has a file nuxt.config.js. This file is used when instantiating Nuxt.js programmatically:

const config = require('nuxt.config.js')
const nuxt = new Nuxt(config)

In this file it is possible to define env object of ENV variables. It is a common practice to require config dynamically here. This gets passed to webpack's definePlugin and can be used for client and server-side like so: process.env.propertyName or context.env.propertyName. These variables are then baked into Nuxt.js' build (see .nuxt/utils.js -> getContext). For more info, see the official Nuxt.js env page.

Notice the mention of webpack? Yup, that means compilation and... that this is not going to work for us.

Hack away!

Understanding how Nuxt.js works means that:

  • we can't use env inside nuxt.config.js anymore
  • any other dynamic variables (like inside head.meta) need to be passed to nuxt.config.js object during run-time

The code in server/index.js:

// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')

becomes

// Import extended Nuxt.js options
const config = require('./utils/extendedNuxtConfig.js').default

where utils/extendedNuxtConfig.js is:

import config from 'config'
import get from 'lodash/get'

// Import and Set Nuxt.js options
const defaultConfig = require('../../nuxt.config.js')

// Place extended config here
const extendedConfig = {}

// (Shallow) extend Nuxt.js config here
const nuxtConfig = {
  ...defaultConfig,
  ...extendedConfig
}

// Do final object manipulation for things where
// extending objects is not appropriate
if (get(nuxtConfig, 'head.meta')) {
  nuxtConfig.head.meta.push({
    hid: 'og:url',
    property: 'og:url',
    content: config.get('app.canonical_domain')
  })
}

export default nuxtConfig

Big elephant in the room

Ok, this solves a minor problem of having dynamic ENV data outside the scope of env in nuxt.config.js. The original question remains however.

I initially thought I would have an abstraction like sharedEnv.js that would be used for:

  • client: creating env.js file that would be loaded on the client in global window scope (window.env.envKey)
  • server: imported in files where needed (the most straight forward part)
  • isomorphic code: something like context.isClient ? window.env[key] : global.sharedEnv[key] (and write a small utility behind it)

This seems very hacky and just not that great. This abstraction does take care of one of my biggest concerns which was leaking sensitive data to the client. Just like env object from nuxt.config.js, this forces everyone on the team to consciously add a value so nothing gets leaked. Not intentionally at least.

Vuex to the rescue

While fiddling with the window scope, I realized that Vuex store is exported to the global window scope. This decision was most likely made because of Nuxt.js' isomorphic nature. Vuex is a Flux inspired data store specifically tailored for Vue.js apps.

So why not use Vuex for our shared env variables?
It is certainly more organic that the previous approach and this data is in a way global state so it fits the bills perfectly.

We start with our good friend server/utils/sharedEnv.js

import config from 'config'

/**
 * Set up object that will be available to both server and client.
 * For the sake of simplicity, please keep this object flat.
 * Be extra diligent about not leaking any sensitive info here!!
 *
 * @type       {Object}
 */
const sharedEnv = {
  // ...
  canonicalDomain: config.get('app.canonical_domain'),
}

export default sharedEnv

This is executed during the server start-up process. We then add it to our Vuex store.

/**
 * Gets the shared environment.
 * Documentation suggests it is only executed server side, but not wrapping
 * this in a conditional results in an error.
 * https://nuxtjs.org/guide/vuex-store/#the-nuxtserverinit-action
 *
 * @return     {Object}  Shared environment variables.
 */
const getSharedEnv = () =>
  process.server
    ? require('~/server/utils/sharedEnv').default || {}
    : {}

// ...

export const state = () => ({
  // ...
  sharedEnv: {}
})

export const mutations = {
  // ...
  setSharedEnv (state, content) {
    state.sharedEnv = content
  }
}

export const actions = {
  nuxtServerInit ({ commit }) {
    if (process.server) {
      commit('setSharedEnv', getSharedEnv())
    }
  }
}

We take advantage of the fact that nuxtServerInit runs on... uhm... server init. It is possible to save our shared env data. Notice the complication here: getSharedEnv method which then checks if this is run on server again. This is necessary to ensure that the client does not run into error and try to execute require.

Consume it!

Now that it we have our shared env variables in our store, we can consume it from our components like so:
this.$store.state.sharedEnv.canonicalDomain

Win!

Not so fast. What about plugins?

Certain plugins may need environment variables to be configured when letting know Vue.js we want to use it: Vue.use(MyPlugin, { someEnvOption: 'no access to vuex store here' })

We ran into a racing condition here. Vue.js is trying to initialize itself before Nuxt.js registers our sharedEnv object to Vuex store.
While exported function that registers a Vue.js plugins provides us a context object (from which we have access to the store), sharedEnv is still empty.

The answer to this is making the plugin function async and await a manual nuxtServerInit dispatch. This ensures that Vue.js waits until we have our sharedEnv object available.

Here's the code:


import Vue from 'vue'
import MyPlugin from 'my-plugin'

/**
 * Adds configured MyPlugin asynchronously.
 */
export default async (context) => {
  // perform a store action manually to have access to `sharedEnv` object
  await context.store.dispatch('nuxtServerInit', context)

  const env = { ...context.store.state.sharedEnv }

  Vue.use(MyPlugin, { option: env.someKey })
}

Now it is a win!

In and out. Ciao!

Vanja

Vanja

Gadget junkie, shutterbug, runs on green tea, beer and chocolate.

Read More
Configure Nuxt.js to consume ENV variables during run-time
Share this