Node-Typescript-Setup

April 29, 2021

This is my prefered setup when I start a new Node project, especially when it covers TypeScript on it’s stack. The setup covers code-convention checks as well as docker builds and a deployment pipeline.

Step 1: Git

initialise repository: git init create .gitignore and import content from node-typescript-starter

Step 2: Node workspace and dependencies

initialise Node project: npm init -y install following dependencies:

npm install prettier --save-dev
npm install typescript --save-dev
npm install eslint --save-dev
npm install @typescript-eslint/eslint-plugin --save-dev
npm install @typescript-eslint/parser --save-dev

Step 3: Configure Prettier

Create .prettierrc Use the following configuration:

{
    "singleQuote": true,
    "semi": true,
    "trailingComma": "none",
    "bracketSpacing": true,
    "arrowParens": "always",
    "printWidth": 90
}

Step 4: Configure EsLint

Create .eslintrc.json Use the following eslint-configuration

{
    "env": {
        "browser": true,
        "es2021": true
    },
    "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaVersion": 12,
        "sourceType": "module",
        "project": "./tsconfig.json"
    },
    "plugins": ["@typescript-eslint"],
    "rules": {
        "quotes": ["error", "double"],
        "sort-imports": [
            "error",
            {
                "ignoreCase": false,
                "ignoreDeclarationSort": false,
                "ignoreMemberSort": false,
                "memberSyntaxSortOrder": ["none", "all", "multiple", "single"],
                "allowSeparatedGroups": false
            }
        ],
        "comma-dangle": ["error", "never"],
        "semi": ["error", "always"],
        "@typescript-eslint/array-type": 2,
        "@typescript-eslint/member-ordering": 2,
        "@typescript-eslint/no-unnecessary-boolean-literal-compare": 2,
        "@typescript-eslint/no-unnecessary-condition": 2,
        "@typescript-eslint/prefer-for-of": 2,
        "@typescript-eslint/require-array-sort-compare": 2,
        "@typescript-eslint/explicit-function-return-type": "off",
        "@typescript-eslint/no-explicit-any": 1,
        "@typescript-eslint/no-inferrable-types": [
            "warn", {
                "ignoreParameters": true
            }
        ],
        "@typescript-eslint/no-unused-vars": "warn"
        "@typescript-eslint/naming-convention": [
        "error",
        {
            "selector": "typeLike",
            "format": ["PascalCase"]
        },
        {
            "selector": "variable",
            "types": ["boolean"],
            "format": ["camelCase"],
            "prefix": ["is"]
        }
        ]
    }
}

Step 5: Configure TypeScript

Create initial tsconfig by running tsc --init. Doublecheck the content against the following.

{
    "compilerOptions": {
        "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
        "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
        "lib": ["dom", "es6"] /* Specify library files to be included in the compilation. */,
        "outDir": "dist" /* Redirect output structure to the directory. */,
        "rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
        "removeComments": true /* Do not emit comments to output. */,

        "strict": true /* Enable all strict type-checking options. */,
        "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
        "strictNullChecks": true /* Enable strict null checks. */,
        "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
        "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,

        "noUnusedLocals": true /* Report errors on unused locals. */,
        "noUnusedParameters": true /* Report errors on unused parameters. */,
        "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
        "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,

        "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
        "allowSyntheticDefaultImports": true,
        "skipLibCheck": true /* Skip type checking of declaration files. */,
        "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
    },
    "compileOnSave": true
}

Step 6: Setup lint-staged

First install lint-staged via npx mrm lint-staged. Adjust package.json to use lint-staged as intended. The following snippet is an excerpt of an example package.json.

{
    ...
    "scripts": {
        "prettify": "prettier --write",
        "lint": "eslint --cache --fix"
    },
    ...
    "husky": {
        "hooks": {
            "pre-commit": "lint-staged"
        }
    },
    "lint-staged": {
        "*.ts": [
            "npm run prettify",
            "npm run lint"
        ]
    }
}

Step 7: Setup Docker

To build your application with Docker create a Dockerfile at the root and implement the wanted behaviour. This could look like the following example.

FROM node:latest

ENV API_KEY=

WORKDIR /usr/<project>
COPY package.json /usr/<project>/

RUN npm install
COPY ./ /usr/<project>

RUN npm run build
COPY ./ /usr/<project>

CMD ["sh", "-c", "NODE_ENV=production node dist/index.js $API_KEY"]

Step 8: Setup Gitlab CI

Create .gitlab-ci.yml and use the outlined content to run a 5-stage deployment pipeline on every MergeRequest or commit to Master

include:
  - template: 'Workflows/MergeRequest-Pipelines.gitlab-ci.yml'
  
stages:
  - prettier
  - lint
  - typecheck
  - test
  - deploy

prettier: 
  stage: prettier
  image: node:latest
  script:
    - 'npm ci'
    - 'npm run prettiercheck'

lint:
  stage: lint
  image: node:latest
  script: 
    - 'npm ci'
    - 'npm run lint:all'

typecheck:
  stage: typecheck
  image: node:latest
  script:
    - 'npm ci'
    - 'npm run typecheck'

test:
  stage: test
  image: node:latest
  script:
    - 'npm ci'
    - 'npm run test'

deploy:
  stage: deploy
  before_script:
    - 'docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY'
  image: 'docker:latest'
  script:
    - 'docker image build --tag registry.gitlab.com/<username>/<project>:latest .'
    - 'docker push registry.gitlab.com/<username>/<project>:latest'
  services:
    - 'docker:dind'