Best way to baseline nestjs microservice

Best way to baseline nestjs Microservice

There is no specific way or standard way of doing it, but we can maintain minimum standards of writing code or service like

  • Moduel structure
  • shared and feature Modules
  • proper test setup for e2e and unit tests
  • proper test environment
  • proper setup for docker for local development providing (dev and test DB both)
  • proper eslint setup with prettierc
  • commit lint setup for proper commit messages
  • docker and docker compose for spinning local environments
  • jest configurations for E2E and unit tests
  • compiler configuration for test and src application code
  • husky hook to enforce commitlint
  • CI pipeline configurations

Lets start a basic app using nestjs CLI

i hope you already know hoe to create basic nestjs app using CLI Nest.js Application Let’s continue with NestJS! We are going to install the NestJS CLI, so open the terminal of your choice and type:

$ npm i -g @nestjs/cli
$ nest new nest-env

We initialize a new NestJS project with its CLI. That might take up to a minute. The “-p npm” flag means, that we going to choose NPM as our package manager. If you want to choose another package manager, just get rid of this flag. After this command is done you can open your project in your code editor. Since I use Visual Studio Code, I gonna open the project by typing:

$ cd nest-env
$ code .

Now we want to have our file structure Look like this, its our final Goal, lets see how we can achieve this

snap

Now lets follow what all things we need to have this setup for our application

Step-1 commitlint and Husky Git Hooks

we need husky Hooks to enforce commit lint for our code with commitlint Lets first install https://github.com/conventional-changelog/commitlint module

What is commitlint

commitlint checks if your commit messages meet the conventional commit format.

In general the pattern mostly looks like this:

  • type(scope?): subject #scope is optional; multiple scopes are supported (current delimiter options: "/", "" and ",")

Real world examples can look like this:

  • chore: run tests on travis ci
  • fix(server): send cors headers
  • feat(blog): add comment section
  "devDependencies": {
    "@commitlint/cli": "15.0.0",
    "@commitlint/config-conventional": "15.0.0",
    "commitizen": "^4.2.4",
    "cz-conventional-changelog": "^3.3.0",

  },
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog"
  }

commitlint.config.js

module.exports = { extends: ["@commitlint/config-conventional"] };

With the help of Git Hooks, you can run scripts automatically every time a particular event occurs in a Git repository. With this amazing feature of Git and with the help of Husky, You can lint your commit messages, prettify the whole project’s code, run tests, lint code, and … when you commit. So here we can have a meeting between commitlint and husky git hooks, we want to run commit lint to check commit messages using hooks which Husly is managing so Husky and commitlint will work for us

example like a simple git Hook, it is using commitlint Module to tun this script

. "$(dirname "$0")/_/husky.sh"

if [ "$NO_VERIFY" ]; then exit 0; fi
exec < /dev/tty && node_modules/.bin/cz --hook || true
#!/bin/sh
npm install husky --save-dev

Enable Git hooks

#!/bin/sh
npx husky install

To automatically have Git hooks enabled after install, edit package.json npm set-script prepare "husky install" You should have:

{
  "scripts": {
    "prepare": "husky install"
  }
}
# commit-msg
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit $1
# pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint && npm run prettier
#  prepare-commit-msg
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

if [ "$NO_VERIFY" ]; then exit 0; fi
exec < /dev/tty && node_modules/.bin/cz --hook || true

From above configuration, make sure we have all these 3 script in .husky folder in the root Husky Git hooks are using commit lint to provide proper commit message syntex and also running lint with prettier command to clean the linting of the code, These all three Hooks works to place best standards for our code.

Step-2 Docker setup for local and test env

we need docker setup with docker-compsoe files, our container depends on what we are doing in service like

  • Node JS container
  • Postgres container
  • redis container
  • rabbit MQ container etc etc

docker-compose.yml

version: "3.6"
services:
  node:
    build: .
    volumes:
      - .:/app
      - ~/.npmrc/:/root/.npmrc
  postgres:
    image: postgres
    restart: unless-stopped  

docker file

FROM node:12-buster-slim

WORKDIR /app

COPY package.json package-lock.json /app/

RUN npm install && \
    rm -rf /tmp/* /var/tmp/*

COPY ./docker-utils/entrypoint/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh

COPY . /app

RUN npm run build

EXPOSE 3000

USER node

ENV TYPEORM_MIGRATION=ENABLE
ENV NPM_INSTALL=DISABLE
CMD npm run start:prod

docker-compose.override.yml

version: "3"
services:
   node:
     container_name: document_node
     command: npm run start
     environment:
       NPM_INSTALL: ENABLE
       TYPEORM_MIGRATION: ENABLE
     ports:
       - 3000:3000
  postgres:
    environment:
      - POSTGRES_USER=api
      - POSTGRES_PASSWORD=development_pass
      - POSTGRES_MULTIPLE_DATABASES="example-api","example-api-testing"
    volumes:
      - ./docker-utils:/docker-entrypoint-initdb.d
      - api_data:/data/postgres
    ports:
      - 5434:5432
volumes:
  api_data: {}

With all this we also wants to bootstrap postgres container with init database so we don't have to create manually

    environment:
      - POSTGRES_USER=api
      - POSTGRES_PASSWORD=development_pass
      - POSTGRES_MULTIPLE_DATABASES="example-api","example-api-testing"
    volumes:
      - ./docker-utils:/docker-entrypoint-initdb.d

This we can do with script and same script we can mount to volume as init script we can craete script inside docker-utils folder

#!/bin/bash
set -e
set -u

function create_user_and_database() {
	local database=$1
	echo "  Creating user and database '$database'"
	psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
	    CREATE USER $database;
	    CREATE DATABASE $database;
	    GRANT ALL PRIVILEGES ON DATABASE $database TO $database;
EOSQL
}

if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then
	echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES"
	for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do
		create_user_and_database $db
	done
	echo "Multiple databases created"
fi

Step-3 setting up eslint and prettier

Prettier can be run as a plugin for ESLint, which allows you to lint and format your code with a single command. Anything you can do to simplify your dev process is a win in my book. Prettier + ESLint is a match made in developer heaven. If you’ve ever tried to run Prettier and ESLint together, you may have struggled with conflicting rules. Don’t worry! You’re not on your own. Plug in eslint-config-prettier, and all ESLint’s conflicting style rules will be disabled for you automatically.

    "@typescript-eslint/eslint-plugin": "4.33.0",
    "@typescript-eslint/parser": "4.33.0",
    "eslint": "7.32.0",
    "eslint-config-prettier": "8.3.0",
    "eslint-plugin-prettier": "4.0.0",
    "eslint-plugin-unused-imports": "2.0.0",

.eslintrc

module.exports = {
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 12,
    "sourceType": "module"
  },
  "plugins": [
    "@typescript-eslint"
  ],
  "rules": {
    // strict https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md
    // strict which we may not be able to adopt
    "@typescript-eslint/explicit-module-boundary-types": 0,
    // allow common js imports as some deps are still with common-js 
    "@typescript-eslint/no-var-requires": 0,
    "no-useless-escape": 0,
    // this is for regex, it will throw for regex characters
    "no-useless-catch": 0,
    // this i have to add as we mostly use rollbar to catch error 
    "@typescript-eslint/no-explicit-any": 0,
    // this is needed as we assign things from process.env which may be null | undefined | string 
    // and we have explicitly this.configService.get().azure.fileUpload.containerName!
    "@typescript-eslint/no-non-null-assertion": 0,
    "no-async-promise-executor": 0
  }
};

prettierc

{
  "bracketSpacing": true,
  "printWidth": 80,
  "proseWrap": "preserve",
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all",
  "tabWidth": 4,
  "useTabs": true,
  "parser": "typescript",
  "arrowParens": "always",
  "requirePragma": true,
  "insertPragma": true,
  "endOfLine": "lf",
  "overrides": [
    {
      "files": "*.json",
      "options": {
        "singleQuote": false
      }
    },
    {
      "files": ".*rc",
      "options": {
        "singleQuote": false,
        "parser": "json"
      }
    }
  ]
}

And we can add required scripts to have linters enabled

    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix --quiet",
    "prettier": "./node_modules/.bin/prettier --check \"**/*.{js,json,ts,yml,yaml}\"",
    "prettier:write": "./node_modules/.bin/prettier --write \"**/*.{js,json,ts,yml,yaml}\"",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",

Step-4 Build Configurations using tsconfigs

The presence of a tsconfig.json file in a directory indicates that the directory is the root of a TypeScript project. The tsconfig.json file specifies the root files and the compiler options required to compile the project.

JavaScript projects can use a jsconfig.json file instead, which acts almost the same but has some JavaScript-related compiler flags enabled by default. Its good to have one global tsconfig and rest we can also create tsocnfig for build and test like tsconfig.build.json and using this config for build

{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es2017",
    "allowJs": true,
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true,
    "strict": true,
    "skipLibCheck": true,
    "paths": {
      "@app/*": ["src/app/*"],
      "@auth/*": ["src/app/auth/*"]
    }
  },
}

tsocnfig Build configuration, we can override anything we want, we are extending base config and changing things if needed

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "rootDir": "./",
    "declaration": false,
    "removeComments": true,
    "sourceMap": false,
    "incremental": false
  },
  "exclude": ["node_modules", "coverage", "test", "build","dist", "**/*spec.ts", "**/*mocks.ts"]
}

In most of the Projects we are using jest now In test Folder we can have setEnvVars which will populate test config in process.env

const dotenv = require('dotenv');
dotenv.config({ path: './env.test' });
module.exports = {
  setupFiles: ['<rootDir>/test/setEnvVars.js'],
  silent: false,
  moduleFileExtensions: ['js', 'ts'],
  rootDir: '.',
  testRegex: '[.](spec|test).ts$',
  transform: {
    '^.+\\.(t|j)s$': 'ts-jest',
  },
  coverageDirectory: './coverage',
  testEnvironment: 'node',
  roots: ['<rootDir>/'],
  moduleNameMapper: {
    "^@app(.*)$": "<rootDir>/src/app/$1",
    "^@auth(.*)$": "<rootDir>/src/app/auth/$1"
  },
};

Rest all the configurations are project related like ormconfig.ts or knex.ts

  • ormconfig.ts and knex.ts ORM related config
  • nodemon.json or nest-cli.json nestjs cli configuration
  • env and env.test files for local and test env
  • CI configuration files
  • Any other APM related file like newrelic.js
  • deployment related file like procfile for Heroku

Conclusion

As we are building and working in everyday changing environemnt, its better to have a base template which we can keep evolving day by day and easy to start for any new project from ground Zero, I hope above example can help you to build your template for different services

Comments