# Makefile for Node.js developers

Andreas Möller recently posted his Makefile for lazy developers approach for PHP developers, which motivated me to publish our Node.js Makefiles and the story how we got there.

- **Author:** Patrick Heneise
- **Published:** 2018-01-25
- **Updated:** 2018-01-25
- **Tags:** docker, productivity, makefile, node, nodejs

---

_[Cover image by Scott Doxey](https://dribbble.com/shots/2738115-Makefile-Logo-v2)_

**TL;DR** —
[check out the example here](https://github.com/zentered/makefile-example)

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](https://12factor.net/config):

> **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`:

```js
“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](https://github.com/direnv/direnv) and
[dotenv](https://www.npmjs.com/package/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:

```bash
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](https://docs.docker.com/compose/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:

```bash
LOG_LEVEL=error
NODE_ENV=development
```

### dotenv

Next, we tried an npm module called
[dotenv](https://www.npmjs.com/package/dotenv) which can work with the same
files & format as docker-compose, but it requires`require(‘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](https://www.npmjs.com/package/dotenv#preload) 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

```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](https://unix.stackexchange.com/a/235254)). 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:

```bash
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](https://github.com/zentered/makefile-example), feel free
to open an issue or pull request.

If you have any questions or comments, please
[reach out on Bluesky](https://bsky.app/profile/zentered.co) or
[start a discussion on GitHub](https://github.com/zentered/zentered/discussions/new?category=ideas-feedback).
