My Current Stack
- April 26, 2020
As JS developers, we experience a lot of toolchain fatigue. All you have to do is subscribe to a few newsletters, browse on EchoJS, or watch the package numbers at npm and you know there are new libraries and frameworks being released almost hourly. We have our own drinking game about it, so you know we’ve got an issue. When I am building a new backend project, I wanted to go over some of the toolchain that I will reach for and show how I integrate them into a sample project. I’ll start off with the Tl;dr list, with links, and then if you want to delve further you can:
My Stack for Backend Development
- Fastify
- Knex - And yes, I use the fastify-knexjs for integrative purposes
- Convict - Yes, Fastify has some great env and config packages, but for my comfort level, I love Convict by Mozilla
- Dotenv - A classic, and this plus Convict gives me my configuration all the superpowers I want it to have
My process
Assuming you’ve already bought my book (And if you haven’t what are you waiting on? A pandemic?), you know the kind of planning I will do before undertaking the actual coding of an app. There are a number of key things that have to be done before I will put hands to keys and start committing code to the realization of a vision I have for a product. In a separate post, I will go into some of the considerations I will use for developing the front-end of a product, but we’re going to stick to the back-end for now.
Fastify, as you can see from the list above, becomes the centrepiece of a suite of tools brought together to provide the infrastructure of an API backend, with GraphQL as the interface. I have moved from working with Express to working with Fastify for a number of reasons, performance and ecosystem being two of the biggest ones. Express was the go-to for a long time, with dabbles and explorations in hapi, Meteor, and Sails.js. However, Fastify is supplying that solid base on which I feel that I can build products with pride and confidence. It does have a CLI generator, however by default it sets up a pattern that I don’t prefer to work with, so you will find me doing a bit of initial scaffolding manually.
mkdir my_app && cd $_
npm init -y
npm install fastify fastify-gql fastify-helmet fastify-healthcheck fastify-favicon fastify-formbody fastify-sensible fastify-knexjs pg convict dotenv
Config
Before I start setting up the server itself, I want to invest some time in setting up my configuration. Just from experience, I know there are a few things I’m going to want to have available through configuration. These things include, as examples, the port on which the server will be running, my database configuration, maybe an encryption secret, maybe some API keys, and anything else that might come up. So, open up your code editor of choice (mine’s vim
), and create a file named config.js
. I’ll start it off with the following bit of boilerplate:
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
require('dotenv').config({ path: `.env.${process.env.NODE_ENV}` });
const convict = require('convict');
const config = convict({
//Config goes in here, following this pattern
configVar: {
doc: "A string describing what this config var is for",
format: "Some Data type like String or Number or Boolean, or maybe an array of possible values",
default: "A default value",
env: "NAME_OF_ENV_VAR"
}
});
config.validate({allowed: 'strict'});
module.exports = config;
This gives me a few things that are extraordinarily helpful in terms of product development. First is a sane default for my NodeJS environment. As a developer, you can’t guarantee that someone who downloads your project is going to have environment variables set, properly. This may be someone’s first NodeJS project, and I want to make sure it has the best chance of running. Of course, instead of just having a default .env
file, I have files per environment, which allows me to have multiple .env.<environment>
files which allow me to test running my app in various environments to ensure that it behaves as expected (Looking at you, NextJS).
Here the power of the dotenv
package populates my process.env
object with the stored values from my environment-specific .env
files. And then using the convict
API, I have an object into which I can build any number of config options inside of. Please check out the docs for this library to understand how to populate the object passed to the convict()
function. Convict allows you to nest things, and it always returns an object syntax. (This is particularly handy for Knex config.)
Hand-wavey
I’m not going to go through actions like db migrations and GraphQL schema generation. Those are product-specific and not related to scaffolding up an initial app. You can assume that at some point, I did something like the following:
npx knex migrate:make create_users
And then I would open up the resulting migration file and scaffold out a users
table. From there, you can assume that I would define a type definition using gql to be able to return a User
type from my GraphQL API.
The server itself
Given the extensive list of packages listed above that I’m starting off with, it makes sense to take care with getting them set up. Fortunately, and this is one of the places that Fastify shines, the plugin registration API within Fastify is brilliant, and I give Matteo and team a hearty hats-off for coming up with this structure. Here is what a sample server.js
file would look like, integrating the above packages and the config set up that I documented above:
const fastify = require('fastify')({ logger: true })
const helmet = require('fastify-helmet');
const config = require('./config');
const gql = require('fastify-gql');
const schema = require('./data/schema');
const resolvers = require('./data/resolvers');
fastify.register(helmet)
.register(require('fastify-sensible'))
.register(require('fastify-healthcheck'))
.register(require('fastify-formbody'))
.register(require('fastify-knex'), {
client: 'pg',
debug: (process.env.NODE_ENV === 'development'),
connection: config.get('db')
})
.register(gql, { schema });
fastify.route({
method: 'POST',
url: '/gql',
handler: (request, reply) => {
const query = request.body.query;
return fastify.graphql(query);
}
});
const start = async () => {
try {
await fastify.listen(config.get('port'));
fastify.log.info(`GraphQL server listening on ${fastify.server.address().port}`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
The only thing that is above and beyond a pretty much vanilla set-up here is that I have included my GraphQL schema and resolvers from external files. I can move routes to external files, and still take advantage of the fact that Fastify routes support JSON Schema validation out of the box. You’ll notice that by setting up my config keys in my convict file to match the keys that need to be passed to the KnexJS config, I am able to just pass straight from config (config.get('db')
) to Knex with no intermediary step. And since my config is now environment-aware, this means that this code is now more flexible than it would be otherwise. I have also included an extra helper for myself, so that KnexJS debug logging is turned on if I am in my development environment. This means no having to pause execution and figure out what SQL query Knex is trying to run, I can have it logged out to me automatially, which is far nicer than having to go in and write console.log()
s and use the .toSQL()
or .toString()
methods that Knex surfaces to figure out what the query is. If I am working with a new front-end GraphQL query library, other than what I would normally be using, I will often add a similar kind of debug to the route handler for the GraphQL server itself, to output queries and query variables if I am in the development
environment.
From this stable point of scaffolding, I would follow on with feature development tasks and work on individual queries, mutations, and accompanying resolvers to build out the backend for my product.
Other libs I commonly use
There are a number of libraries that end up making up a significant portion of my development. Some of them are new to my toolkit, but others are stalwart friends that I can count on. Here they are, in no particular order:
- Moment - Specifically linked here is moment-timezone. I advocate using that over just plain Moment. Why? Read this.
- I haven’t had a chance to play with Luxon from the Moment team just yet. However, it looks promising. The immutability of it is enticing.
- ws - I will use this for most things websocket-related. I find SocketUI to be full-featured, but the equivalent of acquiring a 500-lb gorilla just to peel bananas.
- I use bcrypt a lot for hashing passwords. It’s (so far) secure, fast enough, and reliable.
- If I am not using Fastify, but need logging, I will use Pino, which is included in Fastify by default, since both are built by the same people. (Who I’ve met multiple times and are awesome folks!)