├── .dockerignore ├── .editorconfig ├── .env.example ├── .env.test ├── .eslintignore ├── .eslintrc ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── actions │ └── setup-node │ │ └── action.yml ├── dependabot.yml ├── labels.yml ├── pr-scope-labeler.yml ├── settings.yml └── workflows │ ├── assign-me.yml │ ├── conventional-label.yml │ ├── dependabot-auto-merge.yml │ ├── dependency-review.yml │ ├── docker-size.yml │ ├── greetings.yml │ ├── lint-dockerfile.yml │ ├── lint-dotenv.yml │ ├── lint-github-action.yml │ ├── lint-pr-title.yml │ ├── lint-yaml.yml │ ├── node.yml │ ├── pr-scope-label.yml │ ├── pr-size-labeler.yml │ ├── stale-issues-and-prs.yml │ ├── sync-labels.yml │ ├── todo-to-issue.yml │ └── typos.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg ├── install.mjs ├── pre-commit └── pre-push ├── .npmignore ├── .npmrc ├── .nvmrc ├── .nycrc.json ├── .prettierignore ├── .swcrc ├── .yamllint.yml ├── Dockerfile ├── LICENSE.md ├── README.md ├── _typos.toml ├── commitlint.config.ts ├── create-vitest-test-config.ts ├── docker-compose.yml ├── docs ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── SECURITY.md ├── images └── express-and-ts.png ├── lint-staged.config.mjs ├── nodemon.json ├── package.json ├── pnpm-lock.yaml ├── prettier.config.mjs ├── scripts ├── calculate-global-test-coverage.ts ├── check_typos.sh └── lint_yaml.sh ├── src ├── app │ ├── config │ │ ├── config.ts │ │ └── load-env-vars.ts │ ├── health │ │ └── api │ │ │ ├── health-controller.ts │ │ │ └── health-router.ts │ └── server.ts ├── contexts │ ├── shared │ │ └── logger │ │ │ ├── console-logger.ts │ │ │ └── logger.ts │ └── users │ │ └── api │ │ ├── user-controller.ts │ │ └── user-router.ts └── main.ts ├── tests ├── e2e │ └── health.test.ts ├── performance │ └── contexts │ │ └── users │ │ └── get-users.mjs └── unit │ └── contexts │ └── users │ └── api │ └── user-controller.test.ts ├── tsconfig.json ├── tsconfig.prod.json ├── vitest.config.e2e.ts ├── vitest.config.ts └── vitest.config.unit.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !/package.json 3 | !/pnpm-lock.yaml 4 | !/tsconfig.prod.json 5 | !/tsconfig.json 6 | !/.swcrc 7 | !/nodemon.json 8 | !/src 9 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ENABLE_EXPERIMENTAL_COREPACK=1 2 | PORT=3000 3 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | ENABLE_EXPERIMENTAL_COREPACK=1 2 | NODE_ENV=test 3 | PORT=0 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | coverage/ 4 | -------------------------------------------------------------------------------- /.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": ["scripts/**"], 70 | "rules": { 71 | "no-console": "off" 72 | } 73 | }, 74 | { 75 | "files": ["tests/**"], 76 | "plugins": ["vitest"], 77 | "extends": ["plugin:vitest/recommended"], 78 | "rules": { 79 | "@typescript-eslint/unbound-method": "off", 80 | "vitest/expect-expect": "off", 81 | "vitest/no-standalone-expect": "off" 82 | } 83 | }, 84 | { 85 | "files": ["tests/performance/**"], 86 | "rules": { 87 | "unicorn/numeric-separators-style": "off", 88 | "unicorn/no-anonymous-default-export": "off", 89 | "@typescript-eslint/no-unsafe-call": "off", 90 | "@typescript-eslint/no-unsafe-assignment": "off", 91 | "@typescript-eslint/no-unsafe-member-access": "off", 92 | "no-undef": "off" 93 | } 94 | } 95 | ], 96 | "env": { 97 | "node": true 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @AlbertHernandez 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /.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/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 | - vitest.config.**.ts 11 | - create-vitest-test-config.ts 12 | - .env.test 13 | - scripts/calculate-global-test-coverage.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 | - commitlint.config.ts 50 | - vitest.config.**.ts 51 | - create-vitest-test-config.ts 52 | - .env.test 53 | - lint-staged.config.mjs 54 | - .nycrc.json 55 | - prettier.config.mjs 56 | - tsconfig.json 57 | - tsconfig.prod.json 58 | - nodemon.json 59 | 60 | 📦 Dependencies: 61 | - changed-files: 62 | - any-glob-to-any-file: 63 | - package.json 64 | - pnpm-lock.yaml 65 | 66 | 🚀 CI/CD: 67 | - changed-files: 68 | - any-glob-to-any-file: 69 | - .github/workflows/** 70 | - .github/dependabot.yml 71 | - .github/pr-scope-labeler.yml 72 | - .husky/** 73 | 74 | 🏠 Github Configuration: 75 | - changed-files: 76 | - any-glob-to-any-file: 77 | - .github/ISSUE_TEMPLATE/** 78 | - .github/CODEOWNERS 79 | - .github/labels.yml 80 | - .github/PULL_REQUEST_TEMPLATE.md 81 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | name: express-typescript-service-template 3 | description: Template for new services based on Express and Typescript with the Best Practices and Ready for Production 4 | homepage: github.com/AlbertHernandez/express-typescript-service-template 5 | topics: nodejs, template, typescript, nodejs-service-template, express, nodejs-express-template 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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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/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/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/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 | -------------------------------------------------------------------------------- /.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/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.1.0 13 | id: hadolint 14 | with: 15 | dockerfile: Dockerfile 16 | - name: Build dockerfile 17 | run: docker build . -t service 18 | -------------------------------------------------------------------------------- /.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-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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /.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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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.29.7 14 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | node --run build 2 | find dist/main.js 3 | node --run test 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .tmp 4 | .idea 5 | .env 6 | coverage/ 7 | .npm 8 | .vscode 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | coverage/ 3 | node_modules/ 4 | tsconfig.json 5 | tsconfig.prod.json 6 | -------------------------------------------------------------------------------- /.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 | "baseUrl": "./", 11 | "paths": { 12 | "@/src/*": ["src/*"], 13 | "@/app/*": ["src/app/*"], 14 | "@/contexts/*": ["src/contexts/*"], 15 | "@/shared/*": ["src/contexts/shared/*"], 16 | "@/tests/*": ["tests/*"] 17 | }, 18 | "target": "esnext" 19 | }, 20 | "module": { 21 | "type": "es6", 22 | "resolveFully": true 23 | }, 24 | "minify": false 25 | } 26 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 nodemon.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 src src 42 | 43 | RUN node --run build && \ 44 | pnpm prune --prod 45 | 46 | FROM base AS production 47 | 48 | ENV NODE_ENV=production 49 | ENV USER=node 50 | 51 | COPY --from=build /usr/bin/dumb-init /usr/bin/dumb-init 52 | COPY --from=build $DIR/package.json . 53 | COPY --from=build $DIR/pnpm-lock.yaml . 54 | COPY --from=build $DIR/node_modules node_modules 55 | COPY --from=build $DIR/dist dist 56 | 57 | USER $USER 58 | EXPOSE $PORT 59 | CMD ["dumb-init", "node", "dist/main.js"] 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Express Logo 3 |

4 | 5 |

⭐ Express Typescript Service Template ⭐

6 | 7 |

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

10 | 11 |

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

20 | 21 | ## 👀 Motivation 22 | 23 | Starting a new service in NodeJS can be a bit frustrating, there are a lot of things to consider if we want to have a really good starting point where later we can iterate. 24 | 25 | The main objective of this template is to provide a good base configuration for our NodeJS services that we can start using and move to production as soon as possible. 26 | 27 | ## 🌟 What is including this template? 28 | 29 | 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. 30 | 2. 👷 Use [SWC](https://swc.rs/) for compiling and running the tests of the service. 31 | 3. ⚡️ Configure [Express](https://expressjs.com/) as HTTP framework. 32 | 4. 🐶 Integration with [husky](https://typicode.github.io/husky/) to ensure we have good quality and conventions while we are developing like: 33 | - 💅 Running the linter over the files that have been changed 34 | - 💬 Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) to ensure our commits have a convention. 35 | - ✅ Run the tests automatically. 36 | - ⚙️ Check our project does not have type errors with Typescript. 37 | - 🙊 Check typos to ensure we don't have grammar mistakes. 38 | 5. 🧪 Testing with [Vitest](https://vitest.dev/) and [supertest](https://github.com/ladjs/supertest) for unit and e2e tests. 39 | 6. 🏎️ Performance testing using [k6](https://grafana.com/oss/k6/). 40 | 7. 🤜🤛 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. 41 | 8. 📌 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`). 42 | 9. 🚀 CI/CD using GitHub Actions, helping ensure a good quality of our code and providing useful insights about dependencies, security vulnerabilities and others. 43 | 10. 🐦‍🔥 Usage of ESModules instead of CommonJS, which is the standard in JavaScript. 44 | 11. 📦 Use of [pnpm](https://pnpm.io/) as package manager, which is faster and more efficient than npm or yarn. 45 | 46 | ## 🤩 Other templates 47 | 48 | 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: 49 | 50 | - [Template for new Typescript Libraries](https://github.com/AlbertHernandez/typescript-library-template) 51 | - [Template for new NestJS Services](https://github.com/AlbertHernandez/nestjs-service-template) 52 | - [Template for new GitHub Actions based on NodeJS](https://github.com/AlbertHernandez/github-action-nodejs-template) 53 | 54 | ## 🧑‍💻 Developing 55 | 56 | First, we will need to create our .env file, we can create a copy from the example one: 57 | 58 | ```bash 59 | cp .env.example .env 60 | ``` 61 | 62 | Now, we will need to install `pnpm` globally, you can do it running: 63 | 64 | ```bash 65 | npm install -g pnpm@9.14.2 66 | ``` 67 | 68 | The project is fully dockerized 🐳, if we want to start the app in **development mode**, we just need to run: 69 | 70 | ```bash 71 | docker-compose up -d my-service-dev 72 | ``` 73 | 74 | This development mode with work with **hot-reload** and exposing a **debug port**, the `9229`, so later we can connect from our editor to it. 75 | 76 | 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 config: 77 | 78 | ```json 79 | { 80 | "version": "0.1.0", 81 | "configurations": [ 82 | { 83 | "type": "node", 84 | "request": "attach", 85 | "name": "Attach to docker", 86 | "restart": true, 87 | "port": 9229, 88 | "remoteRoot": "/app" 89 | } 90 | ] 91 | } 92 | ``` 93 | 94 | Also, if you want to run the **production mode**, you can run: 95 | 96 | ```bash 97 | docker-compose up -d my-service-production 98 | ``` 99 | 100 | This service is providing just a health endpoint which you can call to verify the service is working as expected: 101 | 102 | ```bash 103 | curl --request GET \ 104 | --url http://localhost:3000/health 105 | ``` 106 | 107 | If you want to stop developing, you can stop the service running: 108 | 109 | ```bash 110 | docker-compose down 111 | ``` 112 | 113 | ## ⚙️ Building 114 | 115 | ```bash 116 | node --run build 117 | ``` 118 | 119 | ## ✅ Testing 120 | 121 | The service provide different scripts for running the tests, to run all of them you can run: 122 | 123 | ```bash 124 | node --run test 125 | ``` 126 | 127 | If you are interested just in the unit tests, you can run: 128 | 129 | ```bash 130 | node --run test:unit 131 | ``` 132 | 133 | Or if you want e2e tests, you can execute: 134 | 135 | ```bash 136 | node --run test:e2e 137 | ``` 138 | 139 | We also have performance testing with [k6](https://k6.io/), if you want to run it via docker, execute: 140 | 141 | ```bash 142 | docker-compose up k6 143 | ``` 144 | 145 | Or if you want to run it from your machine, execute: 146 | 147 | ```bash 148 | brew install k6 149 | node --run test:performance 150 | ``` 151 | 152 | ## 💅 Linting 153 | 154 | To run the linter you can execute: 155 | 156 | ```bash 157 | node --run lint 158 | ``` 159 | 160 | And for trying to fix lint issues automatically, you can run: 161 | 162 | ```bash 163 | node --run lint:fix 164 | ``` 165 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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", 59 | "web-dashboard", 60 | "/tests/performance/contexts/users/get-users.mjs" 61 | ] 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please report security issues to `alberthernandezdev@gmail.com`. 6 | -------------------------------------------------------------------------------- /images/express-and-ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javiersteven0612/ts-express/65eb275cc570d615a63e9ab004e9ecf6c5159455/images/express-and-ts.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src", ".env"], 3 | "exec": "tsx --inspect=0.0.0.0:9229 ./src/main.ts", 4 | "ext": "ts, js, json" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-typescript-service-template", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "Skeleton for new typescript services based on express", 6 | "author": "alberthernandezdev@gmail.com", 7 | "license": "MIT", 8 | "bugs": { 9 | "url": "https://github.com/AlbertHernandez/express-typescript-service-template/issues" 10 | }, 11 | "homepage": "https://github.com/AlbertHernandez/express-typescript-service-template#readme", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/AlbertHernandez/express-typescript-service-template.git" 15 | }, 16 | "keywords": [ 17 | "typescript", 18 | "express", 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 validate-typescript && node --run build:clean && node --run generate-dist", 29 | "start": "node dist/main.js", 30 | "dev": "nodemon", 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 . --ext .js,.mjs,cjs,.ts,.mts", 38 | "lint:fix": "eslint . --ext .js,.mjs,cjs,.ts,.mts --fix", 39 | "lint:file": "eslint", 40 | "lint:yaml": "chmod +x scripts/lint_yaml.sh && ./scripts/lint_yaml.sh", 41 | "build:clean": "rimraf dist; exit 0", 42 | "validate-typescript": "tsc -p tsconfig.prod.json --noEmit", 43 | "generate-dist": "swc ./src -d dist --strip-leading-paths", 44 | "typos": "chmod +x scripts/check_typos.sh && ./scripts/check_typos.sh" 45 | }, 46 | "dependencies": { 47 | "dotenv": "^16.4.5", 48 | "express": "^5.0.1", 49 | "http-status-codes": "^2.3.0" 50 | }, 51 | "devDependencies": { 52 | "@commitlint/cli": "^19.5.0", 53 | "@commitlint/config-conventional": "^19.5.0", 54 | "@commitlint/types": "^19.0.3", 55 | "@swc/cli": "^0.5.0", 56 | "@swc/core": "^1.7.42", 57 | "@types/express": "^5.0.0", 58 | "@types/fs-extra": "^11.0.4", 59 | "@types/node": "^22.8.7", 60 | "@types/supertest": "^6.0.2", 61 | "@typescript-eslint/eslint-plugin": "^7.18.0", 62 | "@typescript-eslint/parser": "^7.18.0", 63 | "@vitest/coverage-istanbul": "1.3.1", 64 | "concurrently": "^9.0.1", 65 | "eslint": "^8.57.1", 66 | "eslint-config-prettier": "^9.0.0", 67 | "eslint-plugin-node": "^11.1.0", 68 | "eslint-plugin-prettier": "^5.2.1", 69 | "eslint-plugin-simple-import-sort": "^12.1.1", 70 | "eslint-plugin-unicorn": "^56.0.0", 71 | "eslint-plugin-vitest": "^0.4.1", 72 | "fs-extra": "^11.2.0", 73 | "husky": "^9.1.6", 74 | "lint-staged": "^15.2.10", 75 | "nock": "^13.5.5", 76 | "nodemon": "^3.1.7", 77 | "nyc": "^17.1.0", 78 | "prettier": "^3.3.3", 79 | "rimraf": "^6.0.1", 80 | "supertest": "^7.0.0", 81 | "tsconfig-paths": "^4.2.0", 82 | "tsx": "^4.19.2", 83 | "typescript": "^5.6.3", 84 | "unplugin-swc": "^1.5.1", 85 | "vite": "^5.4.10", 86 | "vitest": "^1.3.1" 87 | }, 88 | "optionalDependencies": { 89 | "@rollup/rollup-linux-x64-gnu": "^4.24.3", 90 | "@swc/core-linux-arm64-musl": "^1.7.42", 91 | "@swc/core-linux-x64-gnu": "^1.7.42", 92 | "@swc/core-linux-x64-musl": "^1.7.42" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/app/config/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | server: { 3 | port: process.env.PORT ?? 3000, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/config/load-env-vars.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | config(); 3 | -------------------------------------------------------------------------------- /src/app/health/api/health-controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { StatusCodes } from "http-status-codes"; 3 | 4 | export class HealthController { 5 | run(req: Request, res: Response) { 6 | res.status(StatusCodes.OK).send(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/app/health/api/health-router.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | import { HealthController } from "./health-controller"; 4 | 5 | const healthRouter = express.Router(); 6 | 7 | const healthController = new HealthController(); 8 | 9 | healthRouter.get("/", healthController.run.bind(healthController)); 10 | 11 | export { healthRouter }; 12 | -------------------------------------------------------------------------------- /src/app/server.ts: -------------------------------------------------------------------------------- 1 | import http from "node:http"; 2 | import { AddressInfo } from "node:net"; 3 | 4 | import express, { Express } from "express"; 5 | 6 | import { config } from "@/app/config/config"; 7 | import { healthRouter } from "@/app/health/api/health-router"; 8 | 9 | import { ConsoleLogger } from "@/shared/logger/console-logger"; 10 | import { Logger } from "@/shared/logger/logger"; 11 | 12 | import { userRouter } from "@/contexts/users/api/user-router"; 13 | 14 | export class Server { 15 | private readonly app: Express; 16 | private httpServer?: http.Server; 17 | private readonly logger: Logger; 18 | 19 | constructor() { 20 | this.logger = new ConsoleLogger(); 21 | this.app = express(); 22 | this.app.use(express.json()); 23 | this.app.use("/api/health", healthRouter); 24 | this.app.use("/api/users", userRouter); 25 | } 26 | 27 | async start(): Promise { 28 | return new Promise(resolve => { 29 | this.httpServer = this.app.listen(config.server.port, () => { 30 | const { port } = this.httpServer?.address() as AddressInfo; 31 | this.logger.info(`App is ready and listening on port ${port} 🚀`); 32 | resolve(); 33 | }); 34 | }); 35 | } 36 | 37 | async stop(): Promise { 38 | return new Promise((resolve, reject) => { 39 | if (this.httpServer) { 40 | this.httpServer.close(error => { 41 | if (error) { 42 | reject(error); 43 | return; 44 | } 45 | resolve(); 46 | }); 47 | } 48 | 49 | resolve(); 50 | }); 51 | } 52 | 53 | getHttpServer(): http.Server { 54 | if (!this.httpServer) { 55 | throw new Error("Server has not been started yet"); 56 | } 57 | 58 | return this.httpServer; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/contexts/shared/logger/console-logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { Logger } from "./logger"; 3 | 4 | export class ConsoleLogger implements Logger { 5 | info(message: string, attributes: unknown = {}) { 6 | const msg = { 7 | message, 8 | attributes, 9 | }; 10 | 11 | console.log(msg); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/contexts/shared/logger/logger.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | export type Attributes = any; 3 | 4 | export interface Logger { 5 | info: (message: string, attributes?: Attributes) => void; 6 | } 7 | -------------------------------------------------------------------------------- /src/contexts/users/api/user-controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { StatusCodes } from "http-status-codes"; 3 | 4 | import { Logger } from "@/shared/logger/logger"; 5 | 6 | export class UserController { 7 | private readonly logger; 8 | 9 | constructor(dependencies: { logger: Logger }) { 10 | this.logger = dependencies.logger; 11 | } 12 | 13 | run(req: Request, res: Response) { 14 | this.logger.info("Received request to get user"); 15 | res.status(StatusCodes.OK).send({ users: "ok" }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/contexts/users/api/user-router.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | import { ConsoleLogger } from "@/shared/logger/console-logger"; 4 | 5 | import { UserController } from "./user-controller"; 6 | 7 | const userRouter = express.Router(); 8 | 9 | const logger = new ConsoleLogger(); 10 | const userController = new UserController({ logger }); 11 | 12 | userRouter.get("/", userController.run.bind(userController)); 13 | 14 | export { userRouter }; 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import "@/app/config/load-env-vars"; 2 | 3 | import { Server } from "@/app/server"; 4 | 5 | new Server().start().catch(handleError); 6 | 7 | function handleError(error: unknown) { 8 | // eslint-disable-next-line no-console 9 | console.error(error); 10 | // eslint-disable-next-line unicorn/no-process-exit 11 | process.exit(1); 12 | } 13 | 14 | process.on("uncaughtException", handleError); 15 | -------------------------------------------------------------------------------- /tests/e2e/health.test.ts: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from "http-status-codes"; 2 | import * as nock from "nock"; 3 | import request from "supertest"; 4 | 5 | import { Server } from "@/app/server"; 6 | 7 | describe("Health", () => { 8 | let server: Server; 9 | 10 | beforeAll(async () => { 11 | server = new Server(); 12 | await server.start(); 13 | nock.disableNetConnect(); 14 | nock.enableNetConnect("127.0.0.1"); 15 | }); 16 | 17 | afterEach(() => { 18 | nock.cleanAll(); 19 | }); 20 | 21 | afterAll(async () => { 22 | await server.stop(); 23 | nock.enableNetConnect(); 24 | }); 25 | 26 | it("/GET api/health", async () => { 27 | const response = await request(server.getHttpServer()).get("/api/health"); 28 | expect(response.status).toBe(StatusCodes.OK); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/unit/contexts/users/api/user-controller.test.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { StatusCodes } from "http-status-codes"; 3 | import { vi } from "vitest"; 4 | 5 | import { ConsoleLogger } from "@/shared/logger/console-logger"; 6 | import { Logger } from "@/shared/logger/logger"; 7 | 8 | import { UserController } from "@/contexts/users/api/user-controller"; 9 | 10 | describe("UserController", () => { 11 | let controller: UserController; 12 | let logger: Logger; 13 | let req: Request; 14 | let res: Response; 15 | 16 | beforeEach(() => { 17 | req = {} as Request; 18 | res = { 19 | status: vi.fn().mockReturnThis(), 20 | send: vi.fn(), 21 | } as unknown as Response; 22 | logger = new ConsoleLogger(); 23 | controller = new UserController({ logger }); 24 | }); 25 | 26 | describe("run", () => { 27 | it("should respond with status 200", () => { 28 | controller.run(req, res); 29 | expect(res.status).toHaveBeenCalledWith(StatusCodes.OK); 30 | expect(res.send).toHaveBeenCalledWith({ users: "ok" }); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "module": "esnext", 6 | "target": "esnext", 7 | "moduleResolution": "Bundler", 8 | "allowImportingTsExtensions": true, 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "outDir": "dist", 12 | "sourceMap": true, 13 | "inlineSources": true, 14 | "skipLibCheck": true, 15 | "resolveJsonModule": true, 16 | "paths": { 17 | "@/src/*": ["src/*"], 18 | "@/app/*": ["src/app/*"], 19 | "@/contexts/*": ["src/contexts/*"], 20 | "@/shared/*": ["src/contexts/shared/*"], 21 | "@/tests/*": ["tests/*"] 22 | }, 23 | "types": ["vitest/globals"] 24 | }, 25 | "exclude": ["node_modules", "dist"] 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /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({ module: { type: "es6" } })], 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({ module: { type: "es6" } })], 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({ module: { type: "es6" } })], 9 | }); 10 | --------------------------------------------------------------------------------