2
\$\begingroup\$

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!

200_success
145k22 gold badges190 silver badges478 bronze badges
asked Oct 14, 2018 at 1:33
\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

Ideally, I would suggest two changes the the overall design:

  • the environment variable names could have one consistent prefix, e.g. MONGO_ and ATLAS_
  • the keys of the config object could match the environment variable names, e.g. databaseName would match MONGO_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_'
)
answered Oct 15, 2018 at 9:08
\$\endgroup\$
1
\$\begingroup\$

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
answered Mar 7, 2023 at 20:18
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.