Vue.js environment variables

Olivier Pichon
dzangolab
Published in
6 min readJan 26, 2020

--

Environment variables in vue.js are of course critical in order for your app to support multiple environments (local, develop, staging, production). However, they often prove more trouble than they are worth. Every developer I know has spent a few hours trying to figure out weird bugs related to the way env vars are loaded (typically strings when you expect a boolean), and then a couple more hours trying to design a definitive solution to handle them. Collectively, the amount of man-hours wasted by the industry must be staggering. We propose here a simple solution for handling environment variables in a vue.js app. We will create a vue plugin shortly.

Dotenv the right way

dotenv is a fantastic tool for managing your env vars, and you should use it. If your app uses vue-cli, then it’s already included.

With dotenv, you define your env vars in a .env file saved in your project’s root directory. This file should not be under version control. We strongly suggest including a .env.example file, also in your project’s root directory, which will be under version control, to suggest sensible defaults and indicate which env vars are available for any other dev to override. Env vars defined in .env are available via process.env.VAR_NAME (except within vue.js app, as explained below).

Make sure .env is not under version control

# .gitgnore
.env*
!.env.example

Your README file should include instructions like those:

My app
======
...Installation
------------
* Copy .env.example to .env and adjust the values where necessary.

Here is an example of an .env.example file:

# .env.exampleHOST=localhost
PORT=8080
VUE_APP_API_ROOT=https://localhost:8081
VUE_APP_API_KEY=SECRET

and its corresponding .env file:

# .envHOST=localhost
PORT=20000
VUE_APP_API_ROOT=https://localhost:20001
VUE_APP_API_KEY=1234567890abcdef

Your .env file should contain values for your local environment. This way you can use custom settings without interfering with any other developer’s settings. Because it is under version control, the .env.example file should not contain any sensitive or secret values.

The examples above suggest a port of 8080 for running your app locally, which was overridden to 20000. This would be used as follows:

# vue.config.jsmodule.exports = {
...,
devServer: {
host: process.env.HOST || 'localhost',
open: true,
port: process.env.PORT || 8080
},
...
}

Note that env.example shows the default values defined in the block of code above.

Using env vars via process.env

All env vars are available via process.env.VAR_NAME. With vue-cli, if you want the env vars to be available within the client bundle, you must prefix their name with VUE_APP_ (https://cli.vuejs.org/guide/mode-and-env.html#using-env-variables-in-client-side-code).

Code outside of the client bundle (eg vue.config.js) has access to all env vars. The example in the previous paragraph made use of HOST and PORT vars.

Best practices

Contrary to the vue-cli official docs, I strongly recommend following the dotenv best practices and not using mode-specific env files (eg .env.test, .env.develop, etc.). Env vars should be injected at build- or run-time only.

Further, I recommend not using calls to process.env within your code, for several reasons:
1. process.env is an interface between your app and the environment. There is no reason to let it deep inside your code.

2. Env vars processed by dotenv are always strings. Sooner or later, this inevitably leads to confusion. If I define a flag as an env var (eg to toggle a feature) within the code I expect it to be a Boolean. Getting a string will cause confusion, and then having to add some silly code to test the truthiness of the string here and there is just messy.

3. Vue.js apps can have lots of env vars, and the .env file can quickly become bloated. If like me (and as you should), you want to organize your env vars alphabetically, then it becomes difficult to clearly indicate which must be defined locally, which can be overridden, and which must not be overridden. Ideally .env should only be used for env vars that can or need to be overridden locally.

Config.js

As a solution, I want a “single point of entry” so to speak, ie a single piece of code that will process all the env vars for the app, and return the properly typed value.

We can start with this:

# config.jsconst config = {
apiBaseUrl: process.env.VUE_APP_API_BASE_URL,
locale: process.env.VUE_APP_LOCALE,
sso: {
enabled: process.env.VUE_APP_SSO_ENABLED,
...
},
...
}
export {
config
}

For convenience, this can be defined as a Vue plugin:

export default {
install (Vue) {
Vue.appConfig = config
Vue.prototype.$appConfig = config
}
}

In your code, instead of accessing env vars, you access config settings via this.$appConfig or $appConfig, eg this.$appConfig.apiBaseUrl or $appConfig.sso.enabled.

This config file should also provide suitable defaults. My practice is to define defaults for the local environment. For server-side deployments (develop, staging, production), I prefer to define all env vars explicitly at build time or runtime, so any default will be overridden.

The simple way to do this is like so:

# config.jsconst config = {
apiBaseUrl: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8081',
locale: process.env.VUE_APP_LOCALE || 'en',
sso: {
enabled: process.env.VUE_APP_SSO_ENABLED || false
},
...
}

However, I also want this config file to reliably cast all config settings to the appropriate type. To do this, I use this extra logic:

# config.jsconst config = {
apiBaseUrl: parse(process.env.VUE_APP_API_BASE_URL, 'http://localhost:8081'),
locale: parse(process.env.VUE_APP_LOCALE, 'en'),
sso: {
enabled: parse(process.env.VUE_APP_SSO_ENABLED, false),
...
},
...
}
function parse (value, fallback) {
if (typeof value === 'undefined') {
return fallback
}
switch (typeof fallback) {
case 'boolean' :
return !!JSON.parse(value)
case 'number' :
return JSON.parse(value)
default :
return value
}
}

Because all config settings are defined in a single file, it is easy for a developer to lookup which settings are available, and the .env.example file can be reserved to indicate only those env vars that need to be overridden locally. Of course, the .env file itself would only need to define those same settings. In most cases, the .env.example and .env files can be pruned to nothing.

At this point, we’ve achieved all our stated goals: a single file providing all app configuration settings based on environment variables loaded via dotenv, cast to the appropriate type, and with the ability to define defaults so that the .env file can be shrunk to a minimum.

But we can go one step further and pack even more value by adding support for feature toggles.

Feature toggles

Feature toggles are extremely useful, and you really want to use them. There are a number of good packages available for this, but this is so simple to implement in config.js that I’d rather avoid yet another dependency.

# config.jsconst config: {
...,

features: {
example: parse(process.env.VUE_APP_FEATURE_EXAMPLE, false),
...
},
...
}
function feature (name) {
return config.features[name]
}
export default {
install (Vue) {
Vue.appConfig = config
Vue.feature = feature
Vue.prototype.$appConfig = config
Vue.prototype.$feature = feature
}
}

To define a feature toggle, add a child property to config.features. By convention, to keep things simple, I name the corresponding env var after the toggle prefixed withVUE_APP_FEATURE_. Eg the example feature toggle is defined by env var VUE_APP_FEATURE_EXAMPLE.

To test if that feature is enabled, callthis.$feature('example') or $feature('example').

Conclusion

Here is the full config.js file:

# config.jsconst config = {
...,
features: {
...
}.
...
}
function feature (name) {
return config.features[name]
}
function parse (value, fallback) {
if (typeof value === 'undefined') {
return fallback
}
switch (typeof fallback) {
case 'boolean' :
return !!JSON.parse(value)
case 'number' :
return JSON.parse(value)
default :
return value
}
}
export {
config
}
export default {
install (Vue) {
Vue.appConfig = config
Vue.feature = feature
Vue.prototype.$appConfig = config
Vue.prototype.$feature = feature
}
}

Copy this file to your src/ directory and include it in main.js:

# main.jsimport Vue from 'vue`
import configPlugin from '@/config'
Vue.use(configPlugin)

--

--

Olivier Pichon
dzangolab

Tech entrepreneur. CTO at dzango tech accelerator.