At decent labs we know a thing or two about building software. In our pursuit of client happiness paired with using solid software fundamentals to build resilient, scalable systems, we’ve iterated through the process of building APIs maaaany times.
I took the time yesterday to (start to) take all of my current best practices around API design and put them into one “boilerplate” repo.
This post kicks off a series of blog posts aimed at walking through and explaining this repository in detail.
To let readers know which technologies, libraries, and concepts we’ll be going over (and to help with SEO):
- highly-supported runtimes via
- simple servers via
- postgres via
- programmatic sql via
- extreme access via
- es6 support via
- testing via
- logging via
- error handling via custom errors
- linting via
- hot reloading via
We need to install some tools on our system to support the project and tests actually running locally. Then we’ll need to install some packages into our project to support the various repetitive tasks of developing code.
System Development Dependencies
yarn, use some sort of version manager, like
yvm. It's a good idea for when you're working with different projects with different runtime requirements on your computer. This isn't necessary though, installing from your favorite package manager, or by downloading from their websites, will work just fine.
Project Development Dependencies
If you’re creating a new project, go ahead and
yarn init your way into it. If you're adding the contents of this guide into an existing project, good luck!
Now we’re getting into it! Let’s install a bunch of packages to aid with development. Run this in your terminal:
$ yarn add \
chaiare for the test suite
dotenvis for our environment variables
eslint-*is for linting
nodemonis for watching for file changes on the system
rimrafis for deleting shit, like when we're creating a production build and want to clean the project first
Now we’ve got a
node project with some tools installed. Neat.
I like to build projects in a way that use as much “default” configuration options from third party tools as possible. This just keeps things as simple as possible.
Sometimes, though, that’s just not possible. Let’s create some new config files for some of our tools.
If you’ve ever worked in a
git repository before, this should be of no surprise. Need that
.gitignore. I like to use https://gitignore.io for my
.gitignore needs. Here's what gitignore.io has for the standard
node project: https://gitignore.io/api/node.
There’s one more thing to add to our
.gitignore, which will be the directory that production builds get transpiled into. We'll be calling that directory
dist, so add an entry for
dist in your
.gitignore from the final repo: https://github.com/decentorganization/decent-api/blob/master/.gitignore
I’ve also got and entry for
.vscode in there, too, because I like to tell VSCode to auto-format on save, for certain projects, and I don't want those options part of the repo: they're really just my preference.
To manage environment variables on our development machines, we’ll use
dotenv. This package relies on a file called
.env to read environment variables from, and inject them as actual system environment variables into the running
.gitignored, so I like to create a file called
.env.example and commit that into the repository. In the README, instruct developers to copy the contents from
.env.example into a new (ignored) file named
For our project, the
.env(.example) file will hold a port number on which the API should be exposed (
API_PORT), database connection information (
DB_*) and a "base" logger string (
LOGGING_BASE), which can be anything. It's used to prefix logs.
All of the values in this
.env.example are defaulted to in the actual codebase. A developer really only needs to create their own
.env file if they want to override anything here.
babel, we'll use a
Honestly, I haven’t taken much time trying to understand the nuances of
babel, so I'll just show you what works for our project. This works for our needs:
nodemon is a simple package that monitors for changes on your filesystem, and kicks of scripts when they do. We'll use
nodemon to watch for changes to our source code and test code, then to run the linter and run the tests and restart the development server.
We will ignore the specific directory that holds migration files, because things get weird if those get executed before we’re done writing them. (The app executes migrations automatically upon startup, so if we’re restarting it in the middle of writing a migration via
nodemon, a half-formed migration will be executed against the development database and sucks.)
We’re using docker in this project for one very specific purpose: to provide us with a database. We’re not dockerizing our project (yet lol).
docker-compose.yml to specify that we just need a basic
The Non-Development-Specific Dependencies
Now we’ll need to install the packages which will support and enable the actually running API process. Do this in your terminal:
$ yarn add \
expressis the minimalist web framework
corsis a little
expressmiddleware package that enables CORS for our API
express-list-endpointsis a nice package that enables us to output a list of all the endpoints of our API. Good for self-documentation.
morganare for logging
Now we’ve got all of the tools and packages installed that we’ll need to support writing some actual code.
One more thing to do before we can start writing code. We’ll add a bunch of scripts to
package.json to help us do some common tasks. Add the following
scripts object into your
"build": "babel ./src --out-dir dist",
"clean": "rimraf dist",
"dev": "NODE_ENV=development dotenv yarn dev:logs",
"dev:logs": "DEBUG=$LOGGING_BASE:* yarn server",
"lint": "eslint . --ignore-pattern '/dist/*'",
"migration": "knex migrate:make --migrations-directory ./src/database/migrations",
"prod": "yarn clean && yarn build && yarn server:prod",
"server": "babel-node ./src",
"server:prod": "NODE_ENV=production node ./dist",
"start": "yarn prod",
"test": "NODE_ENV=test mocha --recursive --require @babel/register",
clean: deletes the
lint: lints the source code
migration: creates a new
knexmigration and sticks it in the
prod: cleans and builds the project using existing scripts then starts the server via
babel-nodeto kick off a development server using the
server:prod: sets the
NODE_ENVenvironment variables to
productionthen starts node by pointing at the
distdirectory, which is where our built production code lives
start: simply an alias for the
See the final
package.json here: https://github.com/decentorganization/decent-api/blob/master/package.json
Without yet writing a single line of code, we’ve:
- installed some system development dependency tools
- installed some project development dependency packages
- configured our development tools
- installed project dependency tools
- created some scripts to help us do common development tasks
Join me in the next post and we’ll start diving into some real code!
Originally published at https://blog.decentlabs.io on October 17, 2019.