In the code below, I am building a NODE_ENV-sensitive config object from environment variables.
let username
let password
let cluster
let hosts
let databaseName
let replicaSet
if (process.env.NODE_ENV === 'production') {
username = process.env.ATLAS_HUB_USERNAME
password = process.env.ATLAS_HUB_PASSWORD
cluster = process.env.ATLAS_CLUSTER
hosts = process.env.ATLAS_HOSTS
databaseName = process.env.ATLAS_DATABASE
replicaSet = process.env.ATLAS_REPLICA_SET
} else {
username = process.env.MONGO_HUB_USERNAME
password = process.env.MONGO_HUB_PASSWORD
cluster = process.env.MONGO_CLUSTER
hosts = process.env.MONGO_HOSTS
databaseName = process.env.MONGO_DATABASE
replicaSet = process.env.MONGO_REPLICA_SET
}
const config = { username, password, cluster, hosts, databaseName, replicaSet }
In this new age of fancy spread and rest operators, I hate polluting my files with code like this which repeat the same variable name multiple times and uses let
instead of const
for the wrong reasons, all for something simple and frequent. I could use the ternary operator to get something way better:
const config = {
username: process.env.NODE_ENV === 'production' ? process.env.ATLAS_HUB_USERNAME : process.env.MONGO_HUB_USERNAME,
password: process.env.NODE_ENV === 'production' ? process.env.ATLAS_HUB_PASSWORD : process.env.MONGO_HUB_PASSWORD,
cluster: process.env.NODE_ENV === 'production' ? process.env.ATLAS_CLUSTER : process.env.MONGO_CLUSTER,
hosts: process.env.NODE_ENV === 'production' ? process.env.ATLAS_HOSTS : process.env.MONGO_HOSTS,
databaseName: process.env.NODE_ENV === 'production' ? process.env.ATLAS_DATABASE : process.env.MONGO_DATABASE,
replicaSet: process.env.NODE_ENV === 'production' ? process.env.ATLAS_REPLICA_SET : process.env.MONGO_REPLICA_SET
}
But even this seems one step short of what modern js should be able to do. I'd now like to get rid of the repeated process.env.NODE_ENV
, I just can't figure out how (apart from creating a new const with a shorter name). If I had a magic wand, I'd write something along the following lines:
const config = ({
username: [process.env.MONGO_HUB_USERNAME, process.env.ATLAS_HUB_USERNAME],
password: [process.env.MONGO_HUB_PASSWORD, process.env.ATLAS_HUB_PASSWORD],
cluster: [process.env.MONGO_CLUSTER, process.env.ATLAS_CLUSTER],
hosts: [process.env.MONGO_HOSTS, process.env.ATLAS_HOSTS],
databaseName: [process.env.MONGO_DATABASE, process.env.ATLAS_DATABASE],
replicaSet: [process.env.MONGO_REPLICA_SET, process.env.ATLAS_REPLICA_SET]
}).*[new Number(process.env.NODE_ENV === 'production')]
But I don't, and it's not even all that great, sooo, any suggestions?
I thought of using a function, like below, but this just introduces another dependency you need to internalize for a simple batch conditional assignment operation... And if the function is in-line, there is duplication across files and it's frankly just confusing.
function fromEach (obj, key) {
const final = {}
Object.keys(obj).forEach((k) => {
final[k] = obj[k][key]
})
return final
}
const config = fromEach({
username: [process.env.MONGO_HUB_USERNAME, process.env.ATLAS_HUB_USERNAME],
password: [process.env.MONGO_HUB_PASSWORD, process.env.ATLAS_HUB_PASSWORD],
cluster: [process.env.MONGO_CLUSTER, process.env.ATLAS_CLUSTER],
hosts: [process.env.MONGO_HOSTS, process.env.ATLAS_HOSTS],
databaseName: [process.env.MONGO_DATABASE, process.env.ATLAS_DATABASE],
replicaSet: [process.env.MONGO_REPLICA_SET, process.env.ATLAS_REPLICA_SET]
}, new Number(process.env.NODE_ENV === 'production'))
Note: I use new Number(process.env.NODE_ENV === 'production')
in these examples, but I don't like it, feel free to propose something better!
2 Answers 2
Ideally, I would suggest two changes the the overall design:
- the environment variable names could have one consistent prefix, e.g.
MONGO_
andATLAS_
- the keys of the config object could match the environment variable names, e.g.
databaseName
would matchMONGO_DATABASE_NAME
If that for some reason is not possible, then we can hardcode the config-to-environment name mapping:
function getConfigurationFor(prefix) {
const configMap = {
username: 'HUB_USERNAME',
password: 'HUB_PASSWORD',
cluster: 'CLUSTER',
hosts: 'HOSTS',
databaseName: 'DATABASE',
replicaSet: 'REPLICA_SET'
}
prefix = prefix.toUpperCase() + '_'
return Object
.entries(configMap)
.reduce((result, [key, envKey]) => {
result[key] = process.env[prefix + envKey]
return result
}, {})
}
const config = getConfigurationFor(
process.env.NODE_ENV === 'production'
? 'atlas'
: 'mongo'
)
if you can make these design changes, then we have more flexibility: we can create a function that would read all the environment variables that start with a certain prefix and return this as a plain JavaScript object:
const getEnvironmentForPrefix(prefix, source) {
const camelCase = require('lodash.camelcase')
return Object
.entries(source || process.env)
.filter([key] => key.startsWith(prefix))
.map(([key, value]) => [key.substring(prefix.length), value])
.map(([key, value]) => [camelcase(key), value])
.reduce((result, [key, value]) => {
result[key] = value
return result
}, {})
}
const config = getEnvironmentForPrefix(
process.env.NODE_ENV === 'production'
? 'ATLAS_'
: 'MONGO_'
)
Years later, I'm using a different approach of expecting environment-appropriate values in the environment variables and delegating the decision to the execution context.
First, we define the values for the variables in various environments:
.env.development
HUB_USERNAME=usernameForDevelopment
HUB_PASSWORD=passwordForDevelopment
CLUSTER=clusterForDevelopment
HOSTS=hostsForDevelopment
DATABASE=databaseForDevelopment
REPLICA_SET=replicaSetForDevelopment
.env.production
HUB_USERNAME=usernameForProduction
HUB_PASSWORD=passwordForProduction
CLUSTER=clusterForProduction
HOSTS=hostsForProduction
DATABASE=databaseForProduction
REPLICA_SET=replicaSetForProduction
Next, we expect the correct file to have been used and the variables to have environment-appropriate values, removing the need for the process.env.NODE_ENV === 'production'
conditional altogether:
index.js
const env = process.env
const config = {
username: env.HUB_USERNAME,
password: env.HUB_PASSWORD,
cluster: env.CLUSTER,
hosts: env.HOSTS,
databaseName: env.DATABASE,
replicaSet: env.REPLICA_SET
}
The execution context can decide which set of variables to use.
For example, in Next.js, it looks like this:
next dev // Environment variables fetched from `.env.development`
next start // Environment variables fetched from `.env.production`
Or, using Node.js directly (with the help of the dotenv
package, don't forget to npm i -D dotenv
):
node -r dotenv/config index.js dotenv_config_path=.env.development // DEV
node -r dotenv/config index.js dotenv_config_path=.env.production // PROD