Like John Ruddell wrote in the comments, we should still use NODE_ENV=production
in a staging environment to keep it as close as prod as possible. But that doesn't help with our problem here.
The reason why NODE_ENV
can't be used reliably is that most Node modules use NODE_ENV
to adjust and optimize with sane defaults, like Express, React, Next, etc. Next even completely changes its features depending on the commonly used values development
, test
and production
.
So the solution is to create our own variable, and how to do that depends on the project we're working on.
The documentation says:
Note: You must create custom environment variables beginning with REACT_APP_
. Any other variables except NODE_ENV
will be ignored to avoid accidentally exposing a private key on the machine that could have the same name.
It was discussed in an issue where Ian Schmitz says:
Instead you can create your own variable like REACT_APP_SERVER_URL
which can have default values in dev and prod through the .env
file if you'd like, then simply set that environment variable when building your app for staging like REACT_APP_SERVER_URL=... npm run build
.
A common package that I use is cross-env
so that anyone can run our npm scripts on any platform.
"scripts": {
"build:uat": "cross-env REACT_APP_SERVER_URL='http://uat.api.azure.com:8080' npm run build"
Any other JS project
If we're not bound to CRA, or have ejected, we can easily configure any number of environment configurations we'd like in a similar fashion.
Personally, I like dotenv-extended
which offers validation for required variables and default values.
Similarly, in the package.json
file:
"scripts": {
"build:uat": "cross-env APP_ENV=UAT npm run build"
Then, in an entry point node script (one of the first script loaded, e.g. required in a babel config):
const dotEnv = require('dotenv-extended');
// Import environment values from a .env.* file
const envFile = dotEnv.load({
path: `.env.${process.env.APP_ENV || 'local'}`,
defaults: 'build/env/.env.defaults',
schema: 'build/env/.env.schema',
errorOnMissing: true,
silent: false,
});
Then, as an example, a babel configuration file could use these like this:
const env = require('./build/env');
module.exports = {
plugins: [
['transform-define', env],
],
};
Runtime configuration
John Ruddell also mentioned that one can detect at runtime the domain the app is running off of.
function getApiUrl() {
const { href } = window.location;
// UAT
if (href.indexOf('https://my-uat-env.example.com') !== -1) {
return 'http://uat.api.azure.com:8080';
}
// PROD
if (href.indexOf('https://example.com') !== -1) {
return 'http://my.cool.api.com';
}
// Defaults to local
return 'http://localhost:3004';
}
This is quick and simple, works without changing the build/CI/CD pipeline at all. Though it has some downsides:
- All the configuration is "leaked" in the final build,
- It won't benefit from dead-code removal at minification time when using something like
babel-plugin-transform-define
or Webpack's DefinePlugin
resulting in a slightly bigger file size.
- Won't be available at compile time.
- Trickier if using Server-Side Rendering (though not impossible)
APP_ENV
variable. – Emile Bergeron.env
files, say.env.uat
. – Emile Bergeron