article

Makefile for Node.js developers

25 Jan 2018 | 4 min read

pattern/design file

Cover image by Scott Doxey

TL;DRcheck out the example here

Configuration used to be easy:

  • config.dev.js
  • config.prod.js
  • config.test.js

and done. Except that those weren't environments as we have them today (k8s, Travis, Compose, to name a few).

Since early 2017 we tried out various ways to work with environment variables and configuration to achieve a Twelve-Factor-App:

The twelve-factor app stores config in environment variables

The Environment Issue

When working with node.js and the package.json-file, our biggest issue was the local development machine, as it’s supposed to represent two different environments:

  • development
  • test

The problem starts with the most basic environment variable that every Node.js developer should know: NODE_ENV. On our machines, we use development and on your Continuous Integration (CI) platform we use test as NODE_ENV. Initially, we just added the NODE_ENV to the test script in package.json:

“script”: {
“test”:NODE_ENV=test tape *-test.js”
}

This might work for a while, but then you might want to add a different LOG_LEVEL for test and maybe a different DATABASE_HOST and you quickly have a dozen envrionment variables in your package.json. Those variables might be working out on your machine, but how about the CI?

There’s two possible solutions to improve working with environment variables: direnv and dotenv.

direnv

direnv was our first choice, as it automatically loads the variables from a .envrc file the moment you cd into that directory. .envrc looks like this:

export LOG_LEVEL=error
export NODE_ENV=development

This works great, until you start using docker-compose. In docker-compose, you can load default configuration from an .env file, which is the same as the .envrc file above with one tiny exception: it doesn’t work with ‘export’. The docker-compose env-file needs to look like the example below and without the ‘export’ keyword, direnv won’t work:

LOG_LEVEL=error
NODE_ENV=development

dotenv

Next, we tried an npm module called dotenv which can work with the same files & format as docker-compose, but it requiresrequire(‘dotenv’).config() as a first line in your app. This gets annoying when you write unit tests, as you need to start every test with dotenv. You can preload dotenv in your script, but that means this would also be executed on a “real” test environment like Travis, not just your localhost. Overall it got a little bit too messy, so we had a look at Makefiles.

The Node.js Makefile

#!make
MAKEFLAGS += --silent
include .env
export $(shell sed 's/=.*//' .env)
dev:
node_modules/.bin/nodemon index.js
test:
NODE_ENV=test \
LOG_ENABLED=false \
LOG_LEVEL=silent \
npm test
watch:
node_modules/.bin/chokidar 'test/**/*.js' -c 'node_modules/.bin/tape {path}'
.PHONY: test
.PHONY: dev
.PHONY: watch

The environment variables in .env are your default for development purposes and loaded into the process via include and export (found on StackExchange). As a developer, I can run make dev to run the app in my local development environment with debugging and all the settings defined in my .env-file. I can also watch the test folder for changes and automatically run the tests with make watch- but still have logging etc. as it would be my development environment. Maybe I have PostgreSQL installed locally and use localhost as hostname for development. If I want to run the app as part of a docker-compose system, I overwrite the database host:

env_file: .env
environment:
- POSTGRES_HOST=postgres

This allows two clean and separated environments on your development machine, while not polluting the codebase or package.json with configuration or code that belongs into the environment.

We decided not to provide any defaults (ie. process.env.NODE_ENV || 'development'), that means for example if LOG_LEVEL isn’t set, the app crashes. This is a great way to make sure the environment is ready before the app is launched and there’s no unexpected behaviour.

And the best part is, the environment variables are only set for the execution of the script, so you won’t have any ADMIN_PASSWORD's or other secrets in your Terminal process that someone could read via echo $PASSWORD.

We’ve put together a minimal example here, feel free to open an issue or pull request.

If you have any questions or comments, please reach out on Twitter or start a discussion on GitHub.

Patrick Heneise

Chief Problem Solver and Founder at Zentered

I'm a Software Enginer focusing on web applications and cloud native solutions. In 1990 I started playing on x386 PCs and crafted my first “website” around 1996. Since then I've worked with dozens of programming languages, frameworks, databases and things long forgotten. I have over 20 years professional experience in software solutions and have helped many people accomplish their projects.

pattern/design file