Is NODE_ENV an Anti-Pattern?

(In which I make the argument that using a configuration file (or two) is a better idea than setting the NODE_ENV environment variable and propose the use of the SN Props package to make this process easier.)

Like everyone else in the node community, I started out using the NODE_ENV environment variable to set various details in my apps. I would do something like this to start an app:


   $ NODE_ENV=dev /usr/bin/node foo.js

The shell would set the environment variable NODE_ENV to the string "dev" and launch the foo.js node application. Inside foo.js, we would do something like this:


   var port, host;

   if( "prod" === process.env.NODE_ENV ) {
     port = 80; host = "0.0.0.0";
   } else if( "dev" === process.env.NODE_ENV ) {
     port = 8080; host = "127.0.0.1";
   } else {
     console.log( "ERROR: unsupported NODE_ENV value: " +
       process.env.NODE_ENV );
     process.exit(1);
   }

   // insert other code here

   server.listen( port, host );

And honestly, there's nothing seriously wrong with this. It lets you express different application behavior based on whether you're running in development-mode or if you've deployed to production. But then I saw this:


   var port, host, db_pass;

   if( "prod" === process.env.NODE_ENV ) {
     port = 80; host = "0.0.0.0"; db_pass = "tq6TJwFR";
   } else if( "dev" === process.env.NODE_ENV ) {
     port = 8080; host = "127.0.0.1"; db_pass = "2qpwRfKB";
   } else {
     console.log( "ERROR: unsupported NODE_ENV value: " +
       process.env.NODE_ENV );
     process.exit(1);
   }

   // insert code to connect to the database here

   server.listen( port, host );

And where did I see this? In the source repository, of course.

Now I don't want to be too snobbish, I've written apps that check the password into the source repo before. At the time I thought it was a simple, throw-away app that no one would use after a couple months. By the time the black-hats found this in the source repo, the app would be long-retired. Except that's never the way the world works. The code got used by a different group in the organization and then sold to a third party. By the time I heard what had happened, one of the third parties had lined up a SAS-70 audit they may have failed because of a fixed password in the system.

Moral of the story? Don't hard-code database passwords into your app.

"But what does this have to do with NODE_ENV being an anti-pattern?" you ask.

Simple, using NODE_ENV to set application behavior makes it easy for a developer to do bad things (like hard-coding dev & production environment passwords into the app.) In my apps, I've replaced the use of NODE_ENV with what I call the "Concatenated Config" pattern.

Avoiding Application Brittleness with the Concatenated Config Pattern

Instead of setting my config parameters from an environment variable inside the code, I use multiple JSON files to hold config settings and import them as a javascript object. The SN Props package does all the heavy lifting for you. So now, I launch an app like this:


   $ node ./bar.js \
     --config file:///opt/bar/dev.json \
     --config file:///opt/bar/db_dev.json

SN Props reads the command line looking for URLs pointing to JSON files. In this example, it grabs the files /opt/bar/dev.json and /opt/bar/db_dev.json, smooshes them together and passes them to your app via a callback. Here's what the code in bar.js looks like:


   require( 'sn-props' ).read( function( props ) {
     // db connection setup code goes here
     // http server code goes here
    } );

And the contents of the two config files look something like this:

/opt/bar/production.json:


   {
     "listen": {
       "port": 8080,
       "host": "127.0.0.1"
     }
   }

/opt/bar/db_db002.json:


   {
     "mysql": {
       "host": "127.0.0.1",
       "port": 3306,
       "user": "dev",
       "pass": "goats!"
     }
   }

and, of course, you probably want to define other configs to use in production:

/opt/bar/production.json:


   {
     "listen": {
       "port": 80,
       "host": "0.0.0.0"
     }
   }

/opt/bar/db_db002.json:


   {
     "mysql": {
       "host": "db002.internal.example.com",
       "port": 3306,
       "user": "bar",
       "pass": "JC7VwyguUqHm8D3J"
     }
   }

And if you trust your internal infrastructure, you can replace references to file: URLs with references to http: URLs. You can probably figure out what this does:


   $ node ./bar.js \
     --config https://config.example.com/dev.json \
     --config file:///opt/bar/db_dev.json

Let's Recap:

The benefits of this pattern are:

There are a few draw-backs, but they're relatively mild:

In my experience, the benefits outweigh the drawbacks. I no longer embed application policy in the code itself; instead, i put it in a configuration file that's has slightly different access control and logging settings.