Skip to content

Latest commit

 

History

History
224 lines (168 loc) · 11.3 KB

node-template.md

File metadata and controls

224 lines (168 loc) · 11.3 KB

PNK Stack Service

Node Template Logo Header Image

PostgreSQL, Node, and Koa

This acts as a guide for creating a relatively production ready backend micro-service, which can be customized to your needs. It already has error handling, logging, observability, rate limiting, graceful shutdown, a test file, and Docker optimizations.

The reference template repository can be found at irmerk/pnk-stack

Stack

Setup

All of this page assumes that you are using a Mac and that your software is up to date. If not up to date you may run into install issues when you install the packages below.

Shell

macOS now ships with Zsh as the default shell. If you want more customization and handy shortcuts, you can install oh my zsh.

Terminal Emulator

You can use the Terminal app that ships with macOS, but iTerm2 is a popular and recommended alternative with more features and following. For information regarding prompt customization, see Zsh Prompt.

A helpful setting can also allow you to use Alt/Cmd + Right/Left Arrow in iTerm.

Text Editor / IDE

Use whatever text editor / integrated development environment (IDE) you prefer. At the time of this writing, Visual Studio Code is an extremely popular one with a built-in plugin manager for easily extending and customizing functionality.

For helpful VSCode keyboard shortcuts to incorporate into your workflow, see their Tips and Tricks.

My suggestions for VSCode extensions and keyboard shortcuts.

Command Line Tools

Install the Homebrew package manager for easy and relatively safe installation and upgrades.

Recommended tools:

Manages installations of Node.js and npm:

brew install nvm \
&& mkdir ~/.nvm \
&& echo 'export NVM_DIR="$HOME/.nvm"' >> ~/.zprofile \
&& echo '[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh" # This loads nvm' >> ~/.zprofile \
&& source ~/.zprofile

Node.js and npm could be installed directly via Homebrew as well, but doing so makes it challenging to switch between versions. Some projects each require specific versions of Node.js and npm versions to work properly, so nvm installations are strongly preferred.

nvm install node
brew install git

Set your name and email address explicitly:

git config --global user.name "<YOUR_NAME>"
git config --global user.email <YOUR_EMAIL>

Rationale

Makefile

  • I chose Koa because it is newer, less opinionated, lighter weight, and easier to customize than the standard of Express. Koa is modular and customizable because it requires modules for routing, the templating engine, and JSONP. This way, I can create middleware ourselves or use the built-in middleware.
  • Koa focuses more on modern features of the JavaScript ES6 language, like generators, async functions, and the Node.js runtime. Moreover, it uses promise-based flows and async-await syntax to get rid of callbacks, making the code more manageable, cleaner, and more readable. Because it uses generator syntax instead of callbacks, I can use the yield keyword to exit and then reenter.
  • Koa uses a context object, which encapsulates req/res objects into one, helping us develop more efficiently by using several helpful methods. Finally, Koa uses cascading middleware. Thanks to asynchronous functions, middleware will run in a cascading fashion until the last middleware is reached.
  • HTTP Client Library for Node.js
  • Comparison to other libraries
  • I chose axios instead of got because “[the got] package is native ESM and no longer provides a CommonJS export. If your project uses CommonJS, you will have to convert to ESM or use the dynamic import() function. Please don't open issues for questions regarding CommonJS / ESM.”
  • GOT has issues with linting, TypeScript, ESM/CJS, and JSON.

Logging

The logging transport is configured for logging to the console. This happening in a service set up in a cloud service can be configured to be picked up from STDOUT

Linting

Why I add a new line to the end of a file.

GitHub Actions

npx: When I run a command with npx, it will check whether the command exists in the local node_modules/.bin directory or in the system-wide installed packages. If it doesn't find the command, it will attempt to temporary install the package and execute it. This makes npx an ideal tool to run the locally installed service being called.

  • Dockerfile
    • Dockerizing a service allows for it to behave the same regardless of the platform on which it is run. A Dockerfile is a blueprint on which the Docker image is built. When the built image is running, it is called a container. Essentially, the container starts as a Dockerfile that has the instructions on how to build the Docker image. The same image can be used to spin up one or even hundreds of containers, which is why Docker is so useful for software scalability.
    • I am using a slim production stage and a more feature-rich, development-focused dev stage.
    • Setting NODE_ENV to production can increase performance by 3x.
    • Notice some of our commands are put together with an &&, creating fewer Docker layers, which is good for build caching.
    • Debian-based images because it may be better in some cases than the more standard alpine linux image.
  • docker-compose.yml
    • Docker Compose is a way to more easily setup multiple services running with Docker in an ecosystem. The big advantage of using Compose is you can define your application stack in a file, keep it at the root of your project repo (it’s now version controlled).
  • .dockerignore
    • Just like I wouldn’t use Git without .gitignore, I use a .dockerignore file to ignore files that I don’t want to land in our Docker image. It helps to keep the Docker image small and keep the build cache more efficient by ignoring irrelevant file changes.
  • dumb-init
    • I use dumb-init because it will be the first process (PID 1) and will handle the signal forwarding and the reaping of the zombie processes. It is a simple process supervisor and init system that helps to handle these situations.
    • Docker containers usually run a single application, and for that application, it is normal to expect it to receive signals from the Linux environment. However, when running in the background (as a daemon), the application may not correctly handle the Linux signals, which can lead to zombie processes and not graceful termination.
  • Multiple Stages
    • Optimizing layers with multiple stages (build and production) in the Dockerfile results in a drastically smaller image size:
REPOSITORY      TAG     IMAGE ID        CREATED             SIZE
single-stage    latest  8ceb85286547    4 seconds ago       402MB
multi-stage     <none>  ec0680eefe42    About a minute ago  300MB

Reference