Best practices of app design to be run in cloud or via some orchestration services such as k8s or ECS are wonderfully described in a methodology called 12 factor app.

The official manifest is pretty abstract and here is a projection of each factor implementation in node.js actual for 2020.

1. Codebase

Simply store your app code in git, using some remote git origin like github, gitlab or bitbucket.

2. Dependencies

Store all your dependencies in package.json, if there is any external non standard apps (like curl or net-stat) you call using child_process then you should include it into same docker container your app is packed.

3. Config

The configurable values should be read from environment. To access env variables use process.env object. There is a nice package called dotenv to fallback the env variables to the reads from local file(s).

4. Backing services

As the backing services I consider the services accessed via network like databases, 3rd party APIs, data format converters, etc. So, the url of access to the backing service should be stored in the config, so it can be swapped. Hot swap (at runtime) is not required, it's OK to change env config and restart app to apply changes.

This is super convenient for integration tests. Usually simple backing services docker containers are run locally via docker-compose. The docker-compose.yml with test backing services usually is stored in a root folder of a project.

5. build, release, run separation

This point was more for devops but we can make our app supper friendly to support this three step of app delivery. Just create these three scripts in your package.json.  If your app uses typescript, or newsest version of esNext - not yet supported by node or simply is bundled by webpack or gulp - it's good practice to store the trigger command in build script. To release the package you may use some packages like bumped, lerna or semver. It's good to put their run scripts into release.

For example:

{
	...
	"scripts": {
		"build": "tsc -p .",
		"release:minor": "bumpred release minor",
		"release:patch": "bumpredrelease patch",
		"run": "node ./build/index.js"
	}
	...
}

6. Process is stateless

At each entry point of the app, it expect that there is no data is stored in runtime memory or local filesystem.

7. Port binding

The app should not rely on the external server like php relies on apache, the app should export the port it's listening to.

The default http.Server module of nodejs and express lib already handles the port binding and server.listen or express.app.listen satisfy this concept.

8. Concurrency

Should scale horizontally and never daemonise (run in the background). It means that adding new app instances will do the the same job and not block each other nor create conflicts. Inside the container the app should not run as a daemon or in a background.

9. Disposability

Fast startup and graceful shutdown on sigterm or sigint.

Practically this mean to stop listening to server port and finish all current requests. Or if it's worker, cancel job and return it to the queue. The jobs should be reentrant and idempotent.

process.on("SIGINT", () => {
	// graceful shutdown code goes here
});

process.on("SIGTERM", () => {
	// graceful shutdown code goes here
});

Crash-only software is software that crashes safely and recovers quickly. The only way to stop it is to crash it, and the only way to start it is to recover. Crash only design is roboust against sudden death.

10. Dev/Prod parity

On local development machine eager to use the same backing services as in production environment, if it's possible. For example typeORM has sqlite as default testing mock and we used postgress in production. And timestamp in postgress and sqlite is different (sqlite ignores miliseconds), and unit tests that passed locally failed on a ci system.

11. Logs as event streams

Logs should be written to stdout and than handled by other tools.

I've an example of setup of winston to send to the logs to logz.io - and it does not satisfy the 12fa approach. Better to send all logs to console (which is pushing everything to stdout in node by default) and then they will be picked up by some management tools.

12. Admin processes

If there is some admin code, like migration of database or creating some config or status files, you should put into an npm script into a package.json. Then run new instance of the app or select the existing one, ssh into it and run this npm script.