Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# node-koa-starter
A full-fat, go get 'em starter that I use to build enterprise web applications.
## Contents
* [Rationale](#rationale)
* [Included Batteries](#included-batteries)
* [Docker](#docker)
* [Code Conventions](#code-conventions)
* [Dependency Injection](#dependency-injection)
* [Service Facades](#service-facades)
* [Spaces vs Tabs](#spaces-vs-tabs)
## 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.
```javascript
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.