├── .nvmrc ├── .husky ├── .gitignore ├── pre-commit ├── commit-msg ├── pre-push └── install.mjs ├── .env.test ├── .npmrc ├── .github ├── CODEOWNERS ├── workflows │ ├── typos.yml │ ├── lint-yaml.yml │ ├── sync-labels.yml │ ├── conventional-label.yml │ ├── lint-dotenv.yml │ ├── lint-dockerfile.yml │ ├── dependabot-auto-merge.yml │ ├── pr-scope-label.yml │ ├── node.yml │ ├── dependency-review.yml │ ├── lint-github-action.yml │ ├── assign-me.yml │ ├── todo-to-issue.yml │ ├── stale-issues-and-prs.yml │ ├── pr-size-labeler.yml │ ├── greetings.yml │ ├── lint-pr-title.yml │ └── docker-size.yml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── settings.yml ├── actions │ └── setup-node │ │ └── action.yml ├── labels.yml └── pr-scope-labeler.yml ├── .eslintignore ├── .env.example ├── tsconfig.prod.json ├── .prettierignore ├── .npmignore ├── tests ├── utils │ └── mock.ts ├── performance │ └── contexts │ │ └── users │ │ └── get-users.mjs ├── unit │ └── app │ │ └── health │ │ └── api │ │ └── health.controller.test.ts └── e2e │ └── health.test.ts ├── images └── nestjs.png ├── .gitignore ├── .dockerignore ├── docs ├── SECURITY.md ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md ├── _typos.toml ├── .editorconfig ├── commitlint.config.ts ├── .nycrc.json ├── src ├── contexts │ ├── users │ │ ├── user.module.ts │ │ └── api │ │ │ └── user.controller.ts │ └── shared │ │ └── logger │ │ └── logger.module.ts ├── app │ ├── health │ │ ├── health.module.ts │ │ └── api │ │ │ └── health.controller.ts │ └── app.module.ts └── main.ts ├── nest-cli.json ├── prettier.config.mjs ├── vitest.config.e2e.ts ├── vitest.config.ts ├── vitest.config.unit.ts ├── .yamllint.yml ├── lint-staged.config.mjs ├── scripts ├── lint_yaml.sh ├── calculate-global-test-coverage.ts └── check_typos.sh ├── create-vitest-test-config.ts ├── .swcrc ├── tsconfig.json ├── LICENSE.md ├── Dockerfile ├── docker-compose.yml ├── .eslintrc ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | NODE_ENV=test 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @AlbertHernandez 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | coverage/ 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | node --run build 2 | find dist/main.js 3 | node --run test 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ENABLE_EXPERIMENTAL_COREPACK=1 2 | LOGGER_LEVEL=log 3 | PORT=3000 4 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | } 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | coverage/ 3 | node_modules/ 4 | tsconfig.json 5 | tsconfig.prod.json 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .tmp 4 | .idea 5 | .env 6 | coverage/ 7 | .npm 8 | .vscode 9 | -------------------------------------------------------------------------------- /tests/utils/mock.ts: -------------------------------------------------------------------------------- 1 | export { mock as createMock, MockProxy as Mock } from "vitest-mock-extended"; 2 | -------------------------------------------------------------------------------- /images/nestjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbertHernandez/nestjs-service-template/HEAD/images/nestjs.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .tmp 4 | .idea 5 | .env 6 | coverage/ 7 | .npm 8 | .vscode 9 | .nyc_output 10 | k6-results 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !/package.json 3 | !/pnpm-lock.yaml 4 | !/tsconfig.prod.json 5 | !/tsconfig.json 6 | !/.swcrc 7 | !/nest-cli.json 8 | !/src 9 | -------------------------------------------------------------------------------- /docs/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please report security issues to `alberthernandezdev@gmail.com`. 6 | -------------------------------------------------------------------------------- /_typos.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | extend-ignore-re = [ 3 | "\\b[0-9A-Za-z+/]{91}(=|==)?\\b", # base˚64 strings 4 | "[0-9a-fA-F]{7,}", # git commit hashes and mongo ids 5 | ] 6 | -------------------------------------------------------------------------------- /.husky/install.mjs: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production' || process.env.CI === 'true') { 2 | process.exit(0) 3 | } 4 | const husky = (await import('husky')).default 5 | console.log(husky()) 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /commitlint.config.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from "@commitlint/types"; 2 | 3 | const config: UserConfig = { 4 | extends: ["@commitlint/config-conventional"], 5 | }; 6 | 7 | export default config; 8 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "check-coverage": false, 4 | "branches": 80, 5 | "lines": 80, 6 | "functions": 80, 7 | "statements": 80, 8 | "reporter": ["lcov", "json", "text"] 9 | } 10 | -------------------------------------------------------------------------------- /src/contexts/users/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | 3 | import { UserController } from "./api/user.controller"; 4 | 5 | @Module({ 6 | controllers: [UserController], 7 | }) 8 | export class UserModule {} 9 | -------------------------------------------------------------------------------- /src/app/health/health.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | 3 | import { HealthController } from "./api/health.controller"; 4 | 5 | @Module({ 6 | controllers: [HealthController], 7 | }) 8 | export class HealthModule {} 9 | -------------------------------------------------------------------------------- /src/contexts/users/api/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from "@nestjs/common"; 2 | 3 | @Controller("users") 4 | export class UserController { 5 | @Get() 6 | run() { 7 | return { users: "ok" }; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true, 7 | "builder": "swc", 8 | "typeCheck": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: false, 7 | trailingComma: "all", 8 | bracketSpacing: true, 9 | arrowParens: "avoid", 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /vitest.config.e2e.ts: -------------------------------------------------------------------------------- 1 | import swc from "unplugin-swc"; 2 | import { defineConfig } from "vitest/config"; 3 | 4 | import { createVitestTestConfig } from "./create-vitest-test-config"; 5 | 6 | export default defineConfig({ 7 | test: createVitestTestConfig("e2e"), 8 | plugins: [swc.vite()], 9 | }); 10 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import swc from "unplugin-swc"; 2 | import { defineConfig } from "vitest/config"; 3 | 4 | import { createVitestTestConfig } from "./create-vitest-test-config"; 5 | 6 | export default defineConfig({ 7 | test: createVitestTestConfig("(unit|e2e)"), 8 | plugins: [swc.vite()], 9 | }); 10 | -------------------------------------------------------------------------------- /vitest.config.unit.ts: -------------------------------------------------------------------------------- 1 | import swc from "unplugin-swc"; 2 | import { defineConfig } from "vitest/config"; 3 | 4 | import { createVitestTestConfig } from "./create-vitest-test-config"; 5 | 6 | export default defineConfig({ 7 | test: createVitestTestConfig("unit"), 8 | plugins: [swc.vite()], 9 | }); 10 | -------------------------------------------------------------------------------- /.github/workflows/typos.yml: -------------------------------------------------------------------------------- 1 | name: '🙊 Typos' 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | typos: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 🛬 11 | uses: actions/checkout@v4 12 | - name: 🙊 Run code spell checker to check typos 13 | uses: crate-ci/typos@v1.40.0 14 | -------------------------------------------------------------------------------- /.github/workflows/lint-yaml.yml: -------------------------------------------------------------------------------- 1 | name: '💅 Lint yaml' 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | lint: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Install yamllint 12 | run: pip install yamllint 13 | - name: Lint YAML files 14 | run: yamllint . 15 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yml: -------------------------------------------------------------------------------- 1 | name: '🔄 Sync labels' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: micnncim/action-label-syncer@v1 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | extends: default 2 | 3 | ignore: | 4 | node_modules/ 5 | pnpm-lock.yaml 6 | 7 | rules: 8 | document-start: 9 | present: false 10 | line-length: 11 | ignore: | 12 | /.github/actions/**/*.yml 13 | /.github/workflows/*.yml 14 | /.github/settings.yml 15 | truthy: 16 | ignore: | 17 | /.github/workflows/*.yml 18 | -------------------------------------------------------------------------------- /lint-staged.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | "**/*.{ts?(x),mts}": () => "tsc -p tsconfig.prod.json --noEmit", 3 | "*.{js,jsx,mjs,cjs,ts,tsx,mts}": [ 4 | "node --run lint:file", 5 | "vitest related --run", 6 | ], 7 | "*.{md,json}": "prettier --write", 8 | "*": "node --run typos", 9 | "*.{yml,yaml}": "node --run lint:yaml", 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /src/app/health/api/health.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, HttpCode, Inject, Logger } from "@nestjs/common"; 2 | 3 | @Controller("health") 4 | export class HealthController { 5 | constructor(@Inject(Logger) private readonly logger: Logger) {} 6 | 7 | @Get() 8 | @HttpCode(200) 9 | run() { 10 | this.logger.log("Health endpoint called!"); 11 | return { status: "ok" }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/conventional-label.yml: -------------------------------------------------------------------------------- 1 | name: '🏷️ Conventional release labels' 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, edited, reopened] 6 | 7 | jobs: 8 | label: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: bcoe/conventional-release-labels@v1 12 | with: 13 | type_labels: '{"feat": "🚀 Feature", "fix": "🕵🏻 Fix", "breaking": "⚠️ Breaking Change"}' 14 | -------------------------------------------------------------------------------- /.github/workflows/lint-dotenv.yml: -------------------------------------------------------------------------------- 1 | name: '💅 Lint dotenv' 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build: 8 | name: Lint dotenv 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Install dotenv 13 | run: curl -sSfL https://git.io/JLbXn | sh -s -- -b usr/local/bin v3.3.0 14 | - name: Run dotenv 15 | run: usr/local/bin/dotenv-linter 16 | -------------------------------------------------------------------------------- /.github/workflows/lint-dockerfile.yml: -------------------------------------------------------------------------------- 1 | name: '💅 Lint dockerfile' 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build: 8 | name: Lint dockerfile 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: hadolint/hadolint-action@v3.3.0 13 | id: hadolint 14 | with: 15 | dockerfile: Dockerfile 16 | - name: Build dockerfile 17 | run: docker build . -t service 18 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: '🤖 Dependabot auto merge' 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | auto-merge: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 12 | with: 13 | target: minor 14 | github-token: ${{ secrets.DEPENDABOT_AUTO_MERGE_GITHUB_TOKEN }} 15 | command: squash and merge 16 | -------------------------------------------------------------------------------- /.github/workflows/pr-scope-label.yml: -------------------------------------------------------------------------------- 1 | name: '🏷️ PR Scope label' 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, synchronize, reopened] 6 | 7 | jobs: 8 | labeler: 9 | permissions: 10 | contents: read 11 | pull-requests: write 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/labeler@v5 15 | with: 16 | configuration-path: .github/pr-scope-labeler.yml 17 | sync-labels: true 18 | -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | name: '🐢 Node' 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | run: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 🛬 11 | uses: actions/checkout@v4 12 | - name: Setup Node ⚙️ 13 | uses: ./.github/actions/setup-node 14 | - name: Build typescript 📦 15 | run: node --run build && find dist/main.js 16 | - name: Lint code 💅 17 | run: node --run lint 18 | - name: Run tests ✅ 19 | run: node --run test 20 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { ConfigModule } from "@nestjs/config"; 3 | 4 | import { HealthModule } from "@/app/health/health.module"; 5 | 6 | import { LoggerModule } from "@/shared/logger/logger.module"; 7 | 8 | import { UserModule } from "@/contexts/users/user.module"; 9 | 10 | @Module({ 11 | imports: [ 12 | ConfigModule.forRoot({ isGlobal: true, cache: true }), 13 | LoggerModule, 14 | HealthModule, 15 | UserModule, 16 | ], 17 | }) 18 | export class AppModule {} 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | labels: 8 | - '📦 Dependencies' 9 | commit-message: 10 | prefix: 'fix' 11 | prefix-development: 'chore' 12 | include: 'scope' 13 | versioning-strategy: 'increase' 14 | - package-ecosystem: 'github-actions' 15 | directory: '/' 16 | schedule: 17 | interval: 'daily' 18 | labels: 19 | - '📦 Dependencies' 20 | - '🚀 CI/CD' 21 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: '🛡️ Dependency Review' 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | 11 | jobs: 12 | dependency-review: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: 'Checkout Repository' 16 | uses: actions/checkout@v4 17 | - name: 'Dependency Review' 18 | uses: actions/dependency-review-action@v4 19 | with: 20 | comment-summary-in-pr: always 21 | -------------------------------------------------------------------------------- /.github/workflows/lint-github-action.yml: -------------------------------------------------------------------------------- 1 | name: '💅 Lint GitHub Actions workflows' 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | actionlint: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Download actionlint 12 | id: get_actionlint 13 | run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 1.6.26 14 | shell: bash 15 | - name: Check workflow files 16 | run: ${{ steps.get_actionlint.outputs.executable }} -color 17 | shell: bash 18 | -------------------------------------------------------------------------------- /tests/performance/contexts/users/get-users.mjs: -------------------------------------------------------------------------------- 1 | import { check } from "k6"; 2 | import http from "k6/http"; 3 | 4 | const BASE_URL = __ENV.BASE_URL || "http://localhost:3000"; 5 | 6 | export const options = { 7 | stages: [ 8 | { duration: "10s", target: 10 }, 9 | { duration: "10s", target: 100 }, 10 | { duration: "10s", target: 10 }, 11 | { duration: "10s", target: 0 }, 12 | ], 13 | }; 14 | 15 | export default function () { 16 | const res = http.get(`${BASE_URL}/api/users`); 17 | check(res, { 18 | "Get status is 200": r => r.status === 200, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /scripts/lint_yaml.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! command -v yamllint >/dev/null 2>&1; then 4 | echo "YamlLint CLI tool is not installed, aborting yaml linter." 5 | echo "If you want to install it, you can run 'brew install yamllint'" 6 | exit 0 # We don't want to fail the build if the tool is not installed 7 | fi 8 | 9 | if [ "$#" -eq 0 ]; then 10 | files="." 11 | else 12 | current_dir=$(pwd) 13 | files="" 14 | for file in "$@"; do 15 | relative_file="${file#$current_dir/}" 16 | files="$files $relative_file" 17 | done 18 | fi 19 | 20 | yamllint $files 21 | -------------------------------------------------------------------------------- /.github/workflows/assign-me.yml: -------------------------------------------------------------------------------- 1 | name: '🙋‍♂️ Assign me' 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | jobs: 8 | set_assignee: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/github-script@v7 12 | if: github.actor != 'dependabot[bot]' 13 | with: 14 | script: | 15 | github.rest.issues.addAssignees({ 16 | owner: context.repo.owner, 17 | repo: context.repo.repo, 18 | issue_number: context.issue.number, 19 | assignees: [context.actor], 20 | }) 21 | -------------------------------------------------------------------------------- /create-vitest-test-config.ts: -------------------------------------------------------------------------------- 1 | import { loadEnv } from "vite"; 2 | import { InlineConfig } from "vitest"; 3 | 4 | export const createVitestTestConfig = (testingType: string): InlineConfig => { 5 | return { 6 | root: "./", 7 | globals: true, 8 | isolate: false, 9 | passWithNoTests: true, 10 | include: [`tests/${testingType}/**/*.test.ts`], 11 | env: loadEnv("test", process.cwd(), ""), 12 | coverage: { 13 | provider: "istanbul", 14 | reporter: ["text", "json", "html"], 15 | reportsDirectory: `coverage/${testingType}`, 16 | include: ["src/**/*.ts"], 17 | exclude: ["src/main.ts"], 18 | }, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /.github/workflows/todo-to-issue.yml: -------------------------------------------------------------------------------- 1 | name: '✅ Todo to issue' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: "TODO to Issue" 14 | uses: "alstr/todo-to-issue-action@v4" 15 | with: 16 | ISSUE_TEMPLATE: | 17 | ## ✅ Codebase TODO ✅ 18 | 19 | ### **📝 Title**: {{ title }} 20 | 21 | ### **🔎 Details** 22 | 23 | {{ body }} 24 | {{ url }} 25 | {{ snippet }} 26 | AUTO_ASSIGN: true 27 | IGNORE: ".github/workflows/todo-to-issue.yml" 28 | -------------------------------------------------------------------------------- /scripts/calculate-global-test-coverage.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | import path from "node:path"; 3 | 4 | import fs from "fs-extra"; 5 | 6 | const REPORTS_PATH = path.resolve(process.cwd(), ".nyc_output"); 7 | const COVERAGE_PATH = path.resolve(process.cwd(), "coverage"); 8 | 9 | fs.emptyDirSync(REPORTS_PATH); 10 | fs.copyFileSync( 11 | `${COVERAGE_PATH}/unit/coverage-final.json`, 12 | `${REPORTS_PATH}/unit-coverage.json`, 13 | ); 14 | fs.copyFileSync( 15 | `${COVERAGE_PATH}/e2e/coverage-final.json`, 16 | `${REPORTS_PATH}/e2e-coverage.json`, 17 | ); 18 | execSync(`nyc report --report-dir ${COVERAGE_PATH}/global`, { 19 | stdio: "inherit", 20 | }); 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "🐞 Bug" 6 | assignees: "" 7 | --- 8 | 9 | # Prerequisites 10 | 11 | - [ ] I checked to make sure that this issue has not already been filed 12 | 13 | **Describe the bug** 14 | 15 | A clear and concise description of what the bug is. Include what is current behavior and what are you expecting. Add screenshots if needed and error details in JSON format so it can be easy to copy and paste. 16 | 17 | **To Reproduce** 18 | 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | 24 | **Context** 25 | 26 | Node Versions: 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "👀 Feature Requested" 6 | assignees: "" 7 | --- 8 | 9 | # Prerequisites 10 | 11 | - [ ] I checked the documentation and found no answer 12 | 13 | **Is your feature request related to a problem? Please describe.** 14 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 15 | 16 | **Describe the solution you'd like** 17 | A clear and concise description of what you want to happen. 18 | 19 | **Describe alternatives you've considered** 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | -------------------------------------------------------------------------------- /src/contexts/shared/logger/logger.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Logger, Module, Provider } from "@nestjs/common"; 2 | import { LogLevel } from "@nestjs/common/services/logger.service"; 3 | import { ConfigService } from "@nestjs/config"; 4 | 5 | const loggerProvider: Provider = { 6 | provide: Logger, 7 | useFactory: (configService: ConfigService) => { 8 | const level = configService.get("LOGGER_LEVEL", "log"); 9 | const logger = new Logger(); 10 | logger.localInstance.setLogLevels?.([level]); 11 | return logger; 12 | }, 13 | inject: [ConfigService], 14 | }; 15 | 16 | @Global() 17 | @Module({ 18 | providers: [loggerProvider], 19 | exports: [loggerProvider], 20 | }) 21 | export class LoggerModule {} 22 | -------------------------------------------------------------------------------- /tests/unit/app/health/api/health.controller.test.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "@nestjs/common"; 2 | 3 | import { createMock, Mock } from "@/tests/utils/mock"; 4 | 5 | import { HealthController } from "@/app/health/api/health.controller"; 6 | 7 | describe("HealthController", () => { 8 | let healthController: HealthController; 9 | let logger: Mock; 10 | 11 | beforeEach(() => { 12 | logger = createMock(); 13 | healthController = new HealthController(logger); 14 | }); 15 | 16 | describe("run", () => { 17 | it("should return is healthy", () => { 18 | expect(healthController.run()).toEqual({ status: "ok" }); 19 | expect(logger.log).toHaveBeenCalledTimes(1); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/swcrc", 3 | "sourceMaps": true, 4 | "jsc": { 5 | "parser": { 6 | "syntax": "typescript", 7 | "decorators": true, 8 | "dynamicImport": true 9 | }, 10 | "transform": { 11 | "legacyDecorator": true, 12 | "decoratorMetadata": true 13 | }, 14 | "baseUrl": "./", 15 | "paths": { 16 | "@/src/*": ["src/*"], 17 | "@/app/*": ["src/app/*"], 18 | "@/contexts/*": ["src/contexts/*"], 19 | "@/shared/*": ["src/contexts/shared/*"], 20 | "@/tests/*": ["tests/*"] 21 | }, 22 | "target": "esnext" 23 | }, 24 | "module": { 25 | "type": "es6", 26 | "resolveFully": true 27 | }, 28 | "minify": false 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/stale-issues-and-prs.yml: -------------------------------------------------------------------------------- 1 | name: '⌛ Close stale issues and PRs' 2 | 3 | on: 4 | schedule: 5 | - cron: '30 1 * * *' 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/stale@v9 12 | with: 13 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' 14 | stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' 15 | days-before-issue-stale: 30 16 | days-before-pr-stale: 30 17 | days-before-issue-close: 5 18 | days-before-pr-close: 5 19 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## 👏 Contributing 2 | 3 | As a contributor, here are the guidelines you should follow: 4 | 5 | - [👔 Code of Conduct](CODE_OF_CONDUCT.md) 6 | - [⭐️ Steps](#-steps) 7 | - [💻️ Developing](../README.md#-developing) 8 | 9 | --- 10 | 11 | ## ⭐️ Steps 12 | 13 | 1. Use the issue tracker to make sure the feature request or bug has not been already reported 🔎. 14 | 2. Submit an issue describing your proposed change to the repo 💡. 15 | 3. The repo owner will respond to your issue as soon as we can 💪. 16 | 4. If your proposal change is accepted, fork the repo, develop and test your code changes 🤝. 17 | 5. Ensure that your code adheres to the existing style in the code 💅🏻. 18 | 6. Title your pull request following [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) styling 🪄. 19 | 20 | --- 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "module": "esnext", 6 | "target": "esnext", 7 | "moduleResolution": "Bundler", 8 | "esModuleInterop": true, 9 | "strict": true, 10 | "outDir": "dist", 11 | "sourceMap": true, 12 | "inlineSources": true, 13 | "emitDecoratorMetadata": true, 14 | "experimentalDecorators": true, 15 | "allowSyntheticDefaultImports": true, 16 | "skipLibCheck": true, 17 | "resolveJsonModule": true, 18 | "paths": { 19 | "@/src/*": ["src/*"], 20 | "@/app/*": ["src/app/*"], 21 | "@/contexts/*": ["src/contexts/*"], 22 | "@/shared/*": ["src/contexts/shared/*"], 23 | "@/tests/*": ["tests/*"] 24 | }, 25 | "types": ["vitest/globals"] 26 | }, 27 | "exclude": ["node_modules", "dist"] 28 | } 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Proposed changes 2 | 3 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 4 | 5 | ## Checklist 6 | 7 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 8 | 9 | - [ ] I have added tests that prove my fix is effective or that my feature works 10 | - [ ] I have added necessary documentation (if appropriate) 11 | - [ ] Any dependent changes have been merged and published in downstream modules 12 | 13 | ## Further comments 14 | 15 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 16 | -------------------------------------------------------------------------------- /.github/workflows/pr-size-labeler.yml: -------------------------------------------------------------------------------- 1 | name: '🏷️ PR Size Labeler' 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | jobs: 8 | labeler: 9 | runs-on: ubuntu-latest 10 | name: Label the PR size 11 | steps: 12 | - uses: codelytv/pr-size-labeler@v1 13 | with: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | xs_label: '🤩 size/xs' 16 | xs_max_size: '10' 17 | s_label: '🥳 size/s' 18 | s_max_size: '100' 19 | m_label: '😎 size/m' 20 | m_max_size: '500' 21 | l_label: '😖 size/l' 22 | l_max_size: '1000' 23 | xl_label: '🤯 size/xl' 24 | fail_if_xl: 'false' 25 | message_if_xl: > 26 | This PR exceeds the recommended size of 1000 lines. 27 | Please make sure you are NOT addressing multiple issues with one PR. 28 | Note this PR might be rejected due to its size. 29 | files_to_ignore: 'pnpm-lock.yaml *.lock docs/*' 30 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: '👋 Greetings' 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened] 6 | issues: 7 | types: [opened, reopened] 8 | 9 | jobs: 10 | greeting: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | issues: write 14 | pull-requests: write 15 | steps: 16 | - uses: actions/first-interaction@v1 17 | with: 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | issue-message: | 20 | Hey @${{github.actor}} 👋! 21 | 22 | Thank you for opening your first issue here! ♥️ 23 | If you are reporting a bug 🐞, please make sure to include steps on how to reproduce it. 24 | 25 | We will take it a look as soon as we can 💪 26 | pr-message: | 27 | Hey @${{github.actor}} 👋! 28 | 29 | Thank you for being here and helping this project to grow 🚀 30 | We will review it as soon as we can :D 31 | 32 | Please check out our contributing guidelines in the meantime 📃 33 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | name: nestjs-service-template 3 | description: Template for new services based on NestJS with the Best Practices and Ready for Production 4 | homepage: github.com/AlbertHernandez/nestjs-service-template 5 | topics: nodejs, template, typescript, nestjs-template, nestjs 6 | has_wiki: false 7 | private: false 8 | has_issues: true 9 | has_projects: false 10 | default_branch: main 11 | allow_squash_merge: true 12 | allow_merge_commit: false 13 | allow_rebase_merge: false 14 | delete_branch_on_merge: true 15 | enable_automated_security_fixes: true 16 | enable_vulnerability_alerts: true 17 | branches: 18 | - name: main 19 | protection: 20 | required_pull_request_reviews: 21 | required_approving_review_count: 1 22 | dismiss_stale_reviews: true 23 | require_code_owner_reviews: true 24 | required_status_checks: 25 | strict: true 26 | contexts: [] 27 | enforce_admins: false 28 | required_linear_history: true 29 | restrictions: null 30 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "@nestjs/common"; 2 | import { ConfigService } from "@nestjs/config"; 3 | import { NestFactory } from "@nestjs/core"; 4 | import { 5 | FastifyAdapter, 6 | NestFastifyApplication, 7 | } from "@nestjs/platform-fastify"; 8 | 9 | import { AppModule } from "@/app/app.module"; 10 | 11 | async function bootstrap() { 12 | const app = await NestFactory.create( 13 | AppModule, 14 | new FastifyAdapter(), 15 | ); 16 | 17 | app.setGlobalPrefix("api"); 18 | const configService = app.get(ConfigService); 19 | const port = configService.get("PORT", "3000"); 20 | 21 | await app.listen(port, "0.0.0.0"); 22 | 23 | const logger = app.get(Logger); 24 | logger.log(`App is ready and listening on port ${port} 🚀`); 25 | } 26 | 27 | bootstrap().catch(handleError); 28 | 29 | function handleError(error: unknown) { 30 | // eslint-disable-next-line no-console 31 | console.error(error); 32 | // eslint-disable-next-line unicorn/no-process-exit 33 | process.exit(1); 34 | } 35 | 36 | process.on("uncaughtException", handleError); 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Albert Hernandez 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /tests/e2e/health.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FastifyAdapter, 3 | NestFastifyApplication, 4 | } from "@nestjs/platform-fastify"; 5 | import { Test, TestingModule } from "@nestjs/testing"; 6 | import * as nock from "nock"; 7 | import request from "supertest"; 8 | 9 | import { AppModule } from "@/app/app.module"; 10 | 11 | describe("Health", () => { 12 | let app: NestFastifyApplication; 13 | 14 | beforeAll(async () => { 15 | const moduleFixture: TestingModule = await Test.createTestingModule({ 16 | imports: [AppModule], 17 | }).compile(); 18 | 19 | app = moduleFixture.createNestApplication( 20 | new FastifyAdapter(), 21 | ); 22 | await app.init(); 23 | await app.getHttpAdapter().getInstance().ready(); 24 | nock.disableNetConnect(); 25 | nock.enableNetConnect("127.0.0.1"); 26 | }); 27 | 28 | afterEach(() => { 29 | nock.cleanAll(); 30 | }); 31 | 32 | afterAll(async () => { 33 | await app.close(); 34 | nock.enableNetConnect(); 35 | }); 36 | 37 | it("/GET health", async () => { 38 | const response = await request(app.getHttpServer()).get("/health"); 39 | expect(response.status).toBe(200); 40 | expect(response.body).toEqual({ status: "ok" }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /.github/actions/setup-node/action.yml: -------------------------------------------------------------------------------- 1 | name: '⚙️ Setup node' 2 | 3 | description: 'Setup node with project version and install dependencies' 4 | 5 | inputs: 6 | version: 7 | description: 'Node version to use' 8 | required: false 9 | npm_token: 10 | description: 'NPM Token' 11 | required: false 12 | default: '' 13 | 14 | runs: 15 | using: "composite" 16 | steps: 17 | - name: Authenticate npm 🔑 18 | shell: bash 19 | run: echo "//registry.npmjs.org/:_authToken=${{ inputs.npm_token }}" > ~/.npmrc 20 | - name: Install pnpm 📦 21 | uses: pnpm/action-setup@v4 22 | - name: Cache Dependencies ⌛️ 23 | uses: actions/cache@v4 24 | id: cache-node-modules 25 | with: 26 | path: | 27 | ~/.pnpm-store 28 | node_modules 29 | key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('.github/actions/setup-node/action.yml') }}-node-${{ hashFiles('.nvmrc') }}-${{ inputs.version }} 30 | - name: Setup Node ⚙️ 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{ inputs.version }} 34 | node-version-file: '.nvmrc' 35 | cache: 'pnpm' 36 | - name: Install dependencies 📥 37 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 38 | shell: bash 39 | run: pnpm install --frozen-lockfile 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22-alpine3.20 AS base 2 | 3 | ENV DIR /app 4 | WORKDIR $DIR 5 | ARG NPM_TOKEN 6 | 7 | FROM base AS dev 8 | 9 | ENV NODE_ENV=development 10 | ENV CI=true 11 | 12 | RUN npm install -g pnpm@9.14.2 13 | 14 | COPY package.json pnpm-lock.yaml ./ 15 | 16 | RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ".npmrc" && \ 17 | pnpm install --frozen-lockfile && \ 18 | rm -f .npmrc 19 | 20 | COPY tsconfig*.json . 21 | COPY .swcrc . 22 | COPY nest-cli.json . 23 | COPY src src 24 | 25 | EXPOSE $PORT 26 | CMD ["node", "--run", "dev"] 27 | 28 | FROM base AS build 29 | 30 | ENV CI=true 31 | 32 | RUN apk update && apk add --no-cache dumb-init=1.2.5-r3 && npm install -g pnpm@9.14.2 33 | 34 | COPY package.json pnpm-lock.yaml ./ 35 | RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ".npmrc" && \ 36 | pnpm install --frozen-lockfile && \ 37 | rm -f .npmrc 38 | 39 | COPY tsconfig*.json . 40 | COPY .swcrc . 41 | COPY nest-cli.json . 42 | COPY src src 43 | 44 | RUN node --run build && \ 45 | pnpm prune --prod 46 | 47 | FROM base AS production 48 | 49 | ENV NODE_ENV=production 50 | ENV USER=node 51 | 52 | COPY --from=build /usr/bin/dumb-init /usr/bin/dumb-init 53 | COPY --from=build $DIR/package.json . 54 | COPY --from=build $DIR/pnpm-lock.yaml . 55 | COPY --from=build $DIR/node_modules node_modules 56 | COPY --from=build $DIR/dist dist 57 | 58 | USER $USER 59 | EXPOSE $PORT 60 | CMD ["dumb-init", "node", "dist/main.js"] 61 | -------------------------------------------------------------------------------- /scripts/check_typos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | check_typos_installed() { 4 | if ! command -v typos >/dev/null 2>&1; then 5 | echo "Typos CLI tool is not installed, aborting typo check." 6 | echo "If you want to install it, you can run 'brew install typos-cli'" 7 | exit 0 # We don't want to fail the build if the tool is not installed 8 | fi 9 | } 10 | 11 | get_files() { 12 | if [ "$#" -eq 0 ]; then 13 | echo "." 14 | else 15 | echo "$@" 16 | fi 17 | } 18 | 19 | filter_files() { 20 | IGNORE_EXTENSIONS=("png" "snap" "jpg") 21 | 22 | local files="$1" 23 | local filtered="" 24 | for file in $files; do 25 | ignore_file=false 26 | for ext in "${IGNORE_EXTENSIONS[@]}"; do 27 | if [[ $file == *.$ext ]]; then 28 | ignore_file=true 29 | break 30 | fi 31 | done 32 | if [ "$ignore_file" = false ]; then 33 | filtered="$filtered $file" 34 | fi 35 | done 36 | echo "$filtered" 37 | } 38 | 39 | convert_to_relative_paths() { 40 | local files="$1" 41 | local current_dir=$(pwd) 42 | local relative="" 43 | for file in $files; do 44 | relative="$relative ${file#$current_dir/}" 45 | done 46 | echo "$relative" 47 | } 48 | 49 | check_typos_installed 50 | absolute_path_files=$(get_files "$@") 51 | filtered_files=$(filter_files "$absolute_path_files") 52 | relative_files=$(convert_to_relative_paths "$filtered_files") 53 | typos $relative_files 54 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: '💅 Lint PR Title' 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, synchronize, reopened] 6 | 7 | permissions: 8 | pull-requests: write 9 | 10 | jobs: 11 | main: 12 | name: Validate PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v5 16 | id: lint_pr_title 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | with: 20 | ignoreLabels: | 21 | bot 22 | autorelease: pending 23 | - uses: marocchino/sticky-pull-request-comment@v2 24 | if: always() && (steps.lint_pr_title.outputs.error_message != null) 25 | with: 26 | header: pr-title-lint-error 27 | message: | 28 | Hey mate 👋. Thank you for opening this Pull Request 🤘. It is really awesome to see this contribution 🚀 29 | 30 | 🔎 When working with this project we are requesting to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted 🥶. 31 | 32 | 👇 Bellow you can find details about what failed: 33 | 34 | ``` 35 | ${{ steps.lint_pr_title.outputs.error_message }} 36 | ``` 37 | 38 | - if: ${{ steps.lint_pr_title.outputs.error_message == null }} 39 | uses: marocchino/sticky-pull-request-comment@v2 40 | with: 41 | header: pr-title-lint-error 42 | delete: true 43 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | my-service-production: 3 | container_name: my-service-production 4 | build: 5 | target: production 6 | context: . 7 | args: 8 | - PORT=${PORT:-3000} 9 | ports: 10 | - "${PORT:-3000}:${PORT:-3000}" 11 | deploy: 12 | resources: 13 | limits: 14 | cpus: "1" 15 | memory: "512m" 16 | reservations: 17 | cpus: "0.25" 18 | memory: "256m" 19 | 20 | my-service-dev: 21 | container_name: my-service-dev 22 | restart: unless-stopped 23 | env_file: .env 24 | build: 25 | target: dev 26 | context: . 27 | args: 28 | - PORT=${PORT:-3000} 29 | ports: 30 | - "${PORT:-3000}:${PORT:-3000}" 31 | - "9229:9229" 32 | volumes: 33 | - ./src:/app/src 34 | deploy: 35 | resources: 36 | limits: 37 | cpus: "1" 38 | memory: "512m" 39 | reservations: 40 | cpus: "0.25" 41 | memory: "256m" 42 | 43 | k6: 44 | image: ghcr.io/grafana/xk6-dashboard:0.7.2 45 | container_name: k6 46 | volumes: 47 | - ./tests/performance:/tests/performance 48 | - ./k6-results:/home/k6 49 | ports: 50 | - "5665:5665" 51 | environment: 52 | BASE_URL: "http://host.docker.internal:3000" 53 | K6_WEB_DASHBOARD_EXPORT: "report.html" 54 | K6_WEB_DASHBOARD_PERIOD: "1s" 55 | K6_WEB_DASHBOARD_OPEN: "true" 56 | command: [ 57 | "run", 58 | "--out", "web-dashboard", 59 | "/tests/performance/contexts/users/get-users.mjs" 60 | ] 61 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | - name: 👀 Feature Requested 2 | description: Request for a feature 3 | color: 07D90A 4 | - name: ignore-for-release 5 | description: Ignore pull request for a new release 6 | color: 9C28FC 7 | - name: todo 8 | description: Action we need to perform at some moment 9 | color: 82FC28 10 | - name: 💻 Source 11 | description: Indicates the scope is related to the own service logic 12 | color: FDC720 13 | - name: 🧪 Tests 14 | description: Indicates the scope is related to the tests 15 | color: 088E26 16 | - name: ⚙️ Configuration 17 | description: Indicates the scope is related to the configuration 18 | color: BDBDBD 19 | - name: 🐳 Build 20 | description: Indicates the change is related to the build 21 | color: 0FD4DA 22 | - name: 🚀 CI/CD 23 | description: Indicates the change is related to CI/CD workflows 24 | color: FF4D4D 25 | - name: 🏠 Github Configuration 26 | description: Indicates the change is related to github settings 27 | color: 555555 28 | - name: 🚀 Feature 29 | description: Feature added in the PR 30 | color: F10505 31 | - name: 🐞 Bug 32 | description: Bug identified 33 | color: F4D03F 34 | - name: 🕵🏻 Fix 35 | description: Fix applied in the PR 36 | color: F4D03F 37 | - name: ⚠️ Breaking Change 38 | description: Breaking change in the PR 39 | color: F1F800 40 | - name: 📦 Dependencies 41 | description: Pull requests that update a dependency file 42 | color: 95A5A6 43 | - name: 📝 Documentation 44 | description: Improvements or additions to documentation 45 | color: 228AFF 46 | - name: 🤦‍ Duplicate 47 | description: This issue or pull request already exists 48 | color: 17202A 49 | - name: 🤩 size/xs 50 | description: Pull request size XS 51 | color: 27AE60 52 | - name: 🥳 size/s 53 | description: Pull request size S 54 | color: 2ECC71 55 | - name: 😎 size/m 56 | description: Pull request size M 57 | color: F1C40F 58 | - name: 😖 size/l 59 | description: Pull request size L 60 | color: F39C12 61 | - name: 🤯 size/xl 62 | description: Pull request size XL 63 | color: E67E22 64 | -------------------------------------------------------------------------------- /.github/pr-scope-labeler.yml: -------------------------------------------------------------------------------- 1 | 💻 Source: 2 | - changed-files: 3 | - any-glob-to-any-file: 4 | - src/** 5 | 6 | 🧪 Tests: 7 | - changed-files: 8 | - any-glob-to-any-file: 9 | - tests/** 10 | - .env.test 11 | - scripts/calculate-global-test-coverage.ts 12 | - vitest.config.**.ts 13 | - create-vitest-test-config.ts 14 | - .nycrc.json 15 | 16 | 📝 Documentation: 17 | - changed-files: 18 | - any-glob-to-any-file: 19 | - docs/** 20 | - README.md 21 | - images/** 22 | 23 | 🐳 Build: 24 | - changed-files: 25 | - any-glob-to-any-file: 26 | - .dockerignore 27 | - Dockerfile 28 | - docker-compose.yml 29 | - .nvmrc 30 | - .swcrc 31 | - tsconfig.json 32 | - tsconfig.prod.json 33 | 34 | ⚙️ Configuration: 35 | - changed-files: 36 | - any-glob-to-any-file: 37 | - .dockerignore 38 | - .editorconfig 39 | - .env.example 40 | - .eslintignore 41 | - .eslintrc 42 | - .gitignore 43 | - .npmignore 44 | - .npmrc 45 | - .nvmrc 46 | - .prettierignore 47 | - .swcrc 48 | - .yamllint.yml 49 | - lint-staged.config.mjs 50 | - vitest.config.**.ts 51 | - create-vitest-test-config.ts 52 | - commitlint.config.ts 53 | - nest-cli.json 54 | - .nycrc.json 55 | - prettier.config.mjs 56 | - tsconfig.json 57 | - tsconfig.prod.json 58 | 59 | 📦 Dependencies: 60 | - changed-files: 61 | - any-glob-to-any-file: 62 | - package.json 63 | - pnpm-lock.yaml 64 | 65 | 🚀 CI/CD: 66 | - changed-files: 67 | - any-glob-to-any-file: 68 | - .github/workflows/** 69 | - .github/dependabot.yml 70 | - .github/pr-scope-labeler.yml 71 | - .husky/** 72 | 73 | 🏠 Github Configuration: 74 | - changed-files: 75 | - any-glob-to-any-file: 76 | - .github/ISSUE_TEMPLATE/** 77 | - .github/CODEOWNERS 78 | - .github/labels.yml 79 | - .github/PULL_REQUEST_TEMPLATE.md 80 | -------------------------------------------------------------------------------- /.github/workflows/docker-size.yml: -------------------------------------------------------------------------------- 1 | name: '🐳 Docker size' 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | permissions: 8 | pull-requests: write 9 | 10 | jobs: 11 | calculate-base: 12 | runs-on: ubuntu-latest 13 | outputs: 14 | image_size: ${{ steps.docker-base.outputs.image_size }} 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | ref: ${{ github.base_ref }} 19 | - name: Get commit short hash 20 | id: commit 21 | run: | 22 | short=$(git rev-parse --short HEAD) 23 | echo "short=$short" >> "$GITHUB_OUTPUT" 24 | - name: 📦 Cache docker image for commit ${{ steps.commit.outputs.short }} 25 | uses: actions/cache@v4 26 | with: 27 | path: base-docker-image.txt 28 | key: base-docker-image-os-${{ runner.os }}-commit-${{ steps.commit.outputs.short }} 29 | - name: 🐳 Calculate docker image size in ${{ github.base_ref }} 30 | id: docker-base 31 | run: | 32 | if [ -f base-docker-image.txt ]; then 33 | echo "Getting docker image from cache" 34 | image_size=$( base-docker-image.txt 41 | echo "image_size=$image_size" >> "$GITHUB_OUTPUT" 42 | calculate-head: 43 | runs-on: ubuntu-latest 44 | outputs: 45 | image_size: ${{ steps.docker-head.outputs.image_size }} 46 | steps: 47 | - uses: actions/checkout@v4 48 | with: 49 | ref: ${{ github.head_ref }} 50 | - name: 🐳 Calculate docker image size in ${{ github.head_ref }} 51 | id: docker-head 52 | run: | 53 | docker build . -t service 54 | image_size=$(docker images service | awk 'NR==2 {print $NF}') 55 | echo "image_size=$image_size" >> "$GITHUB_OUTPUT" 56 | write-comment: 57 | runs-on: ubuntu-latest 58 | needs: [calculate-base, calculate-head] 59 | steps: 60 | - uses: marocchino/sticky-pull-request-comment@v2 61 | env: 62 | BASE_DOCKER_IMAGE_SIZE: ${{needs.calculate-base.outputs.image_size}} 63 | HEAD_DOCKER_IMAGE_SIZE: ${{needs.calculate-head.outputs.image_size}} 64 | with: 65 | header: 66 | message: | 67 | ## 🐳 Docker Metrics 🐳 68 | 69 | * Size of the Docker Image in the base (${{ github.base_ref }}): **${{ env.BASE_DOCKER_IMAGE_SIZE }}** 70 | * Size of the Docker Image in this branch (${{ github.head_ref }}): **${{ env.HEAD_DOCKER_IMAGE_SIZE }}** 71 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "project": "tsconfig.json", 6 | "sourceType": "module", 7 | "ecmaVersion": 2022 8 | }, 9 | "plugins": ["@typescript-eslint", "simple-import-sort"], 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/strict-type-checked", 13 | "plugin:@typescript-eslint/stylistic-type-checked", 14 | "plugin:prettier/recommended", 15 | "plugin:unicorn/recommended", 16 | "plugin:node/recommended" 17 | ], 18 | "rules": { 19 | "simple-import-sort/imports": "error", 20 | "simple-import-sort/exports": "error", 21 | "unicorn/prefer-module": "off", 22 | "unicorn/prefer-top-level-await": "off", 23 | "unicorn/prevent-abbreviations": "off", 24 | "no-console": "warn", 25 | "node/no-missing-import": "off", 26 | "node/no-unsupported-features/es-syntax": [ 27 | "error", 28 | { "ignores": ["modules"] } 29 | ], 30 | "node/no-unpublished-import": "off", 31 | "no-process-exit": "off", 32 | "@typescript-eslint/restrict-template-expressions": [ 33 | "error", 34 | { "allowNumber": true } 35 | ] 36 | }, 37 | "overrides": [ 38 | { 39 | "files": ["*.ts"], 40 | "rules": { 41 | "simple-import-sort/imports": [ 42 | "error", 43 | { 44 | "groups": [ 45 | [ 46 | "^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)" 47 | ], 48 | ["^node:.*\\u0000$", "^@?\\w.*\\u0000$", "^[^.].*\\u0000$", "^\\..*\\u0000$"], 49 | ["^\\u0000"], 50 | ["^node:"], 51 | ["^@?\\w"], 52 | ["^@/tests(/.*|$)"], 53 | ["^@/src(/.*|$)"], 54 | ["^@/app(/.*|$)"], 55 | ["^@/shared(/.*|$)"], 56 | ["^@/contexts(/.*|$)"], 57 | ["^"], 58 | ["^\\."] 59 | ] 60 | } 61 | ] 62 | } 63 | }, 64 | { 65 | "files": ["*.js", "*.mjs", "*.cjs"], 66 | "extends": ["plugin:@typescript-eslint/disable-type-checked"] 67 | }, 68 | { 69 | "files": ["*.module.ts"], 70 | "rules": { 71 | "@typescript-eslint/no-extraneous-class": "off" 72 | } 73 | }, 74 | { 75 | "files": ["scripts/**"], 76 | "rules": { 77 | "no-console": "off" 78 | } 79 | }, 80 | { 81 | "files": ["tests/**"], 82 | "plugins": ["vitest"], 83 | "extends": ["plugin:vitest/recommended"], 84 | "rules": { 85 | "@typescript-eslint/unbound-method": "off", 86 | "vitest/expect-expect": "off", 87 | "vitest/no-standalone-expect": "off" 88 | } 89 | }, 90 | { 91 | "files": ["tests/performance/**"], 92 | "rules": { 93 | "unicorn/numeric-separators-style": "off", 94 | "unicorn/no-anonymous-default-export": "off", 95 | "@typescript-eslint/no-unsafe-call": "off", 96 | "@typescript-eslint/no-unsafe-assignment": "off", 97 | "@typescript-eslint/no-unsafe-member-access": "off", 98 | "no-undef": "off" 99 | } 100 | } 101 | ], 102 | "env": { 103 | "node": true 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-service-template", 3 | "version": "1.0.0", 4 | "description": "Template for new nestjs services", 5 | "author": "alberthernandezdev@gmail.com", 6 | "license": "MIT", 7 | "type": "module", 8 | "bugs": { 9 | "url": "https://github.com/AlbertHernandez/nestjs-service-template/issues" 10 | }, 11 | "homepage": "https://github.com/AlbertHernandez/nestjs-service-template#readme", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/AlbertHernandez/nestjs-service-template.git" 15 | }, 16 | "keywords": [ 17 | "typescript", 18 | "nestjs", 19 | "template" 20 | ], 21 | "engines": { 22 | "node": ">=22.x", 23 | "pnpm": ">=9.x" 24 | }, 25 | "packageManager": "pnpm@9.14.2", 26 | "main": "dist/main.js", 27 | "scripts": { 28 | "build": "node --run build:clean && nest build --path tsconfig.prod.json", 29 | "start": "node dist/main.js", 30 | "dev": "nest start --watch --debug=0.0.0.0:9229", 31 | "test": "rimraf coverage .nyc_output && concurrently 'node --run test:unit' 'node --run test:e2e' && node --run calculate-global-test-coverage", 32 | "test:unit": "vitest run --coverage --config vitest.config.unit.ts", 33 | "test:e2e": "vitest run --coverage --config ./vitest.config.e2e.ts", 34 | "test:performance": "k6 run tests/performance/contexts/users/get-users.mjs", 35 | "calculate-global-test-coverage": "tsx scripts/calculate-global-test-coverage.ts", 36 | "prepare": "[ -f .husky/install.mjs ] && node .husky/install.mjs || true", 37 | "lint": "eslint --ignore-path .gitignore . --ext .js,.mjs,cjs,.ts,.mts", 38 | "lint:fix": "eslint --ignore-path .gitignore . --ext .js,.mjs,cjs,.ts,.mts --fix", 39 | "lint:file": "eslint --ignore-path .gitignore", 40 | "lint:yaml": "chmod +x scripts/lint_yaml.sh && ./scripts/lint_yaml.sh", 41 | "build:clean": "rimraf dist; exit 0", 42 | "typos": "chmod +x scripts/check_typos.sh && ./scripts/check_typos.sh" 43 | }, 44 | "dependencies": { 45 | "@nestjs/cli": "^10.4.5", 46 | "@nestjs/common": "^10.4.16", 47 | "@nestjs/config": "^3.3.0", 48 | "@nestjs/core": "^10.4.6", 49 | "@nestjs/platform-fastify": "^10.4.6", 50 | "reflect-metadata": "^0.2.2", 51 | "rxjs": "^7.8.2" 52 | }, 53 | "devDependencies": { 54 | "@commitlint/cli": "^19.8.1", 55 | "@commitlint/config-conventional": "^19.5.0", 56 | "@commitlint/types": "^19.8.1", 57 | "@nestjs/schematics": "^10.2.3", 58 | "@nestjs/testing": "^10.4.6", 59 | "@swc/cli": "^0.4.0", 60 | "@swc/core": "^1.13.5", 61 | "@types/fs-extra": "^11.0.4", 62 | "@types/node": "^22.8.7", 63 | "@types/supertest": "^6.0.3", 64 | "@typescript-eslint/eslint-plugin": "^7.18.0", 65 | "@typescript-eslint/parser": "^7.18.0", 66 | "@vitest/coverage-istanbul": "1.3.1", 67 | "concurrently": "^9.2.1", 68 | "eslint": "^8.57.1", 69 | "eslint-config-prettier": "^9.0.0", 70 | "eslint-plugin-node": "^11.1.0", 71 | "eslint-plugin-prettier": "^5.5.4", 72 | "eslint-plugin-simple-import-sort": "^12.1.1", 73 | "eslint-plugin-unicorn": "^56.0.0", 74 | "eslint-plugin-vitest": "^0.4.1", 75 | "fs-extra": "^11.3.1", 76 | "husky": "^9.1.6", 77 | "lint-staged": "^15.2.10", 78 | "nock": "^13.5.5", 79 | "nyc": "^17.1.0", 80 | "prettier": "^3.6.2", 81 | "rimraf": "^6.0.1", 82 | "source-map-support": "^0.5.21", 83 | "supertest": "^7.1.4", 84 | "ts-loader": "^9.5.2", 85 | "ts-node": "^10.9.2", 86 | "tsconfig-paths": "^4.2.0", 87 | "tsx": "^4.19.2", 88 | "typescript": "^5.9.2", 89 | "unplugin-swc": "^1.5.8", 90 | "vite": "^5.4.21", 91 | "vitest": "^1.6.1", 92 | "vitest-mock-extended": "^1.3.1" 93 | }, 94 | "optionalDependencies": { 95 | "@rollup/rollup-linux-x64-gnu": "^4.24.3", 96 | "@swc/core-linux-arm64-musl": "^1.13.21", 97 | "@swc/core-linux-x64-gnu": "^1.13.3", 98 | "@swc/core-linux-x64-musl": "^1.13.3" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 |

⭐ NestJS Service Template ⭐

6 | 7 |

8 | Template for new services based on NestJS with the Best Practices and Ready for Production 9 |

10 | 11 |

12 | nodejs 13 | node 14 | typescript 15 | pnpm 16 | fastify 17 | swc 18 | swc 19 | docker 20 |

21 | 22 | ## 👀 Motivation 23 | 24 | When we start creating some new service based on NestJS most often we just use the Nest cli for starting a new service that already give us some convention and structure for our project. This is a good starting point however I was missing a couple of interesting things that almost all services should have to be ready to deploy to production like fully dockerized, ensuring coding conventions... 25 | 26 | For this reason I created this custom template for new services based on this framework, with everything I would like to have to start developing a service with the best practices but with a simple file structure so later developers can change to implement their logic. 27 | 28 | Here we are not providing any specific architecture like hexagonal architecture or others, this is like a simple template where later we can customize and create the architecture we need. 29 | 30 | ## 🌟 What is including this template? 31 | 32 | 1. 🐳 Fully dockerized service ready for development and production environments with the best practices for docker, trying to provide a performance and small image just with the code we really need in your environments. 33 | 2. 👷 Use [SWC](https://swc.rs/) for compiling and running the tests of the service. As commented in the own [NestJS docs](https://docs.nestjs.com/recipes/swc), this is approximately x20 times faster than default typescript compiler that is the one that comes by default in NestJS. 34 | 3. ⚡️ Use [Fastify](https://fastify.dev/) as Web Framework. By default, [NestJS is using Express](https://docs.nestjs.com/techniques/performance) because is the most widely-used framework for working with NodeJS, however, this does not imply is the one is going to give us the most performance. Also, NestJS is fully compatible with Fastify, so we are providing this integration by default. You can check [here](https://github.com/fastify/benchmarks#benchmarks) comparison between different web frameworks. 35 | 4. 🐶 Integration with [husky](https://typicode.github.io/husky/) to ensure we have good quality and conventions while we are developing like: 36 | - 💅 Running the linter over the files that have been changed 37 | - 💬 Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) to ensure our commits have a convention. 38 | - ✅ Run the tests automatically. 39 | - ⚙️ Check our project does not have type errors with Typescript. 40 | - 🙊 Check typos to ensure we don't have grammar mistakes. 41 | 5. 🗂️ Separate tests over production code. By default, NestJS is combining in the same folder, the `src`, the unit tests and the code we are developing for production. This is something I personally don't like so here I am separating this and having a dedicated folder for the unit tests. 42 | 6. 🧪 Testing with [Vitest](https://vitest.dev/) and [supertest](https://github.com/ladjs/supertest) for unit and e2e tests. 43 | 7. 🏎️ Performance testing using [k6](https://grafana.com/oss/k6/). 44 | 8. 🤜🤛 Combine unit and e2e test coverage. In the services we may have both type of tests, unit and e2e tests, and usually we would like to see what is the combined test coverage, so we can see the full picture. 45 | 9. 📌 Custom path aliases, where you can define your own paths (you will be able to use imports like `@/shared/logger` instead of `../../../src/shared/logger`). 46 | 10. 🚀 CI/CD using GitHub Actions, helping ensure a good quality of our code and providing useful insights about dependencies, security vulnerabilities and others. 47 | 11. 🐦‍🔥 Usage of ESModules instead of CommonJS, which is the standard in JavaScript. 48 | 12. 📦 Use of [pnpm](https://pnpm.io/) as package manager, which is faster and more efficient than npm or yarn. 49 | 50 | ## 🤩 Other templates 51 | 52 | Are you thinking in start new projects in other frameworks or create a super fancy library? If you like this template there are others base on this you can check: 53 | 54 | - [Template for new Typescript Libraries](https://github.com/AlbertHernandez/typescript-library-template) 55 | - [Template for new Typescript Express Services](https://github.com/AlbertHernandez/express-typescript-service-template) 56 | - [Template for new GitHub Actions based on NodeJS](https://github.com/AlbertHernandez/github-action-nodejs-template) 57 | 58 | ## 🧑‍💻 Developing 59 | 60 | First, we will need to create our .env file, we can create a copy from the example one: 61 | 62 | ```bash 63 | cp .env.example .env 64 | ``` 65 | 66 | Now, we will need to install `pnpm` globally, you can do it running: 67 | 68 | ```bash 69 | npm install -g pnpm@9.14.2 70 | ``` 71 | 72 | The project is fully dockerized 🐳, if we want to start the app in **development mode**, we just need to run: 73 | 74 | ```bash 75 | docker-compose up -d my-service-dev 76 | ``` 77 | 78 | This development mode will work with **hot-reload** and expose a **debug port**, port `9229`, so later we can connect to it from our editor. 79 | 80 | Now, you should be able to start debugging configuring using your IDE. For example, if you are using vscode, you can create a `.vscode/launch.json` file with the following configuration: 81 | 82 | ```json 83 | { 84 | "version": "0.1.0", 85 | "configurations": [ 86 | { 87 | "type": "node", 88 | "request": "attach", 89 | "name": "Attach to docker", 90 | "restart": true, 91 | "port": 9229, 92 | "remoteRoot": "/app" 93 | } 94 | ] 95 | } 96 | ``` 97 | 98 | Also, if you want to run the **production mode**, you can run: 99 | 100 | ```bash 101 | docker-compose up -d my-service-production 102 | ``` 103 | 104 | This service is providing just a health endpoint which you can call to verify the service is working as expected: 105 | 106 | ```bash 107 | curl --request GET \ 108 | --url http://localhost:3000/health 109 | ``` 110 | 111 | If you want to stop developing, you can stop the service running: 112 | 113 | ```bash 114 | docker-compose down 115 | ``` 116 | 117 | ## ⚙️ Building 118 | 119 | ```bash 120 | node --run build 121 | ``` 122 | 123 | ## ✅ Testing 124 | 125 | The service provide different scripts for running the tests, to run all of them you can run: 126 | 127 | ```bash 128 | node --run test 129 | ``` 130 | 131 | If you are interested just in the unit tests, you can run: 132 | 133 | ```bash 134 | node --run test:unit 135 | ``` 136 | 137 | Or if you want e2e tests, you can execute: 138 | 139 | ```bash 140 | node --run test:e2e 141 | ``` 142 | 143 | We also have performance testing with [k6](https://k6.io/), if you want to run it via docker, execute: 144 | 145 | ```bash 146 | docker-compose up k6 147 | ``` 148 | 149 | Or if you want to run it from your machine, execute: 150 | 151 | ```bash 152 | brew install k6 153 | node --run test:performance 154 | ``` 155 | 156 | ## 💅 Linting 157 | 158 | To run the linter you can execute: 159 | 160 | ```bash 161 | node --run lint 162 | ``` 163 | 164 | And for trying to fix lint issues automatically, you can run: 165 | 166 | ```bash 167 | node --run lint:fix 168 | ``` 169 | --------------------------------------------------------------------------------