Skip to content
Snippets Groups Projects

node-koa-starter

A full-fat, go get 'em starter that I use to build enterprise web applications.

Contents

Rationale

I build a lot of large web applications; sometimes they're monoliths, sometimes they're a series of microservices. This starter represents the collected wisdom that I've gained over the years on how to structure a Node.js web app so that it's usable both on the micro and macro scales without too much churn.

The included libraries have been chosen for their utility in any situation; there isn't any point in including an email facade when not every project would need to send emails (microservices), or including a JSON:API response transformer (not every project would even speak REST!).

Express is an outdated framework. It has a lot of good things going for it, and the community support is astounding, but koa is a lot tighter and provides a nicer base on which to build bespoke functionality.

The one controversial point that I will concede is the usage of NODE_PATH to get absolute module imports. This may not be to everyone's taste, but forms a core basis of some of the dependency injection techniques used.

Included Batteries

  • koa - The HTTP request handling framework that forms the core of this starter. Modern, slim, BYOB attitude with enough community modules to cover the bases.
    • koa-static - Not mounted be default, but included because many projects will need to serve static assets at some point
    • koa-router - The defacto router for Koa, supports middleware, sub-routers and name based url-generation
    • koa-bodyparser - Parses request bodies for forms and json uploads
    • koa-logger - Basic request logging & timing
    • koa-cors - Handle cors OPTIONS requests, very customisable
  • dotenv - Environment file based configuration, feeds into the env/config multi-layered app configuration strategy
  • moment - Almost all apps handle dates or times (even for logging purposes)
    • moment-range - Configured by default to allow seamless date-range manipulation
  • fs-jetpack - Easier file system handling. A global instance is added to the bootstrap to provide a default fs rooted to the app directory
  • uuid - Standard for adding IDs to things
  • lodash - Utilities for most apps
  • core/errors - HTTP errors that, when not caught, will set the response status and contents appropriately

Docker

The included Dockerfile works by default for this setup, but will need tweaking as new resources are added to the project. It utilises multi-stage builds to build the app in an alpine container, and then copies the resources to a slim container for a smaller output size. Additional COPY statements and ENV configs will need to be added as required.

Code Conventions

Application entry points are located at the root level of the project. For example, server.js is a server entry point. When building additional components that make use of a shared code base (e.g. a queue worker), they would be located at the root level as well (e.g. a worker.js file).

All external configuration should be stored in a file located under src/config. These files should either be a JSON file or a JS file that exports an object. Using the config helper from the bootstrap will access values from these files, utilising the node module cache to prevent multiple potentially expensive lookups (e.g. syscalls to the environment). If the config object has a driver key, the config helper will instead use the property with a name that matches the value of driver when retrieving values.

e.g.

const { env } = require('bootstrap')
module.exports = {
    driver: env('DATABASE_DRIVER', 'postgres'),
    postgres: {
    	// ... postgres config ...
    },
    mysql: {
        // ... mysql config ...
    },
}

External services fall in to two categories; configured SDKs and service facades. Configured SDKs should export a configured version of their parent-library, or an SDK factory, from src/vendor/{library-name} such that another module can require('vendor/mylib') to use that library. An example of this would be using pg; src/vendor/pg would export a configured pg instance, typically using config or env from the bootstrap to pull in the configuration.

Service facades are detailed more below, but essentially take the form of a configured mapping of a specific vendor service to a generic service interface. For example, the cache service included with this starter has the null and memory implementations, which are switched between by the CACHE_DRIVER environment variable. A redis implementation could be added by mapping an ioredis configured SDK (detailed above) to a redis service that implements the cache facade.

Dependency injection is detailed more below, but functions by implementing the ContextualModule class and then adding that class to the ServiceProvider module. Each ContextualModule declares it's exported name, and will be made available to any entity that has access to the koa ctx object.

Dependency Injection

  • TBW

Service Facades

  • TBW

Spaces Vs Tabs

The project is configured to expect tab-based indentation for the following reasons:

  • Accessibility: I am dyslexic and I can configure the width of a tab to render as a comfortable amount of space to make understanding code easier
  • Consistency: Every line has X number of tabs based on bracket nesting. There are no spaces for further alignment and the linting rules prevent mixed indentation so it's not possible to succumb to the temptation of vanity indentation
  • Preference: I simply prefer tabs. There are arguments for either, and I don't care too deeply about it. Tabs give me the things I need, so I use them.