├── .codeclimate.yml ├── .dockerignore ├── .env ├── .eslintrc.js ├── .github ├── CODEOWNERS └── workflows │ ├── build-api.yml │ ├── build-micro.yml │ ├── ci.yml │ ├── deploy-api.yml │ ├── deploy-micro.yml │ ├── deploy-nats.yml │ ├── rollback-api.yml │ ├── rollback-micro.yml │ └── rollback-nats.yml ├── .gitignore ├── .prettierrc ├── Dockerfile ├── Makefile ├── README.md ├── apps ├── api │ ├── src │ │ ├── app.module.ts │ │ ├── hello │ │ │ ├── application │ │ │ │ ├── hello.controller.spec.ts │ │ │ │ ├── hello.controller.ts │ │ │ │ ├── hello.resolver.ts │ │ │ │ └── hello.service.ts │ │ │ ├── domain │ │ │ │ ├── hello.input.ts │ │ │ │ ├── hello.object.ts │ │ │ │ ├── hello.objectify.ts │ │ │ │ └── hello.ts │ │ │ ├── hello.module.ts │ │ │ ├── index.ts │ │ │ └── infrastructure │ │ │ │ └── hello.pub-sub.ts │ │ ├── main.ts │ │ └── playground │ │ │ ├── hello.ts │ │ │ ├── tab.ts │ │ │ └── tabs.ts │ ├── test │ │ ├── app.e2e-spec.ts │ │ └── jest-e2e.json │ └── tsconfig.app.json └── micro │ ├── src │ ├── application │ │ ├── micro.controller.spec.ts │ │ ├── micro.controller.ts │ │ └── micro.service.ts │ ├── main.ts │ └── micro.module.ts │ └── tsconfig.app.json ├── build-deploy ├── ci.sh ├── cluster │ └── skeleton.yaml ├── helm │ ├── api │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── templates │ │ │ └── api.yaml │ │ └── values.yaml │ ├── micro │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── templates │ │ │ └── micro.yaml │ │ └── values.yaml │ ├── nats │ │ ├── .helmignore │ │ ├── Chart.lock │ │ ├── Chart.yaml │ │ ├── charts │ │ │ └── nats-0.7.5.tgz │ │ └── values.yaml │ └── volumes │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── templates │ │ └── storage-class.yaml │ │ └── values.yaml ├── main.sh └── wait-for-it.sh ├── docker-compose.yml ├── libs └── core │ ├── src │ ├── core.module.ts │ ├── domain │ │ ├── logger.ts │ │ └── micro-service.ts │ ├── index.ts │ └── infrastructure │ │ ├── nats.pub-sub.ts │ │ └── winston.logger.ts │ └── tsconfig.lib.json ├── nest-cli.json ├── package-lock.json ├── package.json ├── tsconfig.build.json ├── tsconfig.json └── webpack.config.js /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | checks: 3 | argument-count: 4 | config: 5 | threshold: 4 6 | complex-logic: 7 | config: 8 | threshold: 4 9 | file-lines: 10 | config: 11 | threshold: 250 12 | method-complexity: 13 | config: 14 | threshold: 5 15 | method-count: 16 | config: 17 | threshold: 20 18 | method-lines: 19 | config: 20 | threshold: 25 21 | nested-control-flow: 22 | config: 23 | threshold: 4 24 | return-statements: 25 | config: 26 | threshold: 4 27 | similar-code: 28 | config: 29 | threshold: 30 | identical-code: 31 | config: 32 | threshold: 33 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | *Dockerfile* 3 | *docker-compose* 4 | node_modules 5 | dist 6 | .nyc_output 7 | coverage 8 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | NATS_URL=nats://nats:4222 3 | GRAPHQL_DEBUG=1 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier/@typescript-eslint', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @greg-md 2 | -------------------------------------------------------------------------------- /.github/workflows/build-api.yml: -------------------------------------------------------------------------------- 1 | name: Build Api 2 | on: workflow_dispatch 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v2 10 | 11 | - name: Configure AWS credentials 12 | uses: aws-actions/configure-aws-credentials@v1 13 | with: 14 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 15 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 16 | aws-region: eu-central-1 17 | 18 | - name: Login to Amazon ECR 19 | id: login-ecr 20 | uses: aws-actions/amazon-ecr-login@v1 21 | 22 | - name: Build, tag, and push images to Amazon ECR 23 | env: 24 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 25 | ECR_REPOSITORY: skeleton/api 26 | IMAGE_TAG: ${{ github.run_number }} 27 | run: | 28 | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -t $ECR_REGISTRY/$ECR_REPOSITORY:latest --target production-api . 29 | docker push --all-tags $ECR_REGISTRY/$ECR_REPOSITORY 30 | -------------------------------------------------------------------------------- /.github/workflows/build-micro.yml: -------------------------------------------------------------------------------- 1 | name: Build Micro 2 | on: workflow_dispatch 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v2 10 | 11 | - name: Configure AWS credentials 12 | uses: aws-actions/configure-aws-credentials@v1 13 | with: 14 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 15 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 16 | aws-region: eu-central-1 17 | 18 | - name: Login to Amazon ECR 19 | id: login-ecr 20 | uses: aws-actions/amazon-ecr-login@v1 21 | 22 | - name: Build, tag, and push images to Amazon ECR 23 | env: 24 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 25 | ECR_REPOSITORY: skeleton/micro 26 | IMAGE_TAG: ${{ github.run_number }} 27 | run: | 28 | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -t $ECR_REGISTRY/$ECR_REPOSITORY:latest --target production-micro . 29 | docker push --all-tags $ECR_REGISTRY/$ECR_REPOSITORY 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | linter: 5 | name: Linter 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - name: Install Dependencies 10 | run: npm ci 11 | - name: Run 12 | run: npm run lint 13 | tests: 14 | name: Tests 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - run: docker-compose pull 19 | # It takes more time to cache/restore data on small builds 20 | # - uses: satackey/action-docker-layer-caching@v0.0.11 21 | # continue-on-error: true 22 | - run: docker-compose build 23 | - run: NODE_ENV=ci docker-compose up --abort-on-container-exit --exit-code-from ci 24 | - uses: paambaati/codeclimate-action@v2.7.5 25 | env: 26 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 27 | with: 28 | coverageLocations: ${{github.workspace}}/coverage/lcov.info:lcov 29 | - run: docker-compose down -v 30 | -------------------------------------------------------------------------------- /.github/workflows/deploy-api.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Api 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | build: 6 | description: 'Build number' 7 | required: true 8 | default: 'latest' 9 | jobs: 10 | deploy: 11 | name: Deploy 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Configure AWS credentials 18 | uses: aws-actions/configure-aws-credentials@v1 19 | with: 20 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 21 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 22 | aws-region: eu-central-1 23 | 24 | - name: Login to Amazon ECR 25 | id: login-ecr 26 | uses: aws-actions/amazon-ecr-login@v1 27 | 28 | - name: Helm deploy to AWS EKS 29 | uses: koslib/helm-eks-action@master 30 | env: 31 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 32 | ECR_REPOSITORY: skeleton/api 33 | IMAGE_TAG: ${{ github.event.inputs.build }} 34 | KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }} 35 | with: 36 | command: helm upgrade api ./build-deploy/helm/api --install --wait --set image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG --set pullPolicy=Always 37 | -------------------------------------------------------------------------------- /.github/workflows/deploy-micro.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Micro 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | build: 6 | description: 'Build number' 7 | required: true 8 | default: 'latest' 9 | jobs: 10 | deploy: 11 | name: Deploy 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Configure AWS credentials 18 | uses: aws-actions/configure-aws-credentials@v1 19 | with: 20 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 21 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 22 | aws-region: eu-central-1 23 | 24 | - name: Login to Amazon ECR 25 | id: login-ecr 26 | uses: aws-actions/amazon-ecr-login@v1 27 | 28 | - name: Helm deploy to AWS EKS 29 | uses: koslib/helm-eks-action@master 30 | env: 31 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 32 | ECR_REPOSITORY: skeleton/micro 33 | IMAGE_TAG: ${{ github.event.inputs.build }} 34 | KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }} 35 | with: 36 | command: helm upgrade micro ./build-deploy/helm/micro --install --wait --set image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG --set pullPolicy=Always 37 | -------------------------------------------------------------------------------- /.github/workflows/deploy-nats.yml: -------------------------------------------------------------------------------- 1 | name: Deploy NATS 2 | on: workflow_dispatch 3 | jobs: 4 | deploy: 5 | name: Deploy 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v2 10 | 11 | - name: Configure AWS credentials 12 | uses: aws-actions/configure-aws-credentials@v1 13 | with: 14 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 15 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 16 | aws-region: eu-central-1 17 | 18 | - name: Helm deploy to AWS EKS 19 | uses: koslib/helm-eks-action@master 20 | env: 21 | KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }} 22 | with: 23 | command: helm upgrade nats ./build-deploy/helm/nats --install --wait 24 | -------------------------------------------------------------------------------- /.github/workflows/rollback-api.yml: -------------------------------------------------------------------------------- 1 | name: Rollback Api 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | revision: 6 | description: 'Revision number' 7 | jobs: 8 | rollback: 9 | name: Rollback 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | - name: Configure AWS credentials 16 | uses: aws-actions/configure-aws-credentials@v1 17 | with: 18 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 19 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 20 | aws-region: eu-central-1 21 | 22 | - name: Helm rollback in AWS EKS 23 | uses: koslib/helm-eks-action@master 24 | env: 25 | REVISION: ${{ github.event.inputs.revision }} 26 | KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }} 27 | with: 28 | command: helm rollback api $REVISION --wait 29 | -------------------------------------------------------------------------------- /.github/workflows/rollback-micro.yml: -------------------------------------------------------------------------------- 1 | name: Rollback Micro 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | revision: 6 | description: 'Revision number' 7 | jobs: 8 | rollback: 9 | name: Rollback 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | - name: Configure AWS credentials 16 | uses: aws-actions/configure-aws-credentials@v1 17 | with: 18 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 19 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 20 | aws-region: eu-central-1 21 | 22 | - name: Helm rollback in AWS EKS 23 | uses: koslib/helm-eks-action@master 24 | env: 25 | REVISION: ${{ github.event.inputs.revision }} 26 | KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }} 27 | with: 28 | command: helm rollback micro $REVISION --wait 29 | -------------------------------------------------------------------------------- /.github/workflows/rollback-nats.yml: -------------------------------------------------------------------------------- 1 | name: Rollback NATS 2 | on: workflow_dispatch 3 | jobs: 4 | rollback: 5 | name: Rollback 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v2 10 | 11 | - name: Configure AWS credentials 12 | uses: aws-actions/configure-aws-credentials@v1 13 | with: 14 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 15 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 16 | aws-region: eu-central-1 17 | 18 | - name: Helm rollback in AWS EKS 19 | uses: koslib/helm-eks-action@master 20 | env: 21 | REVISION: ${{ github.event.inputs.revision }} 22 | KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }} 23 | with: 24 | command: helm rollback nats $REVISION --wait 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | # GraphQL 37 | schema.gql 38 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 AS development 2 | 3 | # RUN apt-get update 4 | 5 | RUN npm i -g npm@latest 6 | 7 | RUN mkdir /var/log/skeleton 8 | 9 | WORKDIR /node 10 | COPY package*.json ./ 11 | RUN npm ci && npm cache clean --force 12 | ENV PATH /node/node_modules/.bin:$PATH 13 | 14 | COPY . . 15 | 16 | FROM development AS production-builder-api 17 | 18 | RUN npm run build api 19 | 20 | FROM development AS production-builder-micro 21 | 22 | RUN npm run build micro 23 | 24 | FROM node:14-alpine AS production 25 | 26 | # RUN apk update && apk add --no-cache bash 27 | 28 | ARG NODE_ENV=production 29 | ENV NODE_ENV $NODE_ENV 30 | 31 | WORKDIR /node 32 | COPY package*.json ./ 33 | RUN npm ci --production && npm cache clean --force 34 | 35 | CMD ["node", "main"] 36 | 37 | FROM production AS production-api 38 | 39 | WORKDIR /node/api 40 | COPY --from=production-builder-api /node/dist/apps/api . 41 | RUN chown -R node:node /node/api 42 | USER node 43 | 44 | FROM production AS production-micro 45 | 46 | WORKDIR /node/micro 47 | COPY --from=production-builder-micro /node/dist/apps/micro . 48 | RUN chown -R node:node /node/micro 49 | USER node 50 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: ## Help dialog. 2 | @IFS=$$'\n' ; \ 3 | help_lines=(`fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##/:/'`); \ 4 | printf "\nUsage: " ; \ 5 | printf "\033[36m" ; \ 6 | printf "make " ; \ 7 | printf "\033[0m" ; \ 8 | printf "\n\n" ; \ 9 | printf "%-20s %s\n" "command" "description" ; \ 10 | printf "%-20s %s\n" "------" "-----------" ; \ 11 | for help_line in $${help_lines[@]}; do \ 12 | IFS=$$':' ; \ 13 | help_split=($$help_line) ; \ 14 | help_command=`echo $${help_split[0]} | sed -e 's/^ *//' -e 's/ *$$//'` ; \ 15 | help_info=`echo $${help_split[2]} | sed -e 's/^ *//' -e 's/ *$$//'` ; \ 16 | printf '\033[36m'; \ 17 | printf "%-20s %s" $$help_command ; \ 18 | printf '\033[0m'; \ 19 | printf "%s\n" $$help_info; \ 20 | done 21 | 22 | pull: ## Pull docker images. 23 | docker-compose pull 24 | build: ## Build docker images based on docker-compose.yml file. 25 | docker-compose build 26 | up: ## Start docker containers. 27 | docker-compose up 28 | upd: ## Start docker containers in daemon mode. 29 | docker-compose up -d 30 | upp: ## Rebuild and start docker containers. 31 | docker-compose up --build 32 | main: ## Enter the 'main' docker container. 33 | docker-compose exec main bash 34 | stop: ## Stop docker containers. 35 | docker-compose stop 36 | down: ## Destroy docker containers and volumes. 37 | docker-compose down -v 38 | clean: ## Destroy docker containers, local images and volumes. 39 | docker-compose down --rmi=local -v 40 | ci: ## Run tests in CI mode. 41 | NODE_ENV=ci docker-compose up --abort-on-container-exit --exit-code-from ci 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node Skeleton 2 | 3 | [![Maintainability](https://api.codeclimate.com/v1/badges/501d3320c5b7215676e3/maintainability)](https://codeclimate.com/github/greg-md/node-skeleton/maintainability) 4 | [![Test Coverage](https://api.codeclimate.com/v1/badges/501d3320c5b7215676e3/test_coverage)](https://codeclimate.com/github/greg-md/node-skeleton/test_coverage) 5 | [![Known Vulnerabilities](https://snyk.io/test/github/greg-md/node-skeleton/badge.svg?targetFile=package.json)](https://snyk.io/test/github/greg-md/node-skeleton?targetFile=package.json) 6 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=greg-md_node-skeleton&metric=alert_status)](https://sonarcloud.io/dashboard?id=greg-md_node-skeleton) 7 | 8 | Skeleton of a NodeJS backend, using 9 | [NestJS](https://nestjs.com/) + 10 | [GraphQL](https://docs.nestjs.com/graphql/quick-start) + 11 | [NATS](https://docs.nestjs.com/microservices/nats) + 12 | [Microservices](https://docs.nestjs.com/microservices/basics) + 13 | [Docker](https://www.docker.com/) + 14 | CI/CD + 15 | automated deployments with GitHub Actions using 16 | [Kubernetes](https://kubernetes.io/), 17 | [Helm](https://helm.sh/) and 18 | [AWS EKS](https://aws.amazon.com/eks/). 19 | 20 | The code is following [Domain-Driven Design (DDD)](https://thedomaindrivendesign.io/). 21 | 22 | Useful Documentation: 23 | - [Kubernetes Tutorial](https://youtu.be/X48VuDVv0do) 24 | - [AWS EKS - Create Kubernetes cluster on Amazon EKS](https://youtu.be/p6xDCz00TxU) 25 | - [Concept, Pros & Cons of Domain Driven Design](https://www.optimistikinfo.com/blogs/domain-driven-design) 26 | 27 | What has been done to the default app: 28 | - `npm i -g @nestjs/cli` 29 | - `nest new node-skeleton` 30 | - `cd node-skeleton` 31 | - `nest generate app micro` 32 | - `npm i nestjs/platform-fastify` 33 | - `npm remove @nestjs/platform-express @types/express supertest @types/supertest` 34 | - `npm i @nestjs/microservices` 35 | - `npm i nats` 36 | - `npm i sinon` 37 | - `npm i @nestjs/config` 38 | - `npm i @nestjs/graphql graphql-tools graphql apollo-server-fastify @moonwalker/graphql-nats-subscriptions` 39 | - `npm i winston colors` 40 | 41 | ### GitHub Repository Secrets 42 | 43 | ```sh 44 | CC_TEST_REPORTER_ID # CodeClimate Code Coverage Reporter ID 45 | AWS_ACCESS_KEY_ID # AWS Access Key ID 46 | AWS_SECRET_ACCESS_KEY # AWS Access Key Secret 47 | KUBE_CONFIG_DATA # cat $HOME/.kube/eksctl/clusters/skeleton | base64 48 | ``` 49 | 50 | ### Table of Contents 51 | 52 | - [Local Run](#local-run) 53 | - [Build & Deploy](#build--deploy) 54 | - [Pre Requirements](#pre-requirements) 55 | - [Build](#build) 56 | - [Deploy](#deploy) 57 | - [Rollback](#rollback) 58 | - [Destroy](#destroy) 59 | - [Cheatsheet](#cheatsheet) 60 | - [Make Commands](#make-commands) 61 | - [Debug](#debug) 62 | 63 | # Local Run 64 | 65 | Using Docker: 66 | ```sh 67 | docker-compose up 68 | ``` 69 | 70 | # Build & Deploy 71 | 72 | You can build & deploy out of the box to local minikube or to AWS EKS using Kubernetes. 73 | 74 | ## Pre Requirements 75 | 76 | Install [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/); 77 | 78 | Install [Helm](https://helm.sh/); 79 | 80 | ### Minikube 81 | 82 | Install [minikube](https://minikube.sigs.k8s.io/docs/start/); 83 | 84 | Start minikube: 85 | ```sh 86 | minikube start 87 | ``` 88 | 89 | ### AWS ECR 90 | 91 | Install and configure [AWS CLI](https://aws.amazon.com/cli/); 92 | 93 | Install [eksctl](https://eksctl.io/); 94 | 95 | Create AWS Cluster: 96 | ```sh 97 | eksctl create cluster -f ./build-deploy/cluster/skeleton.yaml --auto-kubeconfig 98 | ``` 99 | 100 | Scale Cluster if needed: 101 | ```sh 102 | eksctl scale nodegroup --cluster=skeleton --nodes=4 --name=skeleton-nodes 103 | ``` 104 | 105 | Tip: 106 | > Use `--kubeconfig ~/.kube/eksctl/clusters/skeleton` flag to use AWS EKS with `kubectl` or `helm`. 107 | 108 | ## Build 109 | 110 | ### Minikube 111 | 112 | Use the minikube docker deamon to build the image: 113 | ```bash 114 | eval $(minikube docker-env) 115 | ``` 116 | 117 | Build images: 118 | - `docker build -t skeleton/api --target production-api .` 119 | - `docker build -t skeleton/micro --target production-micro .` 120 | 121 | 122 | ### AWS ECR 123 | 124 | - Run GitHub Action: [Build Api](.github/workflows/build-api.yml); 125 | - Run GitHub Action: [Build Micro](.github/workflows/build-micro.yml). 126 | 127 | ## Deploy 128 | 129 | ### Minikube 130 | 131 | ```sh 132 | helm upgrade nats ./build-deploy/helm/nats --install --wait 133 | helm upgrade micro ./build-deploy/helm/micro --install --wait 134 | helm upgrade api ./build-deploy/helm/api --install --wait 135 | ``` 136 | 137 | Explose API: 138 | ```sh 139 | minikube service api-service 140 | ``` 141 | 142 | ### AWS ECR 143 | 144 | - Run GitHub Action [Deploy NATS](.github/workflows/deploy-nats.yml); 145 | - Run GitHub Action [Deploy Micro](.github/workflows/deploy-micro.yml); 146 | - Run GitHub Action [Deploy Api](.github/workflows/deploy-api.yml). 147 | 148 | ## Rollback 149 | 150 | ### Minikube 151 | 152 | ```sh 153 | helm rollback nats --wait 154 | helm rollback micro --wait 155 | helm rollback api --wait 156 | ``` 157 | 158 | ### AWS ECR 159 | 160 | - Run GitHub Action [Rollback NATS](.github/workflows/rollback-nats.yml); 161 | - Run GitHub Action [Rollback Micro](.github/workflows/rollback-micro.yml); 162 | - Run GitHub Action [Rollback Api](.github/workflows/rollback-api.yml). 163 | 164 | ## Destroy 165 | 166 | ### Minikube 167 | 168 | ```sh 169 | helm uninstall api 170 | helm uninstall micro 171 | helm uninstall nats 172 | ``` 173 | 174 | ### AWS ECR 175 | 176 | ```sh 177 | helm uninstall api --kubeconfig ~/.kube/eksctl/clusters/skeleton 178 | helm uninstall micro --kubeconfig ~/.kube/eksctl/clusters/skeleton 179 | helm uninstall nats --kubeconfig ~/.kube/eksctl/clusters/skeleton 180 | ``` 181 | 182 | Destroy AWS EKS Cluster: 183 | ```sh 184 | eksctl delete cluster --name skeleton 185 | ``` 186 | 187 | # Cheatsheet 188 | 189 | https://kubernetes.io/docs/reference/kubectl/cheatsheet/ 190 | 191 | ## Make commands 192 | 193 | To check the command list: 194 | ```sh 195 | make help 196 | ``` 197 | 198 | Output: 199 | ``` 200 | Usage: make 201 | 202 | command description 203 | ------ ----------- 204 | help Help dialog. 205 | pull Pull docker images. 206 | build Build docker images based on docker-compose.yml file. 207 | up Start docker containers. 208 | upd Start docker containers in daemon mode. 209 | upp Rebuild and start docker containers. 210 | main Enter the 'main' docker container. 211 | stop Stop docker containers. 212 | down Destroy docker containers and volumes. 213 | clean Destroy docker containers, local images and volumes. 214 | ci Run tests in CI mode. 215 | ``` 216 | 217 | ## Debug 218 | 219 | Listen for logs: 220 | ```bash 221 | kubectl logs -f -l app=api --all-containers [ --kubeconfig ~/.kube/eksctl/clusters/skeleton ] 222 | kubectl logs -f -l app=micro --all-containers [ --kubeconfig ~/.kube/eksctl/clusters/skeleton ] 223 | ``` 224 | 225 | Enter pods: 226 | ``` 227 | kubectl exec --stdin --tty -- /bin/sh 228 | ``` 229 | -------------------------------------------------------------------------------- /apps/api/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { CoreModule } from '@app/core'; 2 | import { Module } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { GraphQLModule } from '@nestjs/graphql'; 5 | import { GqlHelloModule } from './hello'; 6 | import { tabs } from './playground/tabs'; 7 | 8 | @Module({ 9 | imports: [ 10 | CoreModule.forRoot({ loggerLabel: 'Api' }), 11 | GraphQLModule.forRootAsync({ 12 | useFactory: (configService: ConfigService) => ({ 13 | debug: configService.get('GRAPHQL_DEBUG') === '1', 14 | playground: 15 | configService.get('NODE_ENV') === 'development' ? { tabs } : false, 16 | 17 | installSubscriptionHandlers: true, 18 | autoSchemaFile: 'schema.gql', 19 | 20 | // Middleware to pass request 21 | context: ({ req, payload, connection }) => { 22 | const payloadRequest = payload?.request; 23 | 24 | const connectionRequest = { 25 | headers: connection?.context?.headers || connection?.context || {}, 26 | }; 27 | 28 | return { req: req || payloadRequest || connectionRequest }; 29 | }, 30 | 31 | subscriptions: { 32 | // Send client payload to the connection context 33 | onConnect: (connectionParams) => connectionParams, 34 | }, 35 | }), 36 | inject: [ConfigService], 37 | }), 38 | GqlHelloModule, 39 | ], 40 | }) 41 | export class AppModule {} 42 | -------------------------------------------------------------------------------- /apps/api/src/hello/application/hello.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { MICRO_SERVICE } from '@app/core'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { of } from 'rxjs'; 4 | import { mock, SinonMock } from 'sinon'; 5 | import { HelloController } from './hello.controller'; 6 | import { HelloService } from './hello.service'; 7 | 8 | describe('HelloController', () => { 9 | let controller: HelloController; 10 | 11 | const clientProxy = { 12 | send: () => null, 13 | emit: () => null, 14 | }; 15 | let clientProxyMock: SinonMock; 16 | 17 | beforeEach(async () => { 18 | clientProxyMock = mock(clientProxy); 19 | const app: TestingModule = await Test.createTestingModule({ 20 | controllers: [HelloController], 21 | providers: [ 22 | HelloService, 23 | { provide: MICRO_SERVICE, useValue: clientProxy }, 24 | ], 25 | }).compile(); 26 | 27 | controller = app.get(HelloController); 28 | }); 29 | 30 | afterEach(async () => { 31 | clientProxyMock.restore(); 32 | }); 33 | 34 | describe('root', () => { 35 | it('should return "Hello World!"', async () => { 36 | const result = 'hello'; 37 | clientProxyMock 38 | .expects('send') 39 | .once() 40 | .withExactArgs({ cmd: 'hello' }, {}) 41 | .returns(of(result)); 42 | clientProxyMock 43 | .expects('emit') 44 | .once() 45 | .withExactArgs('hello_sent', result) 46 | .returns(of(true)); 47 | 48 | expect(await controller.getHello()).toBe('Hello World!'); 49 | clientProxyMock.verify(); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /apps/api/src/hello/application/hello.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Inject } from '@nestjs/common'; 2 | import { ClientProxy } from '@nestjs/microservices'; 3 | import { HelloService } from './hello.service'; 4 | import { timeout } from 'rxjs/operators'; 5 | import { MICRO_SERVICE } from '@app/core'; 6 | 7 | @Controller() 8 | export class HelloController { 9 | constructor( 10 | private readonly helloService: HelloService, 11 | @Inject(MICRO_SERVICE) private client: ClientProxy, 12 | ) {} 13 | 14 | @Get() 15 | async getHello(): Promise { 16 | const result = await this.client 17 | .send({ cmd: 'hello' }, {}) 18 | .pipe(timeout(60000)) 19 | .toPromise(); 20 | 21 | await this.client.emit('hello_sent', result).toPromise(); 22 | 23 | return this.helloService.getHello(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/api/src/hello/application/hello.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Subscription, Query, Mutation, Args } from '@nestjs/graphql'; 2 | import { HelloObject } from '../domain/hello.object'; 3 | import { HelloObjectify } from '../domain/hello.objectify'; 4 | import { Hello, HelloMetadata } from '../domain/hello'; 5 | import { HelloInput } from '../domain/hello.input'; 6 | import { HelloPubSub } from '../infrastructure/hello.pub-sub'; 7 | 8 | @Resolver('Hello') 9 | export class HelloResolver { 10 | constructor( 11 | private helloPubSub: HelloPubSub, 12 | private helloObjectify: HelloObjectify, 13 | ) {} 14 | 15 | @Query(() => HelloObject, { description: 'Say hello world.' }) 16 | async helloWorld(): Promise { 17 | const hello = new Hello('World'); 18 | 19 | return this.helloObjectify.objectify(hello); 20 | } 21 | 22 | @Mutation(() => HelloObject, { description: 'Say hello to someone.' }) 23 | async sayHello(@Args('hello') helloInput: HelloInput): Promise { 24 | const hello = new Hello(helloInput.name); 25 | 26 | return this.helloObjectify.objectify(hello); 27 | } 28 | 29 | @Subscription(() => HelloObject, { 30 | resolve(this: HelloResolver, payload: HelloMetadata) { 31 | return this.helloObjectify.objectifyMetadata(payload); 32 | }, 33 | }) 34 | onHello() { 35 | return this.helloPubSub.onHelloAsyncIterator(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apps/api/src/hello/application/hello.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class HelloService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/api/src/hello/domain/hello.input.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from '@nestjs/graphql'; 2 | 3 | @InputType() 4 | export class HelloInput { 5 | @Field(() => String) 6 | readonly name: string; 7 | } 8 | -------------------------------------------------------------------------------- /apps/api/src/hello/domain/hello.object.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | export class HelloObject { 5 | @Field(() => String) 6 | name: string; 7 | } 8 | -------------------------------------------------------------------------------- /apps/api/src/hello/domain/hello.objectify.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Hello, HelloMetadata } from './hello'; 3 | import { HelloObject } from './hello.object'; 4 | 5 | @Injectable() 6 | export class HelloObjectify { 7 | objectify(hello: Hello): HelloObject { 8 | return this.objectifyMetadata(hello.toMetadata()); 9 | } 10 | 11 | objectifyMetadata(metadata: HelloMetadata): HelloObject { 12 | return { 13 | name: metadata.name, 14 | }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/api/src/hello/domain/hello.ts: -------------------------------------------------------------------------------- 1 | export type HelloMetadata = { 2 | readonly name: string; 3 | }; 4 | 5 | export class Hello { 6 | constructor(public readonly name: string) {} 7 | 8 | toMetadata(): HelloMetadata { 9 | return { 10 | name: this.name, 11 | }; 12 | } 13 | 14 | static fromMetadata(metadata: HelloMetadata): Hello { 15 | return new Hello(metadata.name); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/api/src/hello/hello.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { HelloController } from './application/hello.controller'; 3 | import { HelloResolver } from './application/hello.resolver'; 4 | import { HelloService } from './application/hello.service'; 5 | import { HelloObjectify } from './domain/hello.objectify'; 6 | import { HelloPubSub } from './infrastructure/hello.pub-sub'; 7 | 8 | @Module({ 9 | providers: [HelloObjectify, HelloPubSub, HelloService, HelloResolver], 10 | controllers: [HelloController], 11 | }) 12 | export class GqlHelloModule {} 13 | -------------------------------------------------------------------------------- /apps/api/src/hello/index.ts: -------------------------------------------------------------------------------- 1 | export { GqlHelloModule } from './hello.module'; 2 | -------------------------------------------------------------------------------- /apps/api/src/hello/infrastructure/hello.pub-sub.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PubSubEngine } from 'graphql-subscriptions'; 3 | import { HelloMetadata } from '../domain/hello'; 4 | 5 | export const HelloEvents = { 6 | sayHello: 'SayHello', 7 | }; 8 | 9 | @Injectable() 10 | export class HelloPubSub { 11 | constructor(private pubSub: PubSubEngine) {} 12 | 13 | onHelloAsyncIterator() { 14 | return this.pubSub.asyncIterator(HelloEvents.sayHello); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/api/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { 3 | FastifyAdapter, 4 | NestFastifyApplication, 5 | } from '@nestjs/platform-fastify'; 6 | import { AppModule } from './app.module'; 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create( 10 | AppModule, 11 | new FastifyAdapter(), 12 | ); 13 | 14 | // Starts listening for shutdown hooks 15 | app.enableShutdownHooks(); 16 | 17 | await app.listen(3000, '0.0.0.0'); 18 | } 19 | bootstrap(); 20 | -------------------------------------------------------------------------------- /apps/api/src/playground/hello.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import { print } from 'graphql'; 3 | import { Tab, endpoint, authHeaders } from './tab'; 4 | 5 | export const helloWorld = gql` 6 | query helloWorld { 7 | helloWorld 8 | } 9 | `; 10 | 11 | export const sayHello = gql` 12 | mutation sayHello($hello: HelloInput!) { 13 | sayHello(hello: $hello) 14 | } 15 | `; 16 | 17 | export const onHello = gql` 18 | subscription onHello { 19 | onHello 20 | } 21 | `; 22 | 23 | export const HelloWorldTab: Tab = { 24 | endpoint, 25 | name: 'Hello World', 26 | query: print(helloWorld), 27 | headers: { ...authHeaders }, 28 | }; 29 | 30 | export const SayHelloTab: Tab = { 31 | endpoint, 32 | name: 'Say Hello', 33 | query: print(sayHello), 34 | variables: JSON.stringify({ 35 | name: 'John', 36 | }), 37 | headers: { ...authHeaders }, 38 | }; 39 | 40 | export const OnHelloTab: Tab = { 41 | endpoint, 42 | name: 'On Hello', 43 | query: print(onHello), 44 | headers: { ...authHeaders }, 45 | }; 46 | -------------------------------------------------------------------------------- /apps/api/src/playground/tab.ts: -------------------------------------------------------------------------------- 1 | export const endpoint = '/graphql'; 2 | 3 | export const authHeaders = { 4 | authorization: 'Bearer ', 5 | }; 6 | 7 | export type Tab = { 8 | endpoint: string; 9 | query: string; 10 | name?: string; 11 | variables?: string; 12 | responses?: string[]; 13 | headers?: { 14 | [key: string]: string; 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /apps/api/src/playground/tabs.ts: -------------------------------------------------------------------------------- 1 | import { Tab } from './tab'; 2 | import { SayHelloTab, HelloWorldTab, OnHelloTab } from './hello'; 3 | 4 | export const tabs: Tab[] = [HelloWorldTab, SayHelloTab, OnHelloTab]; 5 | -------------------------------------------------------------------------------- /apps/api/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppModule } from './../src/app.module'; 3 | import { 4 | FastifyAdapter, 5 | NestFastifyApplication, 6 | } from '@nestjs/platform-fastify'; 7 | 8 | describe('AppController (e2e)', () => { 9 | let app: NestFastifyApplication; 10 | 11 | beforeAll(async () => { 12 | const moduleRef: TestingModule = await Test.createTestingModule({ 13 | imports: [AppModule], 14 | }).compile(); 15 | 16 | app = moduleRef.createNestApplication( 17 | new FastifyAdapter(), 18 | ); 19 | 20 | await app.init(); 21 | await app.getHttpAdapter().getInstance().ready(); 22 | }); 23 | 24 | afterAll(async () => { 25 | await app.close(); 26 | }); 27 | 28 | it('/ (GET)', async () => { 29 | const result = await app.inject({ 30 | method: 'GET', 31 | url: '/', 32 | }); 33 | 34 | expect(result.statusCode).toEqual(200); 35 | expect(result.payload).toEqual('Hello World!'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /apps/api/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | }, 9 | "moduleNameMapper": { 10 | "@app/core/(.*)": "/../../../libs/core/src/$1", 11 | "@app/core": "/../../../libs/core/src" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/api/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/apps/api" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/micro/src/application/micro.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { LOGGER } from '@app/core'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { MicroController } from './micro.controller'; 4 | import { MicroService } from './micro.service'; 5 | 6 | describe('MicroController', () => { 7 | let microController: MicroController; 8 | 9 | const logger = { 10 | info: () => null, 11 | }; 12 | 13 | beforeEach(async () => { 14 | const app: TestingModule = await Test.createTestingModule({ 15 | controllers: [MicroController], 16 | providers: [MicroService, { provide: LOGGER, useValue: logger }], 17 | }).compile(); 18 | 19 | microController = app.get(MicroController); 20 | }); 21 | 22 | describe('root', () => { 23 | it('should return "Hello World from Micro!"', () => { 24 | expect(microController.hello()).toBe('Hello World from Micro!'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /apps/micro/src/application/micro.controller.ts: -------------------------------------------------------------------------------- 1 | import { LOGGER } from '@app/core'; 2 | import { Controller, Inject } from '@nestjs/common'; 3 | import { EventPattern, MessagePattern } from '@nestjs/microservices'; 4 | import { Logger } from 'winston'; 5 | import { MicroService } from './micro.service'; 6 | 7 | @Controller() 8 | export class MicroController { 9 | constructor( 10 | private readonly microService: MicroService, 11 | @Inject(LOGGER) private readonly logger: Logger, 12 | ) {} 13 | 14 | @MessagePattern({ cmd: 'hello' }) 15 | hello(): string { 16 | return this.microService.getHello(); 17 | } 18 | 19 | @EventPattern('hello_sent') 20 | async handleHelloSent(data: Record) { 21 | this.logger.info('Hello event received.', data); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/micro/src/application/micro.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class MicroService { 5 | getHello(): string { 6 | return 'Hello World from Micro!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/micro/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { MicroserviceOptions, Transport } from '@nestjs/microservices'; 3 | import { MicroModule } from './micro.module'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.createMicroservice( 7 | MicroModule, 8 | { 9 | transport: Transport.NATS, 10 | options: { 11 | url: process.env.NATS_URL, 12 | queue: 'micro_queue', 13 | }, 14 | }, 15 | ); 16 | 17 | // Starts listening for shutdown hooks 18 | app.enableShutdownHooks(); 19 | 20 | await app.listenAsync(); 21 | } 22 | bootstrap(); 23 | -------------------------------------------------------------------------------- /apps/micro/src/micro.module.ts: -------------------------------------------------------------------------------- 1 | import { CoreModule } from '@app/core'; 2 | import { Module } from '@nestjs/common'; 3 | import { MicroController } from './application/micro.controller'; 4 | import { MicroService } from './application/micro.service'; 5 | 6 | @Module({ 7 | imports: [CoreModule.forRoot({ loggerLabel: 'Micro' })], 8 | controllers: [MicroController], 9 | providers: [MicroService], 10 | }) 11 | export class MicroModule {} 12 | -------------------------------------------------------------------------------- /apps/micro/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/apps/micro" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /build-deploy/ci.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "$NODE_ENV" == "ci" ]; then 4 | ./build-deploy/wait-for-it.sh nats:4222 -s -t 0 -- \ 5 | npx concurrently --raw "npm:test:cov" "npm:test:e2e" 6 | else 7 | ./build-deploy/wait-for-it.sh nats:4222 -s -t 0 -- \ 8 | npx concurrently --raw "npm:test:watch" "npm:test:e2e -- --watch" 9 | fi 10 | -------------------------------------------------------------------------------- /build-deploy/cluster/skeleton.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: eksctl.io/v1alpha5 2 | kind: ClusterConfig 3 | 4 | metadata: 5 | name: skeleton 6 | region: eu-central-1 7 | version: "1.19" 8 | 9 | nodeGroups: 10 | - name: skeleton-nodes 11 | instanceType: t3.small 12 | desiredCapacity: 2 13 | labels: 14 | role: workers 15 | -------------------------------------------------------------------------------- /build-deploy/helm/api/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /build-deploy/helm/api/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: workflow 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /build-deploy/helm/api/templates/api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: api-deployment 5 | labels: 6 | app: api 7 | spec: 8 | replicas: 2 9 | selector: 10 | matchLabels: 11 | app: api 12 | template: 13 | metadata: 14 | labels: 15 | app: api 16 | spec: 17 | containers: 18 | - name: api 19 | image: {{ .Values.image }} 20 | imagePullPolicy: {{ .Values.pullPolicy }} 21 | ports: 22 | - containerPort: 3000 23 | env: 24 | - name: NATS_URL 25 | value: "nats://nats:4222" 26 | volumeMounts: 27 | - name: log 28 | mountPath: /var/log/skeleton 29 | volumes: 30 | - name: log 31 | persistentVolumeClaim: 32 | claimName: api-log-pvc 33 | --- 34 | apiVersion: v1 35 | kind: Service 36 | metadata: 37 | name: api-service 38 | spec: 39 | selector: 40 | app: api 41 | type: LoadBalancer 42 | ports: 43 | - protocol: TCP 44 | port: 3000 45 | targetPort: 3000 46 | nodePort: 30000 47 | --- 48 | apiVersion: v1 49 | kind: PersistentVolumeClaim 50 | metadata: 51 | name: api-log-pvc 52 | spec: 53 | storageClassName: {{ .Values.storageClassName }} 54 | volumeMode: Filesystem 55 | accessModes: 56 | - ReadWriteOnce 57 | resources: 58 | requests: 59 | storage: 100M 60 | -------------------------------------------------------------------------------- /build-deploy/helm/api/values.yaml: -------------------------------------------------------------------------------- 1 | image: skeleton/api 2 | pullPolicy: IfNotPresent 3 | storageClassName: standard 4 | -------------------------------------------------------------------------------- /build-deploy/helm/micro/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /build-deploy/helm/micro/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: workflow 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /build-deploy/helm/micro/templates/micro.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: micro-deployment 5 | labels: 6 | app: micro 7 | spec: 8 | replicas: 2 9 | selector: 10 | matchLabels: 11 | app: micro 12 | template: 13 | metadata: 14 | labels: 15 | app: micro 16 | spec: 17 | containers: 18 | - name: micro 19 | image: {{ .Values.image }} 20 | imagePullPolicy: {{ .Values.pullPolicy }} 21 | env: 22 | - name: NATS_URL 23 | value: "nats://nats:4222" 24 | volumeMounts: 25 | - name: log 26 | mountPath: /var/log/skeleton 27 | volumes: 28 | - name: log 29 | persistentVolumeClaim: 30 | claimName: micro-log-pvc 31 | --- 32 | apiVersion: v1 33 | kind: PersistentVolumeClaim 34 | metadata: 35 | name: micro-log-pvc 36 | spec: 37 | storageClassName: {{ .Values.storageClassName }} 38 | volumeMode: Filesystem 39 | accessModes: 40 | - ReadWriteOnce 41 | resources: 42 | requests: 43 | storage: 100M 44 | -------------------------------------------------------------------------------- /build-deploy/helm/micro/values.yaml: -------------------------------------------------------------------------------- 1 | image: skeleton/micro 2 | pullPolicy: IfNotPresent 3 | storageClassName: standard 4 | -------------------------------------------------------------------------------- /build-deploy/helm/nats/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /build-deploy/helm/nats/Chart.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: nats 3 | repository: https://nats-io.github.io/k8s/helm/charts/ 4 | version: 0.7.5 5 | digest: sha256:dd04a17c74ab0ff87e515607ad8e07aeb37c20d503e0a06e5b9ff06cbf40a934 6 | generated: "2021-03-02T19:05:43.500201+02:00" 7 | -------------------------------------------------------------------------------- /build-deploy/helm/nats/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: workflow 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | 26 | dependencies: 27 | - name: nats 28 | version: "^0.7.5" 29 | repository: "https://nats-io.github.io/k8s/helm/charts/" 30 | -------------------------------------------------------------------------------- /build-deploy/helm/nats/charts/nats-0.7.5.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greg-md/node-skeleton/53de7551cc0434055069dadf5e28ede76cfa0cbb/build-deploy/helm/nats/charts/nats-0.7.5.tgz -------------------------------------------------------------------------------- /build-deploy/helm/nats/values.yaml: -------------------------------------------------------------------------------- 1 | nats: 2 | cluster: 3 | enabled: true 4 | -------------------------------------------------------------------------------- /build-deploy/helm/volumes/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /build-deploy/helm/volumes/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: workflow 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /build-deploy/helm/volumes/templates/storage-class.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: storage.k8s.io/v1 2 | kind: StorageClass 3 | metadata: 4 | name: skeleton-storage-class 5 | provisioner: {{ .Values.provisioner }} 6 | parameters: 7 | type: io1 8 | iopsPerGB: "10" 9 | fsType: ext4 10 | -------------------------------------------------------------------------------- /build-deploy/helm/volumes/values.yaml: -------------------------------------------------------------------------------- 1 | provisioner: kubernetes.io/aws-ebs 2 | -------------------------------------------------------------------------------- /build-deploy/main.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "$NODE_ENV" == "ci" ]; then 4 | ./build-deploy/wait-for-it.sh nats:4222 -s -t 0 -- \ 5 | npx concurrently --raw "npm:start:dev -- micro" 6 | else 7 | ./build-deploy/wait-for-it.sh nats:4222 -s -t 0 -- \ 8 | npx concurrently --raw "npm:start:dev -- api" "npm:start:dev -- micro" 9 | fi 10 | -------------------------------------------------------------------------------- /build-deploy/wait-for-it.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Use this script to test if a given TCP host/port are available 3 | 4 | WAITFORIT_cmdname=${0##*/} 5 | 6 | echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } 7 | 8 | usage() 9 | { 10 | cat << USAGE >&2 11 | Usage: 12 | $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] 13 | -h HOST | --host=HOST Host or IP under test 14 | -p PORT | --port=PORT TCP port under test 15 | Alternatively, you specify the host and port as host:port 16 | -s | --strict Only execute subcommand if the test succeeds 17 | -q | --quiet Don't output any status messages 18 | -t TIMEOUT | --timeout=TIMEOUT 19 | Timeout in seconds, zero for no timeout 20 | -- COMMAND ARGS Execute command with args after the test finishes 21 | USAGE 22 | exit 1 23 | } 24 | 25 | wait_for() 26 | { 27 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then 28 | echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" 29 | else 30 | echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" 31 | fi 32 | WAITFORIT_start_ts=$(date +%s) 33 | while : 34 | do 35 | if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then 36 | nc -z $WAITFORIT_HOST $WAITFORIT_PORT 37 | WAITFORIT_result=$? 38 | else 39 | (echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 40 | WAITFORIT_result=$? 41 | fi 42 | if [[ $WAITFORIT_result -eq 0 ]]; then 43 | WAITFORIT_end_ts=$(date +%s) 44 | echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" 45 | break 46 | fi 47 | sleep 1 48 | done 49 | return $WAITFORIT_result 50 | } 51 | 52 | wait_for_wrapper() 53 | { 54 | # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 55 | if [[ $WAITFORIT_QUIET -eq 1 ]]; then 56 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & 57 | else 58 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & 59 | fi 60 | WAITFORIT_PID=$! 61 | trap "kill -INT -$WAITFORIT_PID" INT 62 | wait $WAITFORIT_PID 63 | WAITFORIT_RESULT=$? 64 | if [[ $WAITFORIT_RESULT -ne 0 ]]; then 65 | echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" 66 | fi 67 | return $WAITFORIT_RESULT 68 | } 69 | 70 | # process arguments 71 | while [[ $# -gt 0 ]] 72 | do 73 | case "$1" in 74 | *:* ) 75 | WAITFORIT_hostport=(${1//:/ }) 76 | WAITFORIT_HOST=${WAITFORIT_hostport[0]} 77 | WAITFORIT_PORT=${WAITFORIT_hostport[1]} 78 | shift 1 79 | ;; 80 | --child) 81 | WAITFORIT_CHILD=1 82 | shift 1 83 | ;; 84 | -q | --quiet) 85 | WAITFORIT_QUIET=1 86 | shift 1 87 | ;; 88 | -s | --strict) 89 | WAITFORIT_STRICT=1 90 | shift 1 91 | ;; 92 | -h) 93 | WAITFORIT_HOST="$2" 94 | if [[ $WAITFORIT_HOST == "" ]]; then break; fi 95 | shift 2 96 | ;; 97 | --host=*) 98 | WAITFORIT_HOST="${1#*=}" 99 | shift 1 100 | ;; 101 | -p) 102 | WAITFORIT_PORT="$2" 103 | if [[ $WAITFORIT_PORT == "" ]]; then break; fi 104 | shift 2 105 | ;; 106 | --port=*) 107 | WAITFORIT_PORT="${1#*=}" 108 | shift 1 109 | ;; 110 | -t) 111 | WAITFORIT_TIMEOUT="$2" 112 | if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi 113 | shift 2 114 | ;; 115 | --timeout=*) 116 | WAITFORIT_TIMEOUT="${1#*=}" 117 | shift 1 118 | ;; 119 | --) 120 | shift 121 | WAITFORIT_CLI=("$@") 122 | break 123 | ;; 124 | --help) 125 | usage 126 | ;; 127 | *) 128 | echoerr "Unknown argument: $1" 129 | usage 130 | ;; 131 | esac 132 | done 133 | 134 | if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then 135 | echoerr "Error: you need to provide a host and port to test." 136 | usage 137 | fi 138 | 139 | WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} 140 | WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} 141 | WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} 142 | WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} 143 | 144 | # check to see if timeout is from busybox? 145 | WAITFORIT_TIMEOUT_PATH=$(type -p timeout) 146 | WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) 147 | if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then 148 | WAITFORIT_ISBUSY=1 149 | WAITFORIT_BUSYTIMEFLAG="-t" 150 | 151 | else 152 | WAITFORIT_ISBUSY=0 153 | WAITFORIT_BUSYTIMEFLAG="" 154 | fi 155 | 156 | if [[ $WAITFORIT_CHILD -gt 0 ]]; then 157 | wait_for 158 | WAITFORIT_RESULT=$? 159 | exit $WAITFORIT_RESULT 160 | else 161 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then 162 | wait_for_wrapper 163 | WAITFORIT_RESULT=$? 164 | else 165 | wait_for 166 | WAITFORIT_RESULT=$? 167 | fi 168 | fi 169 | 170 | if [[ $WAITFORIT_CLI != "" ]]; then 171 | if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then 172 | echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" 173 | exit $WAITFORIT_RESULT 174 | fi 175 | exec "${WAITFORIT_CLI[@]}" 176 | else 177 | exit $WAITFORIT_RESULT 178 | fi 179 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | main: 4 | build: 5 | context: ${DOCKER_ROOT:-.} 6 | target: development 7 | depends_on: 8 | - nats 9 | env_file: 10 | - ${DOCKER_ROOT:-.}/.env 11 | command: ./build-deploy/main.sh 12 | volumes: 13 | - ${DOCKER_ROOT:-.}:/node:delegated 14 | - node_modules:/node/node_modules 15 | ports: 16 | - "3000:3000" 17 | ci: 18 | build: 19 | context: ${DOCKER_ROOT:-.} 20 | target: development 21 | depends_on: 22 | - main 23 | env_file: 24 | - ${DOCKER_ROOT:-.}/.env 25 | environment: 26 | - NODE_ENV=${NODE_ENV:-development} 27 | command: ./build-deploy/ci.sh 28 | volumes: 29 | - ${DOCKER_ROOT:-.}:/node:delegated 30 | - ci_node_modules:/node/node_modules 31 | nats: 32 | image: nats 33 | ports: 34 | - "8222:8222" 35 | # Enable NATS cluster mode if needed. 36 | # nats-1: 37 | # image: nats 38 | # command: "--cluster nats://0.0.0.0:6222 --routes=nats://ruser:T0pS3cr3t@nats:6222" 39 | # networks: 40 | # skeleton: 41 | # depends_on: 42 | # - nats 43 | # nats-2: 44 | # image: nats 45 | # command: "--cluster nats://0.0.0.0:6222 --routes=nats://ruser:T0pS3cr3t@nats:6222" 46 | # networks: 47 | # skeleton: 48 | # depends_on: 49 | # - nats 50 | volumes: 51 | node_modules: 52 | ci_node_modules: 53 | -------------------------------------------------------------------------------- /libs/core/src/core.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { ClientsModule, Transport } from '@nestjs/microservices'; 5 | import { PubSubEngine } from 'graphql-subscriptions'; 6 | import { connect } from 'nats'; 7 | import { LOGGER } from './domain/logger'; 8 | import { MICRO_SERVICE } from './domain/micro-service'; 9 | import { NatsPubSub } from './infrastructure/nats.pub-sub'; 10 | import { createLoggerFactory } from './infrastructure/winston.logger'; 11 | 12 | export type CoreModuleOptions = { 13 | loggerLabel?: string; 14 | }; 15 | 16 | @Module({ 17 | imports: [ 18 | ConfigModule.forRoot(), 19 | ClientsModule.registerAsync([ 20 | { 21 | name: MICRO_SERVICE, 22 | imports: [ConfigModule], 23 | useFactory: (configService: ConfigService) => ({ 24 | transport: Transport.NATS, 25 | options: { 26 | url: configService.get('NATS_URL'), 27 | queue: 'micro_queue', 28 | }, 29 | }), 30 | inject: [ConfigService], 31 | }, 32 | ]), 33 | ], 34 | providers: [ 35 | { 36 | provide: PubSubEngine, 37 | useFactory: (configService: ConfigService) => { 38 | return new NatsPubSub(connect(configService.get('NATS_URL'))); 39 | }, 40 | inject: [ConfigService], 41 | }, 42 | ], 43 | exports: [ConfigModule, ClientsModule, PubSubEngine], 44 | }) 45 | export class CoreModule { 46 | static forRoot(options: CoreModuleOptions): DynamicModule { 47 | return { 48 | global: true, 49 | module: CoreModule, 50 | providers: [ 51 | { 52 | provide: LOGGER, 53 | useFactory: (configService: ConfigService) => { 54 | return createLoggerFactory( 55 | options.loggerLabel || 'Main', 56 | configService, 57 | ); 58 | }, 59 | inject: [ConfigService], 60 | }, 61 | ], 62 | exports: [LOGGER], 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /libs/core/src/domain/logger.ts: -------------------------------------------------------------------------------- 1 | export const LOGGER = 'LOGGER'; 2 | -------------------------------------------------------------------------------- /libs/core/src/domain/micro-service.ts: -------------------------------------------------------------------------------- 1 | export const MICRO_SERVICE = 'MICRO_SERVICE'; 2 | -------------------------------------------------------------------------------- /libs/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { CoreModule } from './core.module'; 2 | export { MICRO_SERVICE } from './domain/micro-service'; 3 | export { LOGGER } from './domain/logger'; 4 | -------------------------------------------------------------------------------- /libs/core/src/infrastructure/nats.pub-sub.ts: -------------------------------------------------------------------------------- 1 | import { PubSubEngine } from 'graphql-subscriptions'; 2 | import { PubSubAsyncIterator } from 'graphql-subscriptions/dist/pubsub-async-iterator'; 3 | import { Injectable, OnModuleDestroy } from '@nestjs/common'; 4 | import { Client } from 'nats'; 5 | 6 | @Injectable() 7 | export class NatsPubSub implements PubSubEngine, OnModuleDestroy { 8 | constructor(private readonly client: Client) { 9 | this.client = client; 10 | } 11 | 12 | public async publish(subject: string, payload: any): Promise { 13 | this.client.publish(subject, JSON.stringify(payload)); 14 | } 15 | 16 | public async subscribe( 17 | subject: string, 18 | onMessage: (message: string) => void, 19 | ): Promise { 20 | return this.client.subscribe(subject, (event: string) => 21 | onMessage(JSON.parse(event)), 22 | ); 23 | } 24 | 25 | public unsubscribe(sid: number) { 26 | this.client.unsubscribe(sid); 27 | } 28 | 29 | public asyncIterator(subjects: string | string[]): AsyncIterator { 30 | return new PubSubAsyncIterator(this, subjects); 31 | } 32 | 33 | onModuleDestroy() { 34 | this.client.close(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /libs/core/src/infrastructure/winston.logger.ts: -------------------------------------------------------------------------------- 1 | import { format } from 'logform'; 2 | import * as colors from 'colors/safe'; 3 | import { createLogger, transports } from 'winston'; 4 | import { ConfigService } from '@nestjs/config'; 5 | 6 | colors.setTheme({ 7 | error: 'red', 8 | warn: 'magenta', 9 | info: 'green', 10 | http: 'cyan', 11 | verbose: 'cyan', 12 | debug: 'blue', 13 | silly: 'grey', 14 | }); 15 | 16 | const splat = (Symbol.for('splat') as unknown) as string; 17 | const level = (Symbol.for('level') as unknown) as string; 18 | 19 | const formatMetadata = format((info) => { 20 | info.label = `[${info.label}]`; 21 | info.level = `[${info.level}]`; 22 | info.timestamp = new Date(info.timestamp).toLocaleString(undefined, { 23 | year: 'numeric', 24 | hour: 'numeric', 25 | minute: 'numeric', 26 | second: 'numeric', 27 | day: '2-digit', 28 | month: '2-digit', 29 | }); 30 | 31 | return info; 32 | }); 33 | 34 | const colorize = format((info) => { 35 | info.label = colors.yellow(info.label); 36 | info.ms = colors.yellow(info.ms); 37 | 38 | if (colors[info[level]]) { 39 | info.level = colors[info[level]](info.level); 40 | info.message = colors[info[level]](info.message); 41 | } else { 42 | info.level = colors.green(info.level); 43 | info.message = colors.green(info.message); 44 | } 45 | 46 | return info; 47 | }); 48 | 49 | const logFormat = format.printf( 50 | ({ timestamp, label, level, message, ms, ...meta }) => { 51 | return `${timestamp} ${label} ${level} ${message} ${ms} ${meta[splat] 52 | .map((context: unknown) => JSON.stringify(context)) 53 | .join(' ')}`; 54 | }, 55 | ); 56 | 57 | export function createLoggerFactory( 58 | label: string, 59 | configService: ConfigService, 60 | ) { 61 | const formats = [ 62 | format.timestamp(), 63 | format.ms(), 64 | format.label({ label }), 65 | formatMetadata(), 66 | ]; 67 | 68 | const logger = createLogger({ 69 | format: format.combine(...formats, logFormat), 70 | transports: [ 71 | new transports.File({ filename: '/var/log/skeleton/main.log' }), 72 | ], 73 | }); 74 | 75 | if (configService.get('NODE_ENV') === 'development') { 76 | logger.add( 77 | new transports.Console({ 78 | level: 'silly', 79 | format: format.combine(colorize(), logFormat), 80 | }), 81 | ); 82 | } 83 | 84 | return logger; 85 | } 86 | -------------------------------------------------------------------------------- /libs/core/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/core" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "apps/api/src", 4 | "monorepo": true, 5 | "root": "apps/api", 6 | "compilerOptions": { 7 | "webpack": true, 8 | "tsConfigPath": "apps/api/tsconfig.app.json" 9 | }, 10 | "projects": { 11 | "api": { 12 | "type": "application", 13 | "root": "apps/api", 14 | "entryFile": "main", 15 | "sourceRoot": "apps/api/src", 16 | "compilerOptions": { 17 | "tsConfigPath": "apps/api/tsconfig.app.json" 18 | } 19 | }, 20 | "micro": { 21 | "type": "application", 22 | "root": "apps/micro", 23 | "entryFile": "main", 24 | "sourceRoot": "apps/micro/src", 25 | "compilerOptions": { 26 | "tsConfigPath": "apps/micro/tsconfig.app.json" 27 | } 28 | }, 29 | "core": { 30 | "type": "library", 31 | "root": "libs/core", 32 | "entryFile": "index", 33 | "sourceRoot": "libs/core/src", 34 | "compilerOptions": { 35 | "tsConfigPath": "libs/core/tsconfig.lib.json" 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skeleton", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/apps/api/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", 17 | "lint:fix": "npm run lint -- --fix", 18 | "test": "jest", 19 | "test:watch": "jest --watch", 20 | "test:cov": "jest --coverage", 21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 22 | "test:e2e": "jest --testTimeout 60000 --config ./apps/api/test/jest-e2e.json" 23 | }, 24 | "dependencies": { 25 | "@nestjs/common": "^7.5.1", 26 | "@nestjs/config": "^0.6.3", 27 | "@nestjs/core": "^7.5.1", 28 | "@nestjs/graphql": "^7.9.11", 29 | "@nestjs/microservices": "^7.6.12", 30 | "@nestjs/platform-fastify": "^7.6.12", 31 | "@types/sinon": "^9.0.10", 32 | "apollo-server-fastify": "^2.21.0", 33 | "graphql": "^14.7.0", 34 | "graphql-tools": "^4.0.8", 35 | "nats": "^1.4.12", 36 | "reflect-metadata": "^0.1.13", 37 | "rimraf": "^3.0.2", 38 | "rxjs": "^6.6.3", 39 | "sinon": "^9.2.4", 40 | "winston": "^3.3.3" 41 | }, 42 | "devDependencies": { 43 | "@nestjs/cli": "^7.5.1", 44 | "@nestjs/schematics": "^7.1.3", 45 | "@nestjs/testing": "^7.5.1", 46 | "@types/jest": "^26.0.15", 47 | "@types/node": "^14.14.6", 48 | "@typescript-eslint/eslint-plugin": "^4.6.1", 49 | "@typescript-eslint/parser": "^4.6.1", 50 | "concurrently": "^5.3.0", 51 | "eslint": "^7.12.1", 52 | "eslint-config-prettier": "7.2.0", 53 | "eslint-plugin-prettier": "^3.1.4", 54 | "jest": "^26.6.3", 55 | "prettier": "^2.1.2", 56 | "ts-jest": "^26.4.3", 57 | "ts-loader": "^8.0.8", 58 | "ts-node": "^9.0.0", 59 | "tsconfig-paths": "^3.9.0", 60 | "typescript": "^4.0.5" 61 | }, 62 | "jest": { 63 | "moduleFileExtensions": [ 64 | "js", 65 | "json", 66 | "ts" 67 | ], 68 | "rootDir": ".", 69 | "testRegex": ".*\\.spec\\.ts$", 70 | "transform": { 71 | "^.+\\.(t|j)s$": "ts-jest" 72 | }, 73 | "collectCoverageFrom": [ 74 | "**/*.(t|j)s" 75 | ], 76 | "coverageDirectory": "./coverage", 77 | "testEnvironment": "node", 78 | "roots": [ 79 | "/apps/", 80 | "/libs/" 81 | ], 82 | "moduleNameMapper": { 83 | "@app/core/(.*)": "/libs/core/src/$1", 84 | "@app/core": "/libs/core/src" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "paths": { 15 | "@app/core": [ 16 | "libs/core/src" 17 | ], 18 | "@app/core/*": [ 19 | "libs/core/src/*" 20 | ] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(options) { 2 | return { 3 | ...options, 4 | watchOptions: { 5 | aggregateTimeout: 300, 6 | poll: 1000, 7 | }, 8 | }; 9 | }; 10 | --------------------------------------------------------------------------------