├── .commitlintrc.js ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc.js ├── .npmignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .release-it.json ├── Dockerfile ├── LICENSE ├── README.md ├── example.env ├── nest-cli.json ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── app.config.ts ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── config │ ├── database.config.ts │ ├── index.ts │ └── jwt.config.ts ├── exceptions │ ├── bad-request.exception.ts │ ├── exceptions.constants.ts │ ├── exceptions.interface.ts │ ├── forbidden.exception.ts │ ├── index.ts │ ├── internal-server-error.exception.ts │ └── unauthorized.exception.ts ├── filters │ ├── all-exception.filter.ts │ ├── bad-request-exception.filter.ts │ ├── forbidden-exception.filter.ts │ ├── index.ts │ ├── internal-server-error-exception.filter.ts │ ├── not-found-exception.filter.ts │ ├── unauthorized-exception.filter.ts │ └── validator-exception.filter.ts ├── main.ts ├── modules │ ├── auth │ │ ├── auth.controller.ts │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── decorators │ │ │ └── get-user.decorator.ts │ │ ├── dtos │ │ │ ├── index.ts │ │ │ ├── login.req.dto.ts │ │ │ ├── login.res.dto.ts │ │ │ ├── signup.req.dto.ts │ │ │ └── signup.res.dto.ts │ │ ├── guards │ │ │ └── jwt-user-auth.guard.ts │ │ ├── interfaces │ │ │ └── jwt-user-payload.interface.ts │ │ └── strategies │ │ │ └── jwt-user.strategy.ts │ ├── user │ │ ├── dtos │ │ │ ├── get-profile.res.dto.ts │ │ │ └── index.ts │ │ ├── user.controller.ts │ │ ├── user.module.ts │ │ ├── user.query.service.ts │ │ ├── user.repository.ts │ │ └── user.schema.ts │ └── workspace │ │ ├── workspace.module.ts │ │ ├── workspace.query-service.ts │ │ ├── workspace.repository.ts │ │ └── workspace.schema.ts └── shared │ ├── enums │ ├── db.enum.ts │ ├── index.ts │ ├── log-level.enum.ts │ └── node-env.enum.ts │ └── types │ ├── index.ts │ └── schema.type.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | // https://www.conventionalcommits.org/en/v1.0.0 2 | 3 | module.exports = { extends: ['@commitlint/config-conventional'] }; -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | .github 4 | node_modules 5 | npm-debug.log 6 | dist -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | trim_trailing_whitespace = true 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | 9 | [package.json] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [*.yml] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | [*.ts] 18 | quote_type = single -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/* 2 | **/dist/* 3 | .idea 4 | .vscode -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: [ 9 | '@typescript-eslint/eslint-plugin', 10 | 'import', 11 | 'unused-imports', // Auto remove unused imports 12 | 'sort-imports-es6-autofix', // Auto sort the import order 13 | 'prettier', 14 | ], 15 | extends: [ 16 | // NestJS default extends 17 | 'plugin:@typescript-eslint/recommended', 18 | // Airbnb-base depended plugin 19 | 'plugin:import/recommended', 20 | // Support TypeScript [Import] 21 | 'plugin:import/typescript', 22 | 'airbnb-base', 23 | // Support TypeScript [Airbnb] 24 | 'airbnb-typescript/base', 25 | // IMPORTANT: add this to the last of the extends to override ESLint rules 26 | 'plugin:prettier/recommended', 27 | ], 28 | root: true, 29 | env: { 30 | node: true, 31 | jest: true, 32 | }, 33 | ignorePatterns: ['.eslintrc.js'], 34 | rules: { 35 | // NestJS default rules 36 | '@typescript-eslint/interface-name-prefix': 'off', 37 | // NestJS default rules 38 | '@typescript-eslint/explicit-function-return-type': 'off', 39 | // NestJS default rules 40 | '@typescript-eslint/explicit-module-boundary-types': 'off', 41 | // NestJS default rules 42 | '@typescript-eslint/no-explicit-any': 'off', 43 | // to avoid line ending issues in windows & linux (LF vs CRLF) 44 | 'prettier/prettier': ['error', { endOfLine: 'auto' }], 45 | // prefer template string over concat string 46 | 'prefer-template': 'error', 47 | curly: ['error', 'all'], 48 | 'no-trailing-spaces': 'error', 49 | 'lines-between-class-members': 'error', 50 | 'no-underscore-dangle': [ 51 | 'error', 52 | { 53 | allow: ['_id', '_default'], 54 | }, 55 | ], 56 | 57 | '@typescript-eslint/naming-convention': 'off', // disable the rule for now due to _id not being allowed 58 | // Example setting of unused-imports plugin 59 | 'unused-imports/no-unused-imports': 'warn', 60 | 'unused-imports/no-unused-vars': [ 61 | 'warn', 62 | { 63 | vars: 'all', 64 | varsIgnorePattern: '^_', 65 | args: 'after-used', 66 | argsIgnorePattern: '^_', 67 | }, 68 | ], 69 | // Conflict with sort-imports-es6 plugin 70 | 'import/order': 'error', 71 | 72 | // Example setting of sort-imports-es6 plugin 73 | 'sort-imports-es6-autofix/sort-imports-es6': [ 74 | 'warn', 75 | { 76 | ignoreCase: false, 77 | ignoreMemberSort: false, 78 | memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], 79 | }, 80 | ], 81 | // Not enforce using 'this' in a class function since some function can be a pure function 82 | 'class-methods-use-this': 'off', 83 | 84 | // For Typescript, it is better not to use default export: https://stackoverflow.com/a/33307487/11440474 85 | 'import/prefer-default-export': 'off', 86 | 87 | // Conflict with alias path 88 | 'import/extensions': 'off', 89 | 90 | 'no-param-reassign': ['error', { props: false }], 91 | }, 92 | }; 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | .env -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC1091 3 | . "$(dirname "$0")/_/husky.sh" 4 | 5 | npx commitlint --edit "$1" -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | echo '\033[1mRUNNING PRE-COMMIT VALIDATIONS\033[0m'; 5 | npx lint-staged -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '{src,test}/**/*.[tj]s': ['npm run lint'], 3 | }; 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | dist/test 2 | dist/**/*.{js.map} 3 | 4 | .DS_Store 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | 23 | .editorconfig 24 | .eslintrc.js 25 | tsconfig.json 26 | 27 | .turbo 28 | *.tsbuildinfo -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piyush-kacha/nestjs-starter-kit/8e26ab0afe39a42124a7c5a8ec4a1af2cc49be6d/.npmrc -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | package.json 4 | package-lock.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 140, 6 | "tabWidth": 2, 7 | "endOfLine": "lf" 8 | } -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "commitMessage": "chore(): release v${version}" 4 | }, 5 | "github": { 6 | "release": true 7 | } 8 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ################### 2 | # BUILD FOR LOCAL DEVELOPMENT 3 | ################### 4 | 5 | FROM node:20-alpine As development 6 | 7 | USER node 8 | 9 | # Create app directory 10 | WORKDIR /usr/src/app 11 | 12 | # Copy application dependency manifests to the container image. 13 | # A wildcard is used to ensure copying both package.json AND package-lock.json (when available). 14 | # Copying this first prevents re-running npm install on every code change. 15 | COPY --chown=node:node package*.json ./ 16 | COPY --chown=node:node .npmignore ./ 17 | COPY --chown=node:node .npmrc ./ 18 | 19 | # Install app dependencies 20 | RUN npm ci --ignore-scripts 21 | 22 | # Bundle app source 23 | COPY --chown=node:node . . 24 | 25 | ################### 26 | # BUILD FOR PRODUCTION 27 | ################### 28 | 29 | # Base image for production 30 | FROM node:20-alpine As build 31 | 32 | USER node 33 | 34 | # Create app directory 35 | WORKDIR /usr/src/app 36 | 37 | COPY --chown=node:node package*.json ./ 38 | COPY --chown=node:node .npmignore ./ 39 | COPY --chown=node:node .npmrc ./ 40 | 41 | COPY --chown=node:node --from=development /usr/src/app/node_modules ./node_modules 42 | 43 | # Bundle app source 44 | COPY --chown=node:node . . 45 | 46 | RUN npm run build 47 | 48 | # Set NODE_ENV environment variable 49 | ENV NODE_ENV production 50 | 51 | RUN npm ci --ignore-scripts --only=production && npm cache clean --force 52 | 53 | ################### 54 | # PRODUCTION 55 | ################### 56 | 57 | # Base image for production 58 | FROM node:20-alpine As production 59 | 60 | # Copy the bundled code to the production image 61 | COPY --chown=node:node --from=build /usr/src/app/node_modules ./node_modules 62 | COPY --chown=node:node --from=build /usr/src/app/dist ./dist 63 | 64 | # Start the server using the production build 65 | CMD [ "node", "dist/main.js" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Piyush Kacha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NestJS Starter Kit 2 | 3 | This is a starter kit for building a Nest.js application with MongoDB, Express, Clustering, Swagger, Pino, and Exception handling. 4 | 5 | 6 | ## Getting Started 7 | To get started with this project, clone the repository and install the dependencies: 8 | 9 | ```bash 10 | git clone https://github.com/piyush-kacha/nestjs-starter-kit.git 11 | cd nestjs-starter-kit 12 | npm ci 13 | ``` 14 | 15 | 16 | Use the copy command to create a copy of the example .env file. Replace `example.env` with the name of your example .env file: 17 | 18 | ```bash 19 | cp example.env .env 20 | ``` 21 | 22 | ## Generating an RSA Key Pair for JWT Authentication with OpenSSL 23 | 24 | 1. Generate a private key: 25 | 26 | ```sh 27 | openssl genrsa -out private_key.pem 2048 28 | ``` 29 | 2. Extract the public key from the private key: 30 | ```sh 31 | openssl rsa -in private_key.pem -outform PEM -pubout -out public_key.pem 32 | ``` 33 | This will create two files in the current directory: private_key.pem (the private key) and public_key.pem (the corresponding public key). 34 | 35 | Note: The key size (2048 bits in this example) can be adjusted to suit your security requirements. 36 | 3. Encode the public key in base64 encoding: 37 | ```sh 38 | openssl base64 -A -in public_key.pem -out public_key_base64.txt 39 | ``` 40 | Copy the contents of the public_key_base64.txt file and paste it into the public_key_base64.txt file in .env JWT_PUBLIC_KEY variable. 41 | 42 | 4. Encode the private key in base64 encoding: 43 | ```sh 44 | openssl base64 -A -in private_key.pem -out private_key_base64.txt 45 | ``` 46 | Copy the contents of the private_key_base64.txt file and paste it into the private_key_base64.txt file in .env JWT_PRIVATE_KEY variable. 47 | 48 | 5. Remove the public_key.pem and private_key.pem files. 49 | 50 | 6. Remove the public_key_base64.txt and private_key_base64.txt files. 51 | 52 | 53 | ## Running the Application 54 | To run the application in development mode, use the following command: 55 | ```bash 56 | npm run start:dev 57 | ``` 58 | 59 | This will start the application in watch mode, so any changes you make to the code will automatically restart the server. 60 | 61 | To run the application in production mode, use the following command: 62 | ```bash 63 | npm run start:prod 64 | ``` 65 | 66 | ## Features: 67 | 1. **Modularity**: The project structure follows a modular approach, organizing files into directories based on their functionalities. This makes it easier to locate and maintain code related to specific features or components of the application. 68 | 69 | 2. **Scalability**: The modular structure allows for easy scalability as the application grows. New modules, controllers, services, and other components can be added without cluttering the main directory. 70 | 71 | 3. **Separation of Concerns**: Each file has a clear purpose and responsibility, making it easier to understand and maintain the codebase. For example, configuration files are located in the `config/` directory, exception classes in the `exceptions/` directory, and shared modules in the `shared/` directory. 72 | 73 | 4. **Global Exception Handling**: The starter kit includes global exception filters (`AllExceptionsFilter`, `BadRequestExceptionFilter`, etc.) that catch and handle exceptions across the application. This promotes consistent error handling and improves the overall robustness of the application. 74 | 75 | 5. **Configuration Management**: The use of `@nestjs/config` allows for centralized management of environment variables and application configuration. This simplifies the process of accessing and modifying configuration values. 76 | 77 | 6. **Clustering**: The code includes clustering support, which enables the application to utilize multiple CPU cores for improved performance and scalability. 78 | 79 | 7. **Logging**: The integration of `nestjs-pino` enables efficient and customizable logging for the application. Pino is a fast and low-overhead logger that can be easily configured according to specific logging requirements. 80 | 81 | 8. **Swagger Documentation**: The starter kit includes Swagger integration (`@nestjs/swagger`) for generating interactive API documentation. Swagger provides a user-friendly interface for exploring and testing the API endpoints. 82 | 83 | 9. **Linting and Formatting**: The project includes configuration files for ESLint and Prettier, enforcing consistent code style and catching potential errors. This promotes code quality and maintainability across the development team. 84 | 85 | 10. **Docker Support**: The presence of a Dockerfile allows for containerization of the application, making it easier to deploy and run the application in different environments. 86 | 87 | By leveraging this code structure, you can benefit from the well-organized and maintainable foundation provided by the NestJS starter kit. It provides a solid structure for building scalable and robust applications while incorporating best practices and popular libraries. 88 | 89 | 90 | # Project Structure 91 | 92 | ```bash 93 | nestjs-starter-kit/ 94 | . 95 | ├── Dockerfile 96 | ├── LICENSE 97 | ├── README.md 98 | ├── .husky/ 99 | │ ├── commit-msg 100 | │ └── pre-commit 101 | ├── src 102 | │   ├── app.config.ts 103 | │   ├── app.controller.spec.ts 104 | │   ├── app.controller.ts 105 | │   ├── app.module.ts 106 | │   ├── app.service.ts 107 | │   ├── config 108 | │   │   ├── database.config.ts 109 | │   │   ├── index.ts 110 | │   │   └── jwt.config.ts 111 | │   ├── exceptions 112 | │   │   ├── bad-request.exception.ts 113 | │   │   ├── exceptions.constants.ts 114 | │   │   ├── exceptions.interface.ts 115 | │   │   ├── forbidden.exception.ts 116 | │   │   ├── index.ts 117 | │   │   ├── internal-server-error.exception.ts 118 | │   │   └── unauthorized.exception.ts 119 | │   ├── filters 120 | │   │   ├── all-exception.filter.ts 121 | │   │   ├── bad-request-exception.filter.ts 122 | │   │   ├── forbidden-exception.filter.ts 123 | │   │   ├── index.ts 124 | │   │   ├── internal-server-error-exception.filter.ts 125 | │   │   ├── not-found-exception.filter.ts 126 | │   │   ├── unauthorized-exception.filter.ts 127 | │   │   └── validator-exception.filter.ts 128 | │   ├── main.ts 129 | │   ├── modules 130 | │   │   ├── auth 131 | │   │   │   ├── auth.controller.ts 132 | │   │   │   ├── auth.module.ts 133 | │   │   │   ├── auth.service.ts 134 | │   │   │   ├── decorators 135 | │   │   │   │   └── get-user.decorator.ts 136 | │   │   │   ├── dtos 137 | │   │   │   │   ├── index.ts 138 | │   │   │   │   ├── login.req.dto.ts 139 | │   │   │   │   ├── login.res.dto.ts 140 | │   │   │   │   ├── signup.req.dto.ts 141 | │   │   │   │   └── signup.res.dto.ts 142 | │   │   │   ├── guards 143 | │   │   │   │   └── jwt-user-auth.guard.ts 144 | │   │   │   ├── interfaces 145 | │   │   │   │   └── jwt-user-payload.interface.ts 146 | │   │   │   └── strategies 147 | │   │   │   └── jwt-user.strategy.ts 148 | │   │   ├── user 149 | │   │   │   ├── dtos 150 | │   │   │   │   ├── get-profile.res.dto.ts 151 | │   │   │   │   └── index.ts 152 | │   │   │   ├── user.controller.ts 153 | │   │   │   ├── user.module.ts 154 | │   │   │   ├── user.query.service.ts 155 | │   │   │   ├── user.repository.ts 156 | │   │   │   └── user.schema.ts 157 | │   │   └── workspace 158 | │   │   ├── workspace.module.ts 159 | │   │   ├── workspace.query-service.ts 160 | │   │   ├── workspace.repository.ts 161 | │   │   └── workspace.schema.ts 162 | │   └── shared 163 | │   ├── enums 164 | │   │   ├── db.enum.ts 165 | │   │   ├── index.ts 166 | │   │   ├── log-level.enum.ts 167 | │   │   └── node-env.enum.ts 168 | │   └── types 169 | │   ├── index.ts 170 | │   └── schema.type.ts 171 | ├── test 172 | │   ├── app.e2e-spec.ts 173 | │   └── jest-e2e.json 174 | ├── tsconfig.build.json 175 | └── tsconfig.json 176 | ├── example.env 177 | ├── .commitlintrc.js 178 | ├── .dockerignore 179 | ├── .editorconfig 180 | ├── .eslintignore 181 | ├── .eslintrc.js 182 | ├── .gitignore 183 | ├── .lintstagedrc.js 184 | ├── .npmignore 185 | ├── .npmrc 186 | ├── .prettierignore 187 | ├── .prettierrc 188 | ├── nest-cli.json 189 | ├── package-lock.json 190 | ├── package.json 191 | ├── renovate.json 192 | ``` 193 | This project follows a structured organization to maintain clean, scalable code, promoting best practices for enterprise-level applications. 194 | 195 | ### 1. Root Files and Configuration 196 | 197 | - **`Dockerfile`**: Defines how to build the Docker image for the application, including separate stages for development and production. 198 | - **`README.md`**: Provides an overview and documentation for the project. 199 | - **`.husky/`**: Contains Git hooks for automated checks during commit and push operations. 200 | - **`example.env`**: A sample environment file illustrating the required environment variables. 201 | 202 | ### 2. Source Code (`src/`) 203 | 204 | - **`app.config.ts`**: Centralizes application configuration settings. 205 | - **`app.controller.ts`**: Defines the root controller for handling incoming requests. 206 | - **`app.module.ts`**: The main module that aggregates all the feature modules and services. 207 | - **`app.service.ts`**: Contains the primary business logic for the application. 208 | - **`main.ts`**: The entry point of the NestJS application; bootstraps the application and configures clustering. 209 | 210 | #### Subdirectories within `src/` 211 | 212 | - **`config/`**: Stores configuration files (e.g., `database.config.ts`, `jwt.config.ts`) for different aspects of the application. 213 | - **`exceptions/`**: Custom exception classes (e.g., `bad-request.exception.ts`, `unauthorized.exception.ts`) that extend NestJS's built-in exceptions. 214 | - **`filters/`**: Custom exception filters (e.g., `all-exception.filter.ts`, `not-found-exception.filter.ts`) for handling different types of errors globally. 215 | - **`modules/`**: Contains feature modules of the application: 216 | - **`auth/`**: Handles authentication-related functionality, including controllers, services, guards, and strategies. 217 | - **`user/`**: Manages user-related operations, including controllers, services, repositories, and schemas. 218 | - **`workspace/`**: Manages workspace-related functionality, with services, repositories, and schemas. 219 | - **`shared/`**: Contains shared resources and utilities: 220 | - **`enums/`**: Defines enumerations (e.g., `db.enum.ts`, `node-env.enum.ts`) used across the application. 221 | - **`types/`**: Custom TypeScript types (e.g., `schema.type.ts`) used for type safety throughout the codebase. 222 | 223 | ### 3. Testing (`test/`) 224 | 225 | - **`test/`**: Houses end-to-end test specifications (`app.e2e-spec.ts`) and configuration files (`jest-e2e.json`) for testing. 226 | 227 | ### 4. Configuration and Tooling 228 | 229 | - **`.commitlintrc.js`**: Configures commit message linting rules to enforce a consistent commit history using the Conventional Commits specification. 230 | - **`.dockerignore`**: Specifies files and directories to be excluded from the Docker image build context. 231 | - **`.editorconfig`**: Defines coding style settings (e.g., indentation, line endings) to ensure consistency across different editors. 232 | - **`.eslintignore`**: Specifies files and directories to be ignored by ESLint. 233 | - **`.eslintrc.js`**: Configures ESLint rules and settings for code quality and style enforcement. 234 | - **`.gitignore`**: Lists files and directories to be excluded from version control. 235 | - **`.lintstagedrc.js`**: Configures lint-staged for running linters on staged Git files. 236 | - **`.npmignore`**: Specifies files and directories to be excluded when publishing the package to npm. 237 | - **`.npmrc`**: Configures npm-specific settings (e.g., registry URL). 238 | - **`.prettierignore`**: Lists files and directories to be excluded from Prettier formatting. 239 | - **`.prettierrc`**: Configures Prettier settings for code formatting. 240 | - **`nest-cli.json`**: Configuration file for the NestJS CLI, defining paths and settings. 241 | - **`package.json`**: Lists project metadata, scripts, and dependencies. 242 | - **`package-lock.json`**: Lockfile for npm dependencies to ensure consistent installs across environments. 243 | - **`tsconfig.build.json`**: TypeScript configuration for building the project. 244 | - **`tsconfig.json`**: Main TypeScript configuration file. 245 | 246 | ### Key Features of the Project Structure 247 | 248 | - **Modular Design**: The application is organized into modules (`modules/` directory), each encapsulating a specific feature set, making the codebase scalable and maintainable. 249 | - **Centralized Configuration**: All configuration files are stored under `config/`, promoting centralized management of application settings. 250 | - **Custom Error Handling**: Custom exceptions and filters (`exceptions/` and `filters/` directories) provide granular control over error handling. 251 | - **Testing and Linting**: The project is set up with robust testing (`test/` directory) and linting tools (`.eslintrc.js`, `.eslintignore`), ensuring high code quality and reliability. 252 | - **Dockerization**: The `Dockerfile` supports both development and production environments, enabling seamless deployment. 253 | 254 | ## Contributing 255 | 256 | Contributions are welcome! If you find a bug or have a feature request, please open an issue. If you would like to contribute code, please fork the repository and submit a pull request. 257 | 258 | ## License 259 | 260 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 261 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | ## Environment variables for the application 2 | 3 | # The environment the application is running in (e.g. development, production, test) 4 | NODE_ENV= 5 | 6 | # The port number that the application listens on 7 | PORT= 8 | 9 | # The hostname or IP address that the application binds to (e.g. localhost, 127.0.0.1) 10 | HOST= 11 | 12 | # The logging level for the application (e.g. debug, info, warn, error) 13 | LOG_LEVEL= 14 | 15 | # Whether to enable clustering mode for the application (true or false) 16 | CLUSTERING= 17 | 18 | # The URI for the MongoDB database used by the application 19 | MONGODB_URI= 20 | 21 | # The private key for generating JSON Web Tokens (JWTs) 22 | # in README.md for more information how to generate a private key 23 | JWT_PRIVATE_KEY= 24 | 25 | # The public key for verifying JSON Web Tokens (JWTs) 26 | # in README.md for more information how to generate a public key 27 | JWT_PUBLIC_KEY= 28 | 29 | # The expiration time for JSON Web Tokens (JWTs) 30 | # expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js). 31 | # Eg: 60, "2 days", "10h", "7d" 32 | JWT_EXPIRATION_TIME= -------------------------------------------------------------------------------- /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 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-starter-kit", 3 | "version": "0.0.4", 4 | "description": "Nest.js with MongoDB, Express, Clustering, Swagger, Pino, Exception handling", 5 | "main": "dist/main.js", 6 | "scripts": { 7 | "build": "nest build", 8 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 9 | "prebuild": "rimraf dist", 10 | "start": "nest start", 11 | "start:dev": "nest start --watch", 12 | "start:debug": "nest start --debug --watch", 13 | "start:prod": "node dist/main", 14 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 15 | "prepare": "husky install", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/piyush-kacha/nestjs-starter-kit.git" 25 | }, 26 | "author": "Piyush Kacha ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/piyush-kacha/nestjs-starter-kit/issues" 30 | }, 31 | "homepage": "https://github.com/piyush-kacha/nestjs-starter-kit#readme", 32 | "dependencies": { 33 | "@nestjs/common": "^10.3.8", 34 | "@nestjs/config": "^4.0.2", 35 | "@nestjs/core": "^10.3.8", 36 | "@nestjs/jwt": "^10.2.0", 37 | "@nestjs/mongoose": "^10.0.10", 38 | "@nestjs/passport": "^10.0.3", 39 | "@nestjs/platform-express": "^10.3.8", 40 | "@nestjs/swagger": "^7.3.1", 41 | "bcryptjs": "^2.4.3", 42 | "class-transformer": "^0.5.1", 43 | "class-validator": "^0.14.1", 44 | "mongoose": "^8.12.1", 45 | "nestjs-pino": "^4.0.0", 46 | "passport-jwt": "^4.0.1", 47 | "pino": "^9.0.0", 48 | "pino-http": "^9.0.0", 49 | "reflect-metadata": "^0.1.14", 50 | "rimraf": "^5.0.10", 51 | "rxjs": "^7.8.1" 52 | }, 53 | "devDependencies": { 54 | "@commitlint/cli": "^18.6.1", 55 | "@commitlint/config-conventional": "^18.6.3", 56 | "@nestjs/cli": "^10.3.2", 57 | "@nestjs/schematics": "^10.1.1", 58 | "@nestjs/testing": "^10.3.8", 59 | "@types/express": "^4.17.21", 60 | "@types/jest": "29.5.12", 61 | "@types/node": "20.16.2", 62 | "@types/supertest": "^6.0.2", 63 | "@typescript-eslint/eslint-plugin": "^6.21.0", 64 | "@typescript-eslint/parser": "^6.21.0", 65 | "eslint": "^8.56.0", 66 | "eslint-config-airbnb-base": "^15.0.0", 67 | "eslint-config-airbnb-typescript": "^17.1.0", 68 | "eslint-config-prettier": "^9.1.0", 69 | "eslint-plugin-import": "^2.29.1", 70 | "eslint-plugin-prettier": "^5.2.1", 71 | "eslint-plugin-sort-imports-es6-autofix": "^0.6.0", 72 | "eslint-plugin-unused-imports": "^3.0.0", 73 | "husky": "^8.0.3", 74 | "jest": "29.7.0", 75 | "lint-staged": "^15.2.9", 76 | "pino-pretty": "^11.0.0", 77 | "prettier": "^3.2.5", 78 | "source-map-support": "^0.5.21", 79 | "supertest": "^6.3.4", 80 | "ts-jest": "29.2.5", 81 | "ts-loader": "^9.5.1", 82 | "ts-node": "^10.9.2", 83 | "tsconfig-paths": "4.2.0", 84 | "typescript": "^5.3.3" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "semanticCommits": true, 3 | "packageRules": [{ 4 | "depTypeList": ["devDependencies"], 5 | "automerge": true 6 | }], 7 | "extends": [ 8 | "config:base" 9 | ] 10 | } -------------------------------------------------------------------------------- /src/app.config.ts: -------------------------------------------------------------------------------- 1 | // Import external modules 2 | import * as crypto from 'crypto'; // Used to generate random UUIDs 3 | import { IncomingMessage, ServerResponse } from 'http'; // Used to handle incoming and outgoing HTTP messages 4 | import { Params } from 'nestjs-pino'; // Used to define parameters for the Pino logger 5 | 6 | // Import internal modules 7 | import { LogLevel, NodeEnv } from './shared/enums'; // Import application enums 8 | 9 | export class AppConfig { 10 | public static getLoggerConfig(): Params { 11 | // Define the configuration for the Pino logger 12 | const { NODE_ENV, LOG_LEVEL, CLUSTERING } = process.env; 13 | 14 | return { 15 | exclude: [], // Exclude specific path from the logs and may not work for e2e testing 16 | pinoHttp: { 17 | genReqId: () => crypto.randomUUID(), // Generate a random UUID for each incoming request 18 | autoLogging: true, // Automatically log HTTP requests and responses 19 | base: CLUSTERING === 'true' ? { pid: process.pid } : {}, // Include the process ID in the logs if clustering is enabled 20 | customAttributeKeys: { 21 | responseTime: 'timeSpent', // Rename the responseTime attribute to timeSpent 22 | }, 23 | level: LOG_LEVEL || (NODE_ENV === NodeEnv.PRODUCTION ? LogLevel.INFO : LogLevel.TRACE), // Set the log level based on the environment and configuration 24 | serializers: { 25 | req(request: IncomingMessage) { 26 | return { 27 | method: request.method, 28 | url: request.url, 29 | id: request.id, 30 | // Including the headers in the log could be in violation of privacy laws, e.g. GDPR. 31 | // headers: request.headers, 32 | }; 33 | }, 34 | res(reply: ServerResponse) { 35 | return { 36 | statusCode: reply.statusCode, 37 | }; 38 | }, 39 | }, 40 | transport: 41 | NODE_ENV !== NodeEnv.PRODUCTION // Only use Pino-pretty in non-production environments 42 | ? { 43 | target: 'pino-pretty', 44 | options: { 45 | translateTime: 'SYS:yyyy-mm-dd HH:MM:ss', 46 | }, 47 | } 48 | : null, 49 | }, 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | 6 | describe('AppController', () => { 7 | let appController: AppController; 8 | 9 | beforeEach(async () => { 10 | const app: TestingModule = await Test.createTestingModule({ 11 | controllers: [AppController], 12 | providers: [AppService], 13 | }).compile(); 14 | 15 | appController = app.get(AppController); 16 | }); 17 | 18 | describe('root', () => { 19 | it('should return "Hello World!"', () => { 20 | expect(appController.getHello()).toBe('Hello World!'); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { ApiTags } from '@nestjs/swagger'; 2 | import { Controller, Get } from '@nestjs/common'; 3 | 4 | import { AppService } from './app.service'; 5 | 6 | @ApiTags('Health-check') 7 | @Controller() 8 | export class AppController { 9 | constructor(private readonly appService: AppService) {} 10 | 11 | @Get() 12 | getHello(): string { 13 | return this.appService.getHello(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | // Import required modules 2 | import { APP_FILTER, APP_PIPE } from '@nestjs/core'; 3 | import { ConfigModule, ConfigService } from '@nestjs/config'; 4 | import { LoggerModule } from 'nestjs-pino'; 5 | import { Module, ValidationError, ValidationPipe } from '@nestjs/common'; 6 | import { MongooseModule } from '@nestjs/mongoose'; 7 | 8 | // Import application files 9 | 10 | import { AppConfig } from './app.config'; 11 | import { AppController } from './app.controller'; 12 | import { AppService } from './app.service'; 13 | import { configuration } from './config/index'; 14 | 15 | // Import filters 16 | import { 17 | AllExceptionsFilter, 18 | BadRequestExceptionFilter, 19 | ForbiddenExceptionFilter, 20 | NotFoundExceptionFilter, 21 | UnauthorizedExceptionFilter, 22 | ValidationExceptionFilter, 23 | } from './filters'; 24 | import { AuthModule } from './modules/auth/auth.module'; 25 | import { UserModule } from './modules/user/user.module'; 26 | import { WorkspaceModule } from './modules/workspace/workspace.module'; 27 | 28 | // Import other modules 29 | 30 | @Module({ 31 | imports: [ 32 | // Configure environment variables 33 | ConfigModule.forRoot({ 34 | isGlobal: true, // Make the configuration global 35 | load: [configuration], // Load the environment variables from the configuration file 36 | }), 37 | 38 | // Configure logging 39 | LoggerModule.forRoot(AppConfig.getLoggerConfig()), // ! forRootAsync is not working with ConfigService in nestjs-pino 40 | 41 | // Configure mongoose 42 | MongooseModule.forRootAsync({ 43 | imports: [ConfigModule], // Import the ConfigModule so that it can be injected into the factory function 44 | inject: [ConfigService], // Inject the ConfigService into the factory function 45 | useFactory: async (configService: ConfigService) => ({ 46 | // Get the required configuration settings from the ConfigService 47 | uri: configService.get('database.uri'), 48 | }), 49 | }), 50 | // Import other modules 51 | AuthModule, 52 | UserModule, 53 | WorkspaceModule, 54 | ], 55 | controllers: [AppController], // Define the application's controller 56 | providers: [ 57 | AppService, 58 | { provide: APP_FILTER, useClass: AllExceptionsFilter }, 59 | { provide: APP_FILTER, useClass: ValidationExceptionFilter }, 60 | { provide: APP_FILTER, useClass: BadRequestExceptionFilter }, 61 | { provide: APP_FILTER, useClass: UnauthorizedExceptionFilter }, 62 | { provide: APP_FILTER, useClass: ForbiddenExceptionFilter }, 63 | { provide: APP_FILTER, useClass: NotFoundExceptionFilter }, 64 | { 65 | // Allowing to do validation through DTO 66 | // Since class-validator library default throw BadRequestException, here we use exceptionFactory to throw 67 | // their internal exception so that filter can recognize it 68 | provide: APP_PIPE, 69 | useFactory: () => 70 | new ValidationPipe({ 71 | exceptionFactory: (errors: ValidationError[]) => { 72 | return errors[0]; 73 | }, 74 | }), 75 | }, 76 | ], // Define the application's service 77 | }) 78 | export class AppModule {} 79 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Yeah yeah! we are okay!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/config/database.config.ts: -------------------------------------------------------------------------------- 1 | export interface IDatabaseConfig { 2 | uri: string; 3 | } 4 | 5 | export const databaseConfig = (): IDatabaseConfig => ({ 6 | uri: process.env.MONGODB_URI, 7 | }); 8 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | import { IDatabaseConfig, databaseConfig } from './database.config'; 2 | import { IJwtConfig, jwtConfig } from './jwt.config'; 3 | import { NodeEnv } from '../shared/enums/node-env.enum'; 4 | 5 | export interface IConfig { 6 | env: string; 7 | port: number; 8 | host: string; 9 | logLevel: string; 10 | clustering: string; 11 | database: IDatabaseConfig; 12 | jwt: IJwtConfig; 13 | } 14 | 15 | export const configuration = (): Partial => ({ 16 | env: process.env.NODE_ENV || NodeEnv.DEVELOPMENT, 17 | port: parseInt(process.env.PORT, 10) || 3009, 18 | host: process.env.HOST || '127.0.0.1', 19 | logLevel: process.env.LOG_LEVEL, 20 | clustering: process.env.CLUSTERING, 21 | database: databaseConfig(), 22 | jwt: jwtConfig(), 23 | }); 24 | -------------------------------------------------------------------------------- /src/config/jwt.config.ts: -------------------------------------------------------------------------------- 1 | export interface IJwtConfig { 2 | privateKey: string; 3 | publicKey: string; 4 | expiresIn: string; 5 | } 6 | 7 | export const jwtConfig = (): IJwtConfig => ({ 8 | privateKey: Buffer.from(process.env.JWT_PRIVATE_KEY, 'base64').toString('utf-8'), 9 | publicKey: Buffer.from(process.env.JWT_PUBLIC_KEY, 'base64').toString('utf-8'), 10 | expiresIn: process.env.JWT_EXPIRATION_TIME || '1h', 11 | }); 12 | -------------------------------------------------------------------------------- /src/exceptions/bad-request.exception.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A custom exception that represents a BadRequest error. 3 | */ 4 | 5 | // Import required modules 6 | import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; 7 | import { HttpException, HttpStatus } from '@nestjs/common'; 8 | 9 | // Import internal modules 10 | import { ExceptionConstants } from './exceptions.constants'; 11 | import { IException, IHttpBadRequestExceptionResponse } from './exceptions.interface'; 12 | 13 | export class BadRequestException extends HttpException { 14 | @ApiProperty({ 15 | enum: ExceptionConstants.BadRequestCodes, 16 | description: 'A unique code identifying the error.', 17 | example: ExceptionConstants.BadRequestCodes.VALIDATION_ERROR, 18 | }) 19 | code: number; // Internal status code 20 | 21 | @ApiHideProperty() 22 | cause: Error; // Error object causing the exception 23 | 24 | @ApiProperty({ 25 | description: 'Message for the exception', 26 | example: 'Bad Request', 27 | }) 28 | message: string; // Message for the exception 29 | 30 | @ApiProperty({ 31 | description: 'A description of the error message.', 32 | example: 'The input provided was invalid', 33 | }) 34 | description: string; // Description of the exception 35 | 36 | @ApiProperty({ 37 | description: 'Timestamp of the exception', 38 | format: 'date-time', 39 | example: '2022-12-31T23:59:59.999Z', 40 | }) 41 | timestamp: string; // Timestamp of the exception 42 | 43 | @ApiProperty({ 44 | description: 'Trace ID of the request', 45 | example: '65b5f773-df95-4ce5-a917-62ee832fcdd0', 46 | }) 47 | traceId: string; // Trace ID of the request 48 | 49 | /** 50 | * Constructs a new BadRequestException object. 51 | * @param exception An object containing the exception details. 52 | * - message: A string representing the error message. 53 | * - cause: An object representing the cause of the error. 54 | * - description: A string describing the error in detail. 55 | * - code: A number representing internal status code which helpful in future for frontend 56 | */ 57 | constructor(exception: IException) { 58 | super(exception.message, HttpStatus.BAD_REQUEST, { 59 | cause: exception.cause, 60 | description: exception.description, 61 | }); 62 | this.message = exception.message; 63 | this.cause = exception.cause; 64 | this.description = exception.description; 65 | this.code = exception.code; 66 | this.timestamp = new Date().toISOString(); 67 | } 68 | 69 | /** 70 | * Set the Trace ID of the BadRequestException instance. 71 | * @param traceId A string representing the Trace ID. 72 | */ 73 | setTraceId = (traceId: string) => { 74 | this.traceId = traceId; 75 | }; 76 | 77 | /** 78 | * Generate an HTTP response body representing the BadRequestException instance. 79 | * @param message A string representing the message to include in the response body. 80 | * @returns An object representing the HTTP response body. 81 | */ 82 | generateHttpResponseBody = (message?: string): IHttpBadRequestExceptionResponse => { 83 | return { 84 | code: this.code, 85 | message: message || this.message, 86 | description: this.description, 87 | timestamp: this.timestamp, 88 | traceId: this.traceId, 89 | }; 90 | }; 91 | 92 | /** 93 | * Returns a new instance of BadRequestException representing an HTTP Request Timeout error. 94 | * @returns An instance of BadRequestException representing the error. 95 | */ 96 | static HTTP_REQUEST_TIMEOUT = () => { 97 | return new BadRequestException({ 98 | message: 'HTTP Request Timeout', 99 | code: ExceptionConstants.BadRequestCodes.HTTP_REQUEST_TIMEOUT, 100 | }); 101 | }; 102 | 103 | /** 104 | * Create a BadRequestException for when a resource already exists. 105 | * @param {string} [msg] - Optional message for the exception. 106 | * @returns {BadRequestException} - A BadRequestException with the appropriate error code and message. 107 | */ 108 | static RESOURCE_ALREADY_EXISTS = (msg?: string) => { 109 | return new BadRequestException({ 110 | message: msg || 'Resource Already Exists', 111 | code: ExceptionConstants.BadRequestCodes.RESOURCE_ALREADY_EXISTS, 112 | }); 113 | }; 114 | 115 | /** 116 | * Create a BadRequestException for when a resource is not found. 117 | * @param {string} [msg] - Optional message for the exception. 118 | * @returns {BadRequestException} - A BadRequestException with the appropriate error code and message. 119 | */ 120 | static RESOURCE_NOT_FOUND = (msg?: string) => { 121 | return new BadRequestException({ 122 | message: msg || 'Resource Not Found', 123 | code: ExceptionConstants.BadRequestCodes.RESOURCE_NOT_FOUND, 124 | }); 125 | }; 126 | 127 | /** 128 | * Returns a new instance of BadRequestException representing a Validation Error. 129 | * @param msg A string representing the error message. 130 | * @returns An instance of BadRequestException representing the error. 131 | */ 132 | static VALIDATION_ERROR = (msg?: string) => { 133 | return new BadRequestException({ 134 | message: msg || 'Validation Error', 135 | code: ExceptionConstants.BadRequestCodes.VALIDATION_ERROR, 136 | }); 137 | }; 138 | 139 | /** 140 | * Returns a new instance of BadRequestException representing an Unexpected Error. 141 | * @param msg A string representing the error message. 142 | * @returns An instance of BadRequestException representing the error. 143 | */ 144 | static UNEXPECTED = (msg?: string) => { 145 | return new BadRequestException({ 146 | message: msg || 'Unexpected Error', 147 | code: ExceptionConstants.BadRequestCodes.UNEXPECTED_ERROR, 148 | }); 149 | }; 150 | 151 | /** 152 | * Returns a new instance of BadRequestException representing an Invalid Input. 153 | * @param msg A string representing the error message. 154 | * @returns An instance of BadRequestException representing the error. 155 | */ 156 | static INVALID_INPUT = (msg?: string) => { 157 | return new BadRequestException({ 158 | message: msg || 'Invalid Input', 159 | code: ExceptionConstants.BadRequestCodes.INVALID_INPUT, 160 | }); 161 | }; 162 | } 163 | -------------------------------------------------------------------------------- /src/exceptions/exceptions.constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This class defines constants for HTTP error codes. 3 | */ 4 | export class ExceptionConstants { 5 | /** 6 | * Constants for bad request HTTP error codes. 7 | */ 8 | public static readonly BadRequestCodes = { 9 | MISSING_REQUIRED_PARAMETER: 10001, // Required parameter is missing from request 10 | INVALID_PARAMETER_VALUE: 10002, // Parameter value is invalid 11 | UNSUPPORTED_PARAMETER: 10003, // Request contains unsupported parameter 12 | INVALID_CONTENT_TYPE: 10004, // Content type of request is invalid 13 | INVALID_REQUEST_BODY: 10005, // Request body is invalid 14 | RESOURCE_ALREADY_EXISTS: 10006, // Resource already exists 15 | RESOURCE_NOT_FOUND: 10007, // Resource not found 16 | REQUEST_TOO_LARGE: 10008, // Request is too large 17 | REQUEST_ENTITY_TOO_LARGE: 10009, // Request entity is too large 18 | REQUEST_URI_TOO_LONG: 10010, // Request URI is too long 19 | UNSUPPORTED_MEDIA_TYPE: 10011, // Request contains unsupported media type 20 | METHOD_NOT_ALLOWED: 10012, // Request method is not allowed 21 | HTTP_REQUEST_TIMEOUT: 10013, // Request has timed out 22 | VALIDATION_ERROR: 10014, // Request validation error 23 | UNEXPECTED_ERROR: 10015, // Unexpected error occurred 24 | INVALID_INPUT: 10016, // Invalid input 25 | }; 26 | 27 | /** 28 | * Constants for unauthorized HTTP error codes. 29 | */ 30 | public static readonly UnauthorizedCodes = { 31 | UNAUTHORIZED_ACCESS: 20001, // Unauthorized access to resource 32 | INVALID_CREDENTIALS: 20002, // Invalid credentials provided 33 | JSON_WEB_TOKEN_ERROR: 20003, // JSON web token error 34 | AUTHENTICATION_FAILED: 20004, // Authentication failed 35 | ACCESS_TOKEN_EXPIRED: 20005, // Access token has expired 36 | TOKEN_EXPIRED_ERROR: 20006, // Token has expired error 37 | UNEXPECTED_ERROR: 20007, // Unexpected error occurred 38 | RESOURCE_NOT_FOUND: 20008, // Resource not found 39 | USER_NOT_VERIFIED: 20009, // User not verified 40 | REQUIRED_RE_AUTHENTICATION: 20010, // Required re-authentication 41 | INVALID_RESET_PASSWORD_TOKEN: 20011, // Invalid reset password token 42 | }; 43 | 44 | /** 45 | * Constants for internal server error HTTP error codes. 46 | */ 47 | public static readonly InternalServerErrorCodes = { 48 | INTERNAL_SERVER_ERROR: 30001, // Internal server error 49 | DATABASE_ERROR: 30002, // Database error 50 | NETWORK_ERROR: 30003, // Network error 51 | THIRD_PARTY_SERVICE_ERROR: 30004, // Third party service error 52 | SERVER_OVERLOAD: 30005, // Server is overloaded 53 | UNEXPECTED_ERROR: 30006, // Unexpected error occurred 54 | }; 55 | 56 | /** 57 | * Constants for forbidden HTTP error codes. 58 | */ 59 | public static readonly ForbiddenCodes = { 60 | FORBIDDEN: 40001, // Access to resource is forbidden 61 | MISSING_PERMISSIONS: 40002, // User does not have the required permissions to access the resource 62 | EXCEEDED_RATE_LIMIT: 40003, // User has exceeded the rate limit for accessing the resource 63 | RESOURCE_NOT_FOUND: 40004, // The requested resource could not be found 64 | TEMPORARILY_UNAVAILABLE: 40005, // The requested resource is temporarily unavailable 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /src/exceptions/exceptions.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IException { 2 | message: string; 3 | code?: number; 4 | cause?: Error; 5 | description?: string; 6 | } 7 | 8 | export interface IHttpBadRequestExceptionResponse { 9 | code: number; 10 | message: string; 11 | description: string; 12 | timestamp: string; 13 | traceId: string; 14 | } 15 | 16 | export interface IHttpInternalServerErrorExceptionResponse { 17 | code: number; 18 | message: string; 19 | description: string; 20 | timestamp: string; 21 | traceId: string; 22 | } 23 | 24 | export interface IHttpUnauthorizedExceptionResponse { 25 | code: number; 26 | message: string; 27 | description: string; 28 | timestamp: string; 29 | traceId: string; 30 | } 31 | 32 | export interface IHttpForbiddenExceptionResponse { 33 | code: number; 34 | message: string; 35 | description: string; 36 | timestamp: string; 37 | traceId: string; 38 | } 39 | -------------------------------------------------------------------------------- /src/exceptions/forbidden.exception.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A custom exception that represents a Forbidden error. 3 | */ 4 | 5 | // Import required modules 6 | import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; 7 | import { HttpException, HttpStatus } from '@nestjs/common'; 8 | 9 | // Import internal modules 10 | import { ExceptionConstants } from './exceptions.constants'; 11 | import { IException, IHttpForbiddenExceptionResponse } from './exceptions.interface'; 12 | 13 | /** 14 | * A custom exception for forbidden errors. 15 | */ 16 | export class ForbiddenException extends HttpException { 17 | /** The error code. */ 18 | @ApiProperty({ 19 | enum: ExceptionConstants.ForbiddenCodes, 20 | description: 'You do not have permission to perform this action.', 21 | example: ExceptionConstants.ForbiddenCodes.MISSING_PERMISSIONS, 22 | }) 23 | code: number; 24 | 25 | /** The error that caused this exception. */ 26 | @ApiHideProperty() 27 | cause: Error; 28 | 29 | /** The error message. */ 30 | @ApiProperty({ 31 | description: 'Message for the exception', 32 | example: 'You do not have permission to perform this action.', 33 | }) 34 | message: string; 35 | 36 | /** The detailed description of the error. */ 37 | @ApiProperty({ 38 | description: 'A description of the error message.', 39 | }) 40 | description: string; 41 | 42 | /** Timestamp of the exception */ 43 | @ApiProperty({ 44 | description: 'Timestamp of the exception', 45 | format: 'date-time', 46 | example: '2022-12-31T23:59:59.999Z', 47 | }) 48 | timestamp: string; 49 | 50 | /** Trace ID of the request */ 51 | @ApiProperty({ 52 | description: 'Trace ID of the request', 53 | example: '65b5f773-df95-4ce5-a917-62ee832fcdd0', 54 | }) 55 | traceId: string; // Trace ID of the request 56 | 57 | /** 58 | * Constructs a new ForbiddenException object. 59 | * @param exception An object containing the exception details. 60 | * - message: A string representing the error message. 61 | * - cause: An object representing the cause of the error. 62 | * - description: A string describing the error in detail. 63 | * - code: A number representing internal status code which helpful in future for frontend 64 | */ 65 | constructor(exception: IException) { 66 | super(exception.message, HttpStatus.FORBIDDEN, { 67 | cause: exception.cause, 68 | description: exception.description, 69 | }); 70 | 71 | this.message = exception.message; 72 | this.cause = exception.cause; 73 | this.description = exception.description; 74 | this.code = exception.code; 75 | this.timestamp = new Date().toISOString(); 76 | } 77 | 78 | /** 79 | * Set the Trace ID of the ForbiddenException instance. 80 | * @param traceId A string representing the Trace ID. 81 | */ 82 | setTraceId = (traceId: string) => { 83 | this.traceId = traceId; 84 | }; 85 | 86 | /** 87 | * Generate an HTTP response body representing the ForbiddenException instance. 88 | * @param message A string representing the message to include in the response body. 89 | * @returns An object representing the HTTP response body. 90 | */ 91 | generateHttpResponseBody = (message?: string): IHttpForbiddenExceptionResponse => { 92 | return { 93 | code: this.code, 94 | message: message || this.message, 95 | description: this.description, 96 | timestamp: this.timestamp, 97 | traceId: this.traceId, 98 | }; 99 | }; 100 | 101 | /** 102 | * A static method to generate an exception forbidden error. 103 | * @param msg - An optional error message. 104 | * @returns An instance of the ForbiddenException class. 105 | */ 106 | static FORBIDDEN = (msg?: string) => { 107 | return new ForbiddenException({ 108 | message: msg || 'Access to this resource is forbidden.', 109 | code: ExceptionConstants.ForbiddenCodes.FORBIDDEN, 110 | }); 111 | }; 112 | 113 | /** 114 | * A static method to generate an exception missing permissions error. 115 | * @param msg - An optional error message. 116 | * @returns An instance of the ForbiddenException class. 117 | */ 118 | static MISSING_PERMISSIONS = (msg?: string) => { 119 | return new ForbiddenException({ 120 | message: msg || 'You do not have permission to perform this action.', 121 | code: ExceptionConstants.ForbiddenCodes.MISSING_PERMISSIONS, 122 | }); 123 | }; 124 | } 125 | -------------------------------------------------------------------------------- /src/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bad-request.exception'; 2 | export * from './internal-server-error.exception'; 3 | export * from './unauthorized.exception'; 4 | export * from './forbidden.exception'; 5 | -------------------------------------------------------------------------------- /src/exceptions/internal-server-error.exception.ts: -------------------------------------------------------------------------------- 1 | import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; 2 | import { HttpException, HttpStatus } from '@nestjs/common'; 3 | 4 | // Import internal files & modules 5 | import { ExceptionConstants } from './exceptions.constants'; 6 | import { IException, IHttpInternalServerErrorExceptionResponse } from './exceptions.interface'; 7 | 8 | // Exception class for Internal Server Error 9 | export class InternalServerErrorException extends HttpException { 10 | @ApiProperty({ 11 | enum: ExceptionConstants.InternalServerErrorCodes, 12 | description: 'A unique code identifying the error.', 13 | example: ExceptionConstants.InternalServerErrorCodes.INTERNAL_SERVER_ERROR, 14 | }) 15 | code: number; // Internal status code 16 | 17 | @ApiHideProperty() 18 | cause: Error; // Error object causing the exception 19 | 20 | @ApiProperty({ 21 | description: 'Message for the exception', 22 | example: 'An unexpected error occurred while processing your request.', 23 | }) 24 | message: string; // Message for the exception 25 | 26 | @ApiProperty({ 27 | description: 'A description of the error message.', 28 | example: 29 | 'The server encountered an unexpected condition that prevented it from fulfilling the request. This could be due to an error in the application code, a misconfiguration in the server, or an issue with the underlying infrastructure. Please try again later or contact the server administrator if the problem persists.', 30 | }) 31 | description: string; // Description of the exception 32 | 33 | @ApiProperty({ 34 | description: 'Timestamp of the exception', 35 | format: 'date-time', 36 | example: '2022-12-31T23:59:59.999Z', 37 | }) 38 | timestamp: string; // Timestamp of the exception 39 | 40 | @ApiProperty({ 41 | description: 'Trace ID of the request', 42 | example: '65b5f773-df95-4ce5-a917-62ee832fcdd0', 43 | }) 44 | traceId: string; // Trace ID of the request 45 | 46 | /** 47 | * Constructs a new InternalServerErrorException object. 48 | * @param exception An object containing the exception details. 49 | * - message: A string representing the error message. 50 | * - cause: An object representing the cause of the error. 51 | * - description: A string describing the error in detail. 52 | * - code: A number representing internal status code which helpful in future for frontend 53 | */ 54 | constructor(exception: IException) { 55 | super(exception.message, HttpStatus.INTERNAL_SERVER_ERROR, { 56 | cause: exception.cause, 57 | description: exception.description, 58 | }); 59 | this.message = exception.message; 60 | this.cause = exception.cause; 61 | this.description = exception.description; 62 | this.code = exception.code; 63 | this.timestamp = new Date().toISOString(); 64 | } 65 | 66 | /** 67 | * Set the Trace ID of the BadRequestException instance. 68 | * @param traceId A string representing the Trace ID. 69 | */ 70 | setTraceId = (traceId: string) => { 71 | this.traceId = traceId; 72 | }; 73 | 74 | /** 75 | * Generate an HTTP response body representing the BadRequestException instance. 76 | * @param message A string representing the message to include in the response body. 77 | * @returns An object representing the HTTP response body. 78 | */ 79 | generateHttpResponseBody = (message?: string): IHttpInternalServerErrorExceptionResponse => { 80 | return { 81 | code: this.code, 82 | message: message || this.message, 83 | description: this.description, 84 | timestamp: this.timestamp, 85 | traceId: this.traceId, 86 | }; 87 | }; 88 | 89 | /** 90 | * Returns a new instance of InternalServerErrorException with a standard error message and code 91 | * @param error Error object causing the exception 92 | * @returns A new instance of InternalServerErrorException 93 | */ 94 | static INTERNAL_SERVER_ERROR = (error: any) => { 95 | return new InternalServerErrorException({ 96 | message: 'We are sorry, something went wrong on our end. Please try again later or contact our support team for assistance.', 97 | code: ExceptionConstants.InternalServerErrorCodes.INTERNAL_SERVER_ERROR, 98 | cause: error, 99 | }); 100 | }; 101 | 102 | /** 103 | * Returns a new instance of InternalServerErrorException with a custom error message and code 104 | * @param error Error object causing the exception 105 | * @returns A new instance of InternalServerErrorException 106 | */ 107 | static UNEXPECTED_ERROR = (error: any) => { 108 | return new InternalServerErrorException({ 109 | message: 'An unexpected error occurred while processing the request.', 110 | code: ExceptionConstants.InternalServerErrorCodes.UNEXPECTED_ERROR, 111 | cause: error, 112 | }); 113 | }; 114 | } 115 | -------------------------------------------------------------------------------- /src/exceptions/unauthorized.exception.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A custom exception that represents a Unauthorized error. 3 | */ 4 | 5 | // Import required modules 6 | import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; 7 | import { HttpException, HttpStatus } from '@nestjs/common'; 8 | 9 | // Import internal modules 10 | import { ExceptionConstants } from './exceptions.constants'; 11 | import { IException, IHttpUnauthorizedExceptionResponse } from './exceptions.interface'; 12 | 13 | /** 14 | * A custom exception for unauthorized access errors. 15 | */ 16 | export class UnauthorizedException extends HttpException { 17 | /** The error code. */ 18 | @ApiProperty({ 19 | enum: ExceptionConstants.UnauthorizedCodes, 20 | description: 'A unique code identifying the error.', 21 | example: ExceptionConstants.UnauthorizedCodes.TOKEN_EXPIRED_ERROR, 22 | }) 23 | code: number; 24 | 25 | /** The error that caused this exception. */ 26 | @ApiHideProperty() 27 | cause: Error; 28 | 29 | /** The error message. */ 30 | @ApiProperty({ 31 | description: 'Message for the exception', 32 | example: 'The authentication token provided has expired.', 33 | }) 34 | message: string; 35 | 36 | /** The detailed description of the error. */ 37 | @ApiProperty({ 38 | description: 'A description of the error message.', 39 | example: 40 | 'This error message indicates that the authentication token provided with the request has expired, and therefore the server cannot verify the users identity.', 41 | }) 42 | description: string; 43 | 44 | /** Timestamp of the exception */ 45 | @ApiProperty({ 46 | description: 'Timestamp of the exception', 47 | format: 'date-time', 48 | example: '2022-12-31T23:59:59.999Z', 49 | }) 50 | timestamp: string; 51 | 52 | /** Trace ID of the request */ 53 | @ApiProperty({ 54 | description: 'Trace ID of the request', 55 | example: '65b5f773-df95-4ce5-a917-62ee832fcdd0', 56 | }) 57 | traceId: string; // Trace ID of the request 58 | 59 | /** 60 | * Constructs a new UnauthorizedException object. 61 | * @param exception An object containing the exception details. 62 | * - message: A string representing the error message. 63 | * - cause: An object representing the cause of the error. 64 | * - description: A string describing the error in detail. 65 | * - code: A number representing internal status code which helpful in future for frontend 66 | */ 67 | constructor(exception: IException) { 68 | super(exception.message, HttpStatus.UNAUTHORIZED, { 69 | cause: exception.cause, 70 | description: exception.description, 71 | }); 72 | 73 | this.message = exception.message; 74 | this.cause = exception.cause; 75 | this.description = exception.description; 76 | this.code = exception.code; 77 | this.timestamp = new Date().toISOString(); 78 | } 79 | 80 | /** 81 | * Set the Trace ID of the BadRequestException instance. 82 | * @param traceId A string representing the Trace ID. 83 | */ 84 | setTraceId = (traceId: string) => { 85 | this.traceId = traceId; 86 | }; 87 | 88 | /** 89 | * Generate an HTTP response body representing the BadRequestException instance. 90 | * @param message A string representing the message to include in the response body. 91 | * @returns An object representing the HTTP response body. 92 | */ 93 | generateHttpResponseBody = (message?: string): IHttpUnauthorizedExceptionResponse => { 94 | return { 95 | code: this.code, 96 | message: message || this.message, 97 | description: this.description, 98 | timestamp: this.timestamp, 99 | traceId: this.traceId, 100 | }; 101 | }; 102 | 103 | /** 104 | * A static method to generate an exception for token expiration error. 105 | * @param msg - An optional error message. 106 | * @returns An instance of the UnauthorizedException class. 107 | */ 108 | static TOKEN_EXPIRED_ERROR = (msg?: string) => { 109 | return new UnauthorizedException({ 110 | message: msg || 'The authentication token provided has expired.', 111 | code: ExceptionConstants.UnauthorizedCodes.TOKEN_EXPIRED_ERROR, 112 | }); 113 | }; 114 | 115 | /** 116 | * A static method to generate an exception for invalid JSON web token. 117 | * @param msg - An optional error message. 118 | * @returns An instance of the UnauthorizedException class. 119 | */ 120 | static JSON_WEB_TOKEN_ERROR = (msg?: string) => { 121 | return new UnauthorizedException({ 122 | message: msg || 'Invalid token specified.', 123 | code: ExceptionConstants.UnauthorizedCodes.JSON_WEB_TOKEN_ERROR, 124 | }); 125 | }; 126 | 127 | /** 128 | * A static method to generate an exception for unauthorized access to a resource. 129 | * @param description - An optional detailed description of the error. 130 | * @returns An instance of the UnauthorizedException class. 131 | */ 132 | static UNAUTHORIZED_ACCESS = (description?: string) => { 133 | return new UnauthorizedException({ 134 | message: 'Access to the requested resource is unauthorized.', 135 | code: ExceptionConstants.UnauthorizedCodes.UNAUTHORIZED_ACCESS, 136 | description, 137 | }); 138 | }; 139 | 140 | /** 141 | * Create a UnauthorizedException for when a resource is not found. 142 | * @param {string} [msg] - Optional message for the exception. 143 | * @returns {BadRequestException} - A UnauthorizedException with the appropriate error code and message. 144 | */ 145 | static RESOURCE_NOT_FOUND = (msg?: string) => { 146 | return new UnauthorizedException({ 147 | message: msg || 'Resource Not Found', 148 | code: ExceptionConstants.UnauthorizedCodes.RESOURCE_NOT_FOUND, 149 | }); 150 | }; 151 | 152 | /** 153 | * Create a UnauthorizedException for when a resource is not found. 154 | * @param {string} [msg] - Optional message for the exception. 155 | * @returns {BadRequestException} - A UnauthorizedException with the appropriate error code and message. 156 | */ 157 | static USER_NOT_VERIFIED = (msg?: string) => { 158 | return new UnauthorizedException({ 159 | message: msg || 'User not verified. Please complete verification process before attempting this action.', 160 | code: ExceptionConstants.UnauthorizedCodes.USER_NOT_VERIFIED, 161 | }); 162 | }; 163 | 164 | /** 165 | * A static method to generate an exception for unexpected errors. 166 | * @param error - The error that caused this exception. 167 | * @returns An instance of the UnauthorizedException class. 168 | */ 169 | static UNEXPECTED_ERROR = (error: any) => { 170 | return new UnauthorizedException({ 171 | message: 'An unexpected error occurred while processing the request. Please try again later.', 172 | code: ExceptionConstants.UnauthorizedCodes.UNEXPECTED_ERROR, 173 | cause: error, 174 | }); 175 | }; 176 | 177 | /** 178 | * A static method to generate an exception for when a forgot or change password time previous login token needs to be re-issued. 179 | * @param msg - An optional error message. 180 | * @returns - An instance of the UnauthorizedException class. 181 | */ 182 | static REQUIRED_RE_AUTHENTICATION = (msg?: string) => { 183 | return new UnauthorizedException({ 184 | message: 185 | msg || 186 | 'Your previous login session has been terminated due to a password change or reset. Please log in again with your new password.', 187 | code: ExceptionConstants.UnauthorizedCodes.REQUIRED_RE_AUTHENTICATION, 188 | }); 189 | }; 190 | 191 | /** 192 | * A static method to generate an exception for reset password token is invalid. 193 | * @param msg - An optional error message. 194 | * @returns - An instance of the UnauthorizedException class. 195 | */ 196 | static INVALID_RESET_PASSWORD_TOKEN = (msg?: string) => { 197 | return new UnauthorizedException({ 198 | message: msg || 'The reset password token provided is invalid. Please request a new reset password token.', 199 | code: ExceptionConstants.UnauthorizedCodes.INVALID_RESET_PASSWORD_TOKEN, 200 | }); 201 | }; 202 | } 203 | -------------------------------------------------------------------------------- /src/filters/all-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger } from '@nestjs/common'; 2 | import { HttpAdapterHost } from '@nestjs/core'; 3 | 4 | /** 5 | * Catches all exceptions thrown by the application and sends an appropriate HTTP response. 6 | */ 7 | @Catch() 8 | export class AllExceptionsFilter implements ExceptionFilter { 9 | private readonly logger = new Logger(AllExceptionsFilter.name); 10 | 11 | /** 12 | * Creates an instance of `AllExceptionsFilter`. 13 | * 14 | * @param {HttpAdapterHost} httpAdapterHost - the HTTP adapter host 15 | */ 16 | constructor(private readonly httpAdapterHost: HttpAdapterHost) {} 17 | 18 | /** 19 | * Catches an exception and sends an appropriate HTTP response. 20 | * 21 | * @param {*} exception - the exception to catch 22 | * @param {ArgumentsHost} host - the arguments host 23 | * @returns {void} 24 | */ 25 | catch(exception: any, host: ArgumentsHost): void { 26 | // Log the exception. 27 | this.logger.error(exception); 28 | 29 | // In certain situations `httpAdapter` might not be available in the 30 | // constructor method, thus we should resolve it here. 31 | const { httpAdapter } = this.httpAdapterHost; 32 | 33 | const ctx = host.switchToHttp(); 34 | 35 | const httpStatus = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; 36 | 37 | const request = ctx.getRequest(); 38 | 39 | // Construct the response body. 40 | const responseBody = { 41 | error: exception.code, 42 | message: exception.message, 43 | description: exception.description, 44 | timestamp: new Date().toISOString(), 45 | traceId: request.id, 46 | }; 47 | 48 | // Send the HTTP response. 49 | httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/filters/bad-request-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter, Logger } from '@nestjs/common'; 2 | import { HttpAdapterHost } from '@nestjs/core'; 3 | 4 | import { BadRequestException } from '../exceptions/bad-request.exception'; 5 | 6 | /** 7 | * A filter to handle `BadRequestException`. 8 | */ 9 | @Catch(BadRequestException) 10 | export class BadRequestExceptionFilter implements ExceptionFilter { 11 | private readonly logger = new Logger(BadRequestException.name); 12 | 13 | /** 14 | * Constructs a new instance of `BadRequestExceptionFilter`. 15 | * @param httpAdapterHost - The HttpAdapterHost instance to be used. 16 | */ 17 | constructor(private readonly httpAdapterHost: HttpAdapterHost) {} 18 | 19 | /** 20 | * Handles the `BadRequestException` and transforms it into a JSON response. 21 | * @param exception - The `BadRequestException` instance that was thrown. 22 | * @param host - The `ArgumentsHost` instance that represents the current execution context. 23 | */ 24 | catch(exception: BadRequestException, host: ArgumentsHost): void { 25 | // Logs the exception details at the verbose level. 26 | this.logger.verbose(exception); 27 | 28 | // In certain situations `httpAdapter` might not be available in the constructor method, 29 | // thus we should resolve it here. 30 | const { httpAdapter } = this.httpAdapterHost; 31 | 32 | // Retrieves the current HTTP context from the `ArgumentsHost`. 33 | const ctx = host.switchToHttp(); 34 | 35 | // Retrieves the HTTP status code from the `BadRequestException`. 36 | const httpStatus = exception.getStatus(); 37 | 38 | // Retrieves the request object from the HTTP context. 39 | const request = ctx.getRequest(); 40 | 41 | // Sets the trace ID from the request object to the exception. 42 | exception.setTraceId(request.id); 43 | 44 | // Constructs the response body object. 45 | const responseBody = exception.generateHttpResponseBody(); 46 | 47 | // Uses the HTTP adapter to send the response with the constructed response body 48 | // and the HTTP status code. 49 | httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/filters/forbidden-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter, Logger } from '@nestjs/common'; 2 | import { HttpAdapterHost } from '@nestjs/core'; 3 | 4 | import { ForbiddenException } from '../exceptions'; 5 | 6 | /** 7 | * Exception filter to handle unauthorized exceptions 8 | */ 9 | @Catch(ForbiddenException) 10 | export class ForbiddenExceptionFilter implements ExceptionFilter { 11 | private readonly logger = new Logger(ForbiddenExceptionFilter.name); 12 | 13 | constructor(private readonly httpAdapterHost: HttpAdapterHost) {} 14 | 15 | /** 16 | * Method to handle unauthorized exceptions 17 | * @param exception - The thrown unauthorized exception 18 | * @param host - The arguments host 19 | */ 20 | catch(exception: ForbiddenException, host: ArgumentsHost): void { 21 | this.logger.warn(exception); 22 | 23 | // In certain situations `httpAdapter` might not be available in the 24 | // constructor method, thus we should resolve it here. 25 | const { httpAdapter } = this.httpAdapterHost; 26 | 27 | const ctx = host.switchToHttp(); 28 | const httpStatus = exception.getStatus(); 29 | 30 | // Example of fetching path to attach path inside response object 31 | const request = ctx.getRequest(); 32 | // const path = httpAdapter.getRequestUrl(request); 33 | 34 | // Sets the trace ID from the request object to the exception. 35 | exception.setTraceId(request.id); 36 | 37 | // Constructs the response body object. 38 | const responseBody = exception.generateHttpResponseBody(); 39 | 40 | // Uses the HTTP adapter to send the response with the constructed response body 41 | // and the HTTP status code. 42 | httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './not-found-exception.filter'; 2 | export * from './all-exception.filter'; 3 | export * from './bad-request-exception.filter'; 4 | export * from './unauthorized-exception.filter'; 5 | export * from './forbidden-exception.filter'; 6 | export * from './validator-exception.filter'; 7 | -------------------------------------------------------------------------------- /src/filters/internal-server-error-exception.filter.ts: -------------------------------------------------------------------------------- 1 | // Importing required modules and classes from NestJS 2 | import { ArgumentsHost, Catch, ExceptionFilter, Logger } from '@nestjs/common'; 3 | import { HttpAdapterHost } from '@nestjs/core'; 4 | 5 | import { InternalServerErrorException } from '../exceptions/internal-server-error.exception'; 6 | 7 | /** 8 | * A filter to handle `InternalServerErrorException`. 9 | */ 10 | @Catch(InternalServerErrorException) 11 | export class InternalServerErrorExceptionFilter implements ExceptionFilter { 12 | private readonly logger = new Logger(InternalServerErrorException.name); 13 | 14 | /** 15 | * Constructs a new instance of `InternalServerErrorExceptionFilter`. 16 | * @param httpAdapterHost - The HttpAdapterHost instance to be used. 17 | */ 18 | constructor(private readonly httpAdapterHost: HttpAdapterHost) {} 19 | 20 | /** 21 | * Handles the `InternalServerErrorException` and transforms it into a JSON response. 22 | * @param exception - The `InternalServerErrorException` instance that was thrown. 23 | * @param host - The `ArgumentsHost` instance that represents the current execution context. 24 | */ 25 | catch(exception: InternalServerErrorException, host: ArgumentsHost): void { 26 | // Logs the exception details at the error level. 27 | this.logger.error(exception); 28 | 29 | // In certain situations `httpAdapter` might not be available in the constructor method, 30 | // thus we should resolve it here. 31 | const { httpAdapter } = this.httpAdapterHost; 32 | 33 | // Retrieves the current HTTP context from the `ArgumentsHost`. 34 | const ctx = host.switchToHttp(); 35 | 36 | // Retrieves the HTTP status code from the `InternalServerErrorException`. 37 | const httpStatus = exception.getStatus(); 38 | 39 | // Retrieves the request object from the HTTP context. 40 | const request = ctx.getRequest(); 41 | 42 | // Sets the trace ID from the request object to the exception. 43 | exception.setTraceId(request.id); 44 | 45 | // Constructs the response body object. 46 | const responseBody = exception.generateHttpResponseBody(); 47 | 48 | // Uses the HTTP adapter to send the response with the constructed response body 49 | // and the HTTP status code. 50 | httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/filters/not-found-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger, NotFoundException } from '@nestjs/common'; 2 | import { HttpAdapterHost } from '@nestjs/core'; 3 | 4 | /** 5 | * Catches all exceptions thrown by the application and sends an appropriate HTTP response. 6 | */ 7 | @Catch(NotFoundException) 8 | export class NotFoundExceptionFilter implements ExceptionFilter { 9 | private readonly logger = new Logger(NotFoundExceptionFilter.name); 10 | 11 | /** 12 | * Creates an instance of `NotFoundExceptionFilter`. 13 | * 14 | * @param {HttpAdapterHost} httpAdapterHost - the HTTP adapter host 15 | */ 16 | constructor(private readonly httpAdapterHost: HttpAdapterHost) {} 17 | 18 | /** 19 | * Catches an exception and sends an appropriate HTTP response. 20 | * 21 | * @param {*} exception - the exception to catch 22 | * @param {ArgumentsHost} host - the arguments host 23 | * @returns {void} 24 | */ 25 | catch(exception: any, host: ArgumentsHost): void { 26 | // Log the exception. 27 | 28 | // In certain situations `httpAdapter` might not be available in the 29 | // constructor method, thus we should resolve it here. 30 | const { httpAdapter } = this.httpAdapterHost; 31 | 32 | const ctx = host.switchToHttp(); 33 | 34 | const httpStatus = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; 35 | 36 | const request = ctx.getRequest(); 37 | 38 | // Construct the response body. 39 | const responseBody = { 40 | error: exception.code, 41 | message: exception.message, 42 | description: exception.description, 43 | traceId: request.id, 44 | }; 45 | 46 | // Send the HTTP response. 47 | httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/filters/unauthorized-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter, Logger } from '@nestjs/common'; 2 | import { HttpAdapterHost } from '@nestjs/core'; 3 | 4 | import { UnauthorizedException } from '../exceptions/unauthorized.exception'; 5 | 6 | /** 7 | * Exception filter to handle unauthorized exceptions 8 | */ 9 | @Catch(UnauthorizedException) 10 | export class UnauthorizedExceptionFilter implements ExceptionFilter { 11 | private readonly logger = new Logger(UnauthorizedExceptionFilter.name); 12 | 13 | constructor(private readonly httpAdapterHost: HttpAdapterHost) {} 14 | 15 | /** 16 | * Method to handle unauthorized exceptions 17 | * @param exception - The thrown unauthorized exception 18 | * @param host - The arguments host 19 | */ 20 | catch(exception: UnauthorizedException, host: ArgumentsHost): void { 21 | this.logger.warn(exception); 22 | 23 | // In certain situations `httpAdapter` might not be available in the 24 | // constructor method, thus we should resolve it here. 25 | const { httpAdapter } = this.httpAdapterHost; 26 | 27 | const ctx = host.switchToHttp(); 28 | const httpStatus = exception.getStatus(); 29 | 30 | // Example of fetching path to attach path inside response object 31 | const request = ctx.getRequest(); 32 | // const path = httpAdapter.getRequestUrl(request); 33 | 34 | // Sets the trace ID from the request object to the exception. 35 | exception.setTraceId(request.id); 36 | 37 | // Constructs the response body object. 38 | const responseBody = exception.generateHttpResponseBody(); 39 | 40 | // Uses the HTTP adapter to send the response with the constructed response body 41 | // and the HTTP status code. 42 | httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/filters/validator-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus, Logger } from '@nestjs/common'; 2 | import { HttpAdapterHost } from '@nestjs/core'; 3 | import { ValidationError } from 'class-validator'; 4 | 5 | import { BadRequestException } from '../exceptions/bad-request.exception'; 6 | 7 | /** 8 | * An exception filter to handle validation errors thrown by class-validator. 9 | */ 10 | @Catch(ValidationError) 11 | export class ValidationExceptionFilter implements ExceptionFilter { 12 | private readonly logger = new Logger(ValidationExceptionFilter.name); 13 | 14 | constructor(private readonly httpAdapterHost: HttpAdapterHost) {} 15 | 16 | /** 17 | * Handle a validation error. 18 | * @param exception The validation error object. 19 | * @param host The arguments host object. 20 | */ 21 | catch(exception: ValidationError, host: ArgumentsHost): void { 22 | this.logger.verbose(exception); 23 | 24 | // In certain situations `httpAdapter` might not be available in the 25 | // constructor method, thus we should resolve it here. 26 | const { httpAdapter } = this.httpAdapterHost; 27 | 28 | const ctx = host.switchToHttp(); 29 | const httpStatus = HttpStatus.UNPROCESSABLE_ENTITY; 30 | 31 | const request = ctx.getRequest(); 32 | // Example of fetching path to attach path inside response object 33 | // const path = httpAdapter.getRequestUrl(request); 34 | 35 | const errorMsg = exception.constraints || exception.children[0].constraints; 36 | 37 | // Create a new BadRequestException with the validation error message. 38 | const err = BadRequestException.VALIDATION_ERROR(Object.values(errorMsg)[0]); 39 | 40 | const responseBody = { 41 | error: err.code, 42 | message: err.message, 43 | timestamp: new Date().toISOString(), 44 | traceId: request.id, 45 | }; 46 | 47 | // Uses the HTTP adapter to send the response with the constructed response body 48 | // and the HTTP status code. 49 | httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | // Import external modules 2 | import * as cluster from 'cluster'; 3 | import * as os from 'os'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 6 | import { Logger } from '@nestjs/common'; 7 | import { NestFactory } from '@nestjs/core'; 8 | import { Logger as Pino } from 'nestjs-pino'; 9 | 10 | // Import internal modules 11 | import { AppModule } from './app.module'; 12 | 13 | // Create a logger for the bootstrap process 14 | const logger = new Logger('bootstrap'); 15 | 16 | // Define the main function 17 | async function bootstrap() { 18 | // Create the NestJS application instance 19 | const app = await NestFactory.create(AppModule, { 20 | bufferLogs: true, 21 | }); 22 | 23 | // Use the Pino logger for the application 24 | app.useLogger(app.get(Pino)); 25 | 26 | // Allow all origins 27 | app.enableCors(); 28 | 29 | // Define the Swagger options and document 30 | const options = new DocumentBuilder() 31 | .setTitle('NestJS Starter API') 32 | .setDescription('The API for the NestJS Starter project') 33 | .setVersion('1.0') 34 | .addBearerAuth() 35 | .build(); 36 | const document = SwaggerModule.createDocument(app, options); 37 | 38 | // Set up the Swagger UI endpoint 39 | SwaggerModule.setup('docs', app, document, { 40 | swaggerOptions: { 41 | tagsSorter: 'alpha', 42 | operationsSorter: 'alpha', 43 | }, 44 | }); 45 | 46 | // Get the configuration service from the application 47 | const configService = app.get(ConfigService); 48 | 49 | // Get the port number from the configuration 50 | const PORT = configService.get('port'); 51 | 52 | // Start the application 53 | await app.listen(PORT); 54 | 55 | // Log a message to indicate that the application is running 56 | logger.log(`Application listening on port ${PORT}`); 57 | } 58 | 59 | // Check if clustering is enabled 60 | if (process.env.CLUSTERING === 'true') { 61 | // Get the number of CPUs on the machine 62 | const numCPUs = os.cpus().length; 63 | 64 | // If the current process is the master process 65 | if ((cluster as any).isMaster) { 66 | logger.log(`Master process is running with PID ${process.pid}`); 67 | 68 | // Fork workers for each available CPU 69 | for (let i = 0; i < numCPUs; i += 1) { 70 | (cluster as any).fork(); 71 | } 72 | 73 | // Log when a worker process exits 74 | (cluster as any).on('exit', (worker, code, signal) => { 75 | logger.debug(`Worker process ${worker.process.pid} exited with code ${code} and signal ${signal}`); 76 | }); 77 | } else { 78 | // If the current process is a worker process, call the bootstrap function to start the application 79 | bootstrap(); 80 | } 81 | } else { 82 | // Call the bootstrap function to start the application 83 | bootstrap(); 84 | } 85 | -------------------------------------------------------------------------------- /src/modules/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { ApiBadRequestResponse, ApiInternalServerErrorResponse, ApiOkResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; 2 | import { Body, Controller, HttpCode, Post, ValidationPipe } from '@nestjs/common'; 3 | 4 | import { AuthService } from './auth.service'; 5 | import { LoginReqDto, LoginResDto, SignupReqDto, SignupResDto } from './dtos'; 6 | 7 | import { BadRequestException } from '../../exceptions/bad-request.exception'; 8 | import { InternalServerErrorException } from '../../exceptions/internal-server-error.exception'; 9 | import { UnauthorizedException } from '../../exceptions/unauthorized.exception'; 10 | 11 | @ApiBadRequestResponse({ 12 | type: BadRequestException, 13 | }) 14 | @ApiInternalServerErrorResponse({ 15 | type: InternalServerErrorException, 16 | }) 17 | @ApiUnauthorizedResponse({ 18 | type: UnauthorizedException, 19 | }) 20 | @ApiTags('Auth') 21 | @Controller('auth') 22 | export class AuthController { 23 | constructor(private readonly authService: AuthService) {} 24 | 25 | // POST /auth/signup 26 | @ApiOkResponse({ 27 | type: SignupResDto, 28 | }) 29 | @HttpCode(200) 30 | @Post('signup') 31 | async signup(@Body(ValidationPipe) signupReqDto: SignupReqDto): Promise { 32 | return this.authService.signup(signupReqDto); 33 | } 34 | 35 | // POST /auth/login 36 | @ApiOkResponse({ 37 | type: LoginResDto, 38 | }) 39 | @HttpCode(200) 40 | @Post('login') 41 | async login(@Body(ValidationPipe) loginReqDto: LoginReqDto): Promise { 42 | return this.authService.login(loginReqDto); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | // Importing the required libraries 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { Module } from '@nestjs/common'; 5 | import { PassportModule } from '@nestjs/passport'; 6 | 7 | // Importing the required internal files 8 | import { JwtUserStrategy } from './strategies/jwt-user.strategy'; 9 | 10 | // Importing the required external modules and files 11 | import { AuthController } from './auth.controller'; 12 | import { AuthService } from './auth.service'; 13 | import { UserModule } from '../user/user.module'; 14 | import { WorkspaceModule } from '../workspace/workspace.module'; 15 | 16 | @Module({ 17 | imports: [ 18 | PassportModule.register({ defaultStrategy: 'jwt' }), 19 | JwtModule.registerAsync({ 20 | inject: [ConfigService], 21 | imports: [ConfigModule], 22 | useFactory: async (configService: ConfigService) => ({ 23 | privateKey: configService.get('jwt.privateKey'), 24 | publicKey: configService.get('jwt.publicKey'), 25 | signOptions: { expiresIn: configService.get('jwt.expiresIn'), algorithm: 'RS256' }, 26 | verifyOptions: { 27 | algorithms: ['RS256'], 28 | }, 29 | }), 30 | }), 31 | UserModule, 32 | WorkspaceModule, 33 | ], 34 | providers: [JwtUserStrategy, AuthService], 35 | controllers: [AuthController], 36 | exports: [JwtUserStrategy, PassportModule], 37 | }) 38 | export class AuthModule {} 39 | -------------------------------------------------------------------------------- /src/modules/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | // External dependencies 2 | import * as bcrypt from 'bcryptjs'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { JwtService } from '@nestjs/jwt'; 5 | 6 | // Internal dependencies 7 | import { JwtUserPayload } from './interfaces/jwt-user-payload.interface'; 8 | import { LoginReqDto, LoginResDto, SignupReqDto, SignupResDto } from './dtos'; 9 | 10 | // Other modules dependencies 11 | import { User } from '../user/user.schema'; 12 | import { UserQueryService } from '../user/user.query.service'; 13 | import { WorkspaceQueryService } from '../workspace/workspace.query-service'; 14 | 15 | // Shared dependencies 16 | import { BadRequestException } from '../../exceptions/bad-request.exception'; 17 | import { UnauthorizedException } from '../../exceptions/unauthorized.exception'; 18 | 19 | @Injectable() 20 | export class AuthService { 21 | private readonly SALT_ROUNDS = 10; 22 | 23 | constructor( 24 | private readonly userQueryService: UserQueryService, 25 | private readonly workspaceQueryService: WorkspaceQueryService, 26 | private readonly jwtService: JwtService, 27 | ) {} 28 | 29 | async signup(signupReqDto: SignupReqDto): Promise { 30 | const { email, password, workspaceName, name } = signupReqDto; 31 | 32 | const user = await this.userQueryService.findByEmail(email); 33 | if (user) { 34 | throw BadRequestException.RESOURCE_ALREADY_EXISTS(`User with email ${email} already exists`); 35 | } 36 | 37 | const workspacePayload = { 38 | name: workspaceName, 39 | }; 40 | const workspace = await this.workspaceQueryService.create(workspacePayload); 41 | 42 | // Hash password 43 | const saltOrRounds = this.SALT_ROUNDS; 44 | const hashedPassword = await bcrypt.hash(password, saltOrRounds); 45 | 46 | const userPayload: User = { 47 | email, 48 | password: hashedPassword, 49 | workspace: workspace._id, 50 | name, 51 | verified: true, 52 | registerCode: this.generateCode(), 53 | verificationCode: null, 54 | verificationCodeExpiry: null, 55 | resetToken: null, 56 | }; 57 | 58 | await this.userQueryService.create(userPayload); 59 | 60 | return { 61 | message: 'User created successfully', 62 | }; 63 | } 64 | 65 | /** 66 | * Generates a random six digit OTP 67 | * @returns {number} - returns the generated OTP 68 | */ 69 | generateCode(): number { 70 | const OTP_MIN = 100000; 71 | const OTP_MAX = 999999; 72 | return Math.floor(Math.random() * (OTP_MAX - OTP_MIN + 1)) + OTP_MIN; 73 | } 74 | 75 | async login(loginReqDto: LoginReqDto): Promise { 76 | const { email, password } = loginReqDto; 77 | 78 | const user = await this.userQueryService.findByEmail(email); 79 | if (!user) { 80 | throw UnauthorizedException.UNAUTHORIZED_ACCESS('Invalid credentials'); 81 | } 82 | 83 | const isPasswordValid = await bcrypt.compare(password, user.password); 84 | if (!isPasswordValid) { 85 | throw UnauthorizedException.UNAUTHORIZED_ACCESS('Invalid credentials'); 86 | } 87 | 88 | const payload: JwtUserPayload = { 89 | user: user._id, 90 | email: user.email, 91 | code: user.registerCode, 92 | }; 93 | const accessToken = await this.jwtService.signAsync(payload); 94 | 95 | delete user.password; 96 | 97 | return { 98 | message: 'Login successful', 99 | accessToken, 100 | user, 101 | }; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/modules/auth/decorators/get-user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, createParamDecorator } from '@nestjs/common'; 2 | 3 | export const GetUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => { 4 | const request = ctx.switchToHttp().getRequest(); 5 | return request.user; 6 | }); 7 | -------------------------------------------------------------------------------- /src/modules/auth/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './signup.req.dto'; 2 | export * from './signup.res.dto'; 3 | 4 | export * from './login.req.dto'; 5 | export * from './login.res.dto'; 6 | -------------------------------------------------------------------------------- /src/modules/auth/dtos/login.req.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsEmail, IsNotEmpty, IsString, Matches, MaxLength, MinLength } from 'class-validator'; 3 | 4 | export class LoginReqDto { 5 | @ApiProperty({ description: 'Email address of the user', example: 'john@example.com' }) 6 | @IsNotEmpty() 7 | @IsEmail() 8 | email: string; 9 | 10 | @ApiProperty({ 11 | description: 12 | 'Password for the user account. Must be at least 8 characters and contain at least one uppercase letter, one lowercase letter, and one special character.', 13 | example: 'MySecurePassword!@#', 14 | }) 15 | @IsString() 16 | @MinLength(8) 17 | @MaxLength(20) 18 | @Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { 19 | message: 'Password is too weak', 20 | }) 21 | password: string; 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/auth/dtos/login.res.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | import { User } from '../../user/user.schema'; 4 | 5 | export class LoginResDto { 6 | @ApiProperty({ 7 | description: 'Message to the user', 8 | example: 'Login successful', 9 | }) 10 | message: string; 11 | 12 | @ApiProperty({ 13 | description: 'Access token for the user', 14 | }) 15 | accessToken: string; 16 | 17 | @ApiProperty({ 18 | description: 'User details', 19 | type: User, 20 | }) 21 | user: User; 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/auth/dtos/signup.req.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsEmail, IsNotEmpty, IsString, Matches, MaxLength, MinLength } from 'class-validator'; 3 | 4 | export class SignupReqDto { 5 | @ApiProperty({ description: 'Email address of the user', example: 'john@example.com' }) 6 | @IsNotEmpty() 7 | @IsEmail() 8 | email: string; 9 | 10 | @ApiProperty({ description: 'Full name of the user', example: 'John Doe' }) 11 | @IsNotEmpty() 12 | @IsString() 13 | name: string; 14 | 15 | @ApiProperty({ 16 | description: 17 | 'Password for the user account. Must be at least 8 characters and contain at least one uppercase letter, one lowercase letter, and one special character.', 18 | example: 'MySecure@Password!#', 19 | }) 20 | @IsString() 21 | @MinLength(8) 22 | @MaxLength(20) 23 | @Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { 24 | message: 'Password is too weak', 25 | }) 26 | password: string; 27 | 28 | @ApiProperty({ description: 'Name of the workspace', example: 'My Company' }) 29 | @IsNotEmpty() 30 | @IsString() 31 | workspaceName: string; 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/auth/dtos/signup.res.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class SignupResDto { 4 | @ApiProperty({ 5 | example: 'User account created successfully', 6 | }) 7 | message: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/auth/guards/jwt-user-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { AuthGuard } from '@nestjs/passport'; 2 | import { ExecutionContext, Injectable } from '@nestjs/common'; 3 | 4 | import { UnauthorizedException } from '../../../exceptions/unauthorized.exception'; 5 | 6 | @Injectable() 7 | export class JwtUserAuthGuard extends AuthGuard('authUser') { 8 | JSON_WEB_TOKEN_ERROR = 'JsonWebTokenError'; 9 | 10 | TOKEN_EXPIRED_ERROR = 'TokenExpiredError'; 11 | 12 | canActivate(context: ExecutionContext) { 13 | // Add your custom authentication logic here 14 | // for example, call super.logIn(request) to establish a session. 15 | return super.canActivate(context); 16 | } 17 | 18 | handleRequest(err: any, user: any, info: Error, context: any, status: any) { 19 | // You can throw an exception based on either "info" or "err" arguments 20 | if (info?.name === this.JSON_WEB_TOKEN_ERROR) { 21 | throw UnauthorizedException.JSON_WEB_TOKEN_ERROR(); 22 | } else if (info?.name === this.TOKEN_EXPIRED_ERROR) { 23 | throw UnauthorizedException.TOKEN_EXPIRED_ERROR(); 24 | } else if (info) { 25 | throw UnauthorizedException.UNAUTHORIZED_ACCESS(info.message); 26 | } else if (err) { 27 | throw err; 28 | } 29 | 30 | return super.handleRequest(err, user, info, context, status); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/auth/interfaces/jwt-user-payload.interface.ts: -------------------------------------------------------------------------------- 1 | import { Types } from 'mongoose'; 2 | 3 | export interface JwtUserPayload { 4 | user: string | Types.ObjectId; 5 | email: string; 6 | code: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/modules/auth/strategies/jwt-user.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@nestjs/config'; 2 | import { ExtractJwt, Strategy } from 'passport-jwt'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { PassportStrategy } from '@nestjs/passport'; 5 | 6 | import { JwtUserPayload } from '../interfaces/jwt-user-payload.interface'; 7 | import { UnauthorizedException } from '../../../exceptions/unauthorized.exception'; 8 | import { UserQueryService } from '../../user/user.query.service'; 9 | 10 | @Injectable() 11 | export class JwtUserStrategy extends PassportStrategy(Strategy, 'authUser') { 12 | constructor( 13 | private readonly configService: ConfigService, 14 | private readonly userQueryService: UserQueryService, 15 | ) { 16 | super({ 17 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 18 | secretOrKey: configService.get('jwt.publicKey'), 19 | }); 20 | } 21 | 22 | // validate method is called by passport-jwt when it has verified the token signature 23 | async validate(payload: JwtUserPayload) { 24 | const user = await this.userQueryService.findById(payload.user); 25 | if (!user) { 26 | throw UnauthorizedException.UNAUTHORIZED_ACCESS(); 27 | } 28 | if (!user.verified) { 29 | throw UnauthorizedException.USER_NOT_VERIFIED(); 30 | } 31 | if (payload.code !== user.registerCode) { 32 | throw UnauthorizedException.REQUIRED_RE_AUTHENTICATION(); 33 | } 34 | delete user.password; // remove password from the user object 35 | return user; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/modules/user/dtos/get-profile.res.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | import { User } from '../user.schema'; 4 | 5 | export class GetProfileResDto { 6 | @ApiProperty({ 7 | description: 'Message to the user', 8 | example: 'Login successful', 9 | }) 10 | message: string; 11 | 12 | @ApiProperty({ 13 | description: 'User details', 14 | type: User, 15 | }) 16 | user: User; 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/user/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-profile.res.dto'; 2 | -------------------------------------------------------------------------------- /src/modules/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | // External dependencies 2 | import { ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger'; 3 | import { Controller, Get, HttpCode, Logger, UseGuards } from '@nestjs/common'; 4 | 5 | // Internal dependencies 6 | import { GetProfileResDto } from './dtos'; 7 | import { UserDocument } from './user.schema'; 8 | 9 | // Other modules dependencies 10 | import { GetUser } from '../auth/decorators/get-user.decorator'; 11 | import { JwtUserAuthGuard } from '../auth/guards/jwt-user-auth.guard'; 12 | 13 | @ApiBearerAuth() 14 | @ApiTags('User') 15 | @UseGuards(JwtUserAuthGuard) 16 | @Controller('user') 17 | export class UserController { 18 | private readonly logger = new Logger(UserController.name); 19 | 20 | // GET /user/me 21 | @HttpCode(200) 22 | @ApiOkResponse({ 23 | type: GetProfileResDto, 24 | }) 25 | @Get('me') 26 | async getFullAccess(@GetUser() user: UserDocument): Promise { 27 | this.logger.debug(`User ${user.email} requested their profile`); 28 | return { 29 | message: 'Profile retrieved successfully', 30 | user, 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | 4 | import { DatabaseCollectionNames } from '../../shared/enums/db.enum'; 5 | import { UserController } from './user.controller'; 6 | import { UserQueryService } from './user.query.service'; 7 | import { UserRepository } from './user.repository'; 8 | import { UserSchema } from './user.schema'; 9 | 10 | @Module({ 11 | imports: [MongooseModule.forFeature([{ name: DatabaseCollectionNames.USER, schema: UserSchema }])], 12 | providers: [UserQueryService, UserRepository], 13 | exports: [UserQueryService], 14 | controllers: [UserController], 15 | }) 16 | export class UserModule {} 17 | -------------------------------------------------------------------------------- /src/modules/user/user.query.service.ts: -------------------------------------------------------------------------------- 1 | // Objective: Implement the user query service to handle the user queries 2 | // External dependencies 3 | import { Injectable } from '@nestjs/common'; 4 | 5 | // Internal dependencies 6 | import { User, UserDocument } from './user.schema'; 7 | import { UserRepository } from './user.repository'; 8 | 9 | // Other modules dependencies 10 | 11 | // Shared dependencies 12 | import { Identifier } from '../../shared/types/schema.type'; 13 | import { InternalServerErrorException } from '../../exceptions/internal-server-error.exception'; 14 | 15 | @Injectable() 16 | export class UserQueryService { 17 | constructor(private readonly userRepository: UserRepository) {} 18 | 19 | // findByEmail is a method that finds a user by their email address 20 | async findByEmail(email: string): Promise { 21 | try { 22 | return await this.userRepository.findOne({ email }); 23 | } catch (error) { 24 | throw InternalServerErrorException.INTERNAL_SERVER_ERROR(error); 25 | } 26 | } 27 | 28 | // findById is a method that finds a user by their unique identifier 29 | async findById(id: Identifier): Promise { 30 | try { 31 | return await this.userRepository.findById(id); 32 | } catch (error) { 33 | throw InternalServerErrorException.INTERNAL_SERVER_ERROR(error); 34 | } 35 | } 36 | 37 | // create is a method that creates a new user 38 | async create(user: User): Promise { 39 | try { 40 | return await this.userRepository.create(user); 41 | } catch (error) { 42 | throw InternalServerErrorException.INTERNAL_SERVER_ERROR(error); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/modules/user/user.repository.ts: -------------------------------------------------------------------------------- 1 | // Purpose: User repository for user module. 2 | // External dependencies 3 | import { FilterQuery, Model, QueryOptions, Types, UpdateQuery } from 'mongoose'; 4 | import { InjectModel } from '@nestjs/mongoose'; 5 | import { Injectable } from '@nestjs/common'; 6 | 7 | // Internal dependencies 8 | import { User, UserDocument } from './user.schema'; 9 | 10 | // Shared dependencies 11 | import { DatabaseCollectionNames } from '../../shared/enums/db.enum'; 12 | 13 | @Injectable() 14 | export class UserRepository { 15 | constructor(@InjectModel(DatabaseCollectionNames.USER) private userModel: Model) {} 16 | 17 | async find(filter: FilterQuery): Promise { 18 | return this.userModel.find(filter).lean(); 19 | } 20 | 21 | async findById(id: string | Types.ObjectId): Promise { 22 | return this.userModel.findById(id).lean(); 23 | } 24 | 25 | async findOne(filter: FilterQuery): Promise { 26 | return this.userModel.findOne(filter).lean(); 27 | } 28 | 29 | async create(user: User): Promise { 30 | return this.userModel.create(user); 31 | } 32 | 33 | async findOneAndUpdate( 34 | filter: FilterQuery, 35 | update: UpdateQuery, 36 | options: QueryOptions, 37 | ): Promise { 38 | return this.userModel.findOneAndUpdate(filter, update, options); 39 | } 40 | 41 | async findByIdAndUpdate(id, update: UpdateQuery, options: QueryOptions): Promise { 42 | return this.userModel.findByIdAndUpdate(id, update, options); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/modules/user/user.schema.ts: -------------------------------------------------------------------------------- 1 | import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; 2 | 3 | import { HydratedDocument, Schema as MongooseSchema, Types } from 'mongoose'; 4 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 5 | 6 | import { DatabaseCollectionNames } from '../../shared/enums'; 7 | import { Identifier } from '../../shared/types'; 8 | 9 | @Schema({ 10 | timestamps: true, 11 | collection: DatabaseCollectionNames.USER, 12 | }) 13 | export class User { 14 | // _id is the unique identifier of the user 15 | @ApiProperty({ 16 | description: 'The unique identifier of the user', 17 | example: '643405452324db8c464c0584', 18 | }) 19 | @Prop({ 20 | type: MongooseSchema.Types.ObjectId, 21 | default: () => new Types.ObjectId(), 22 | }) 23 | _id?: Types.ObjectId; 24 | 25 | // email is the unique identifier of the user 26 | @ApiProperty({ 27 | description: 'The unique identifier of the user', 28 | example: 'john@example.com', 29 | }) 30 | @Prop({ 31 | required: true, 32 | }) 33 | email: string; 34 | 35 | // password is the hashed password of the user 36 | @ApiHideProperty() 37 | @Prop() 38 | password?: string; 39 | 40 | // workspace is the unique identifier of the workspace that the user belongs to 41 | @ApiProperty({ 42 | description: 'The unique identifier of the workspace', 43 | example: '643405452324db8c464c0584', 44 | }) 45 | @Prop({ 46 | type: MongooseSchema.Types.ObjectId, 47 | ref: DatabaseCollectionNames.WORKSPACE, 48 | }) 49 | workspace: Identifier; 50 | 51 | // name is the full name of the user 52 | @ApiProperty({ 53 | description: 'The full name of the user', 54 | example: 'John Doe', 55 | }) 56 | @Prop() 57 | name?: string; 58 | 59 | // verified is a boolean value that indicates whether the user has verified their email address 60 | @ApiProperty({ 61 | description: 'Indicates whether the user has verified their email address', 62 | example: true, 63 | }) 64 | @Prop({ 65 | type: MongooseSchema.Types.Boolean, 66 | default: false, 67 | }) 68 | verified: boolean; 69 | 70 | // verificationCode is a 6-digit number that is sent to the user's email address to verify their email address 71 | @ApiHideProperty() 72 | @Prop({ 73 | type: MongooseSchema.Types.Number, 74 | }) 75 | verificationCode?: number; 76 | 77 | // verificationCodeExpiry is the date and time when the verification code expires 78 | @ApiHideProperty() 79 | @Prop({ 80 | type: MongooseSchema.Types.Date, 81 | }) 82 | verificationCodeExpiry?: Date; 83 | 84 | @ApiHideProperty() 85 | @Prop() 86 | resetToken?: string; 87 | 88 | // registerCode is used for when user is going to reset password or change password perform at time all same user login session will be logout 89 | @ApiHideProperty() 90 | @Prop({ 91 | type: MongooseSchema.Types.Number, 92 | }) 93 | registerCode?: number; 94 | 95 | @ApiProperty({ 96 | description: 'Date of creation', 97 | }) 98 | @Prop() 99 | createdAt?: Date; 100 | 101 | @ApiProperty({ 102 | description: 'Date of last update', 103 | }) 104 | @Prop() 105 | updatedAt?: Date; 106 | } 107 | 108 | export type UserIdentifier = Identifier | User; 109 | 110 | export type UserDocument = HydratedDocument; 111 | export const UserSchema = SchemaFactory.createForClass(User); 112 | 113 | UserSchema.index({ email: 1, isActive: 1 }); 114 | -------------------------------------------------------------------------------- /src/modules/workspace/workspace.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | 4 | import { DatabaseCollectionNames } from '../../shared/enums/db.enum'; 5 | import { WorkspaceQueryService } from './workspace.query-service'; 6 | import { WorkspaceRepository } from './workspace.repository'; 7 | import { WorkspaceSchema } from './workspace.schema'; 8 | 9 | @Module({ 10 | imports: [MongooseModule.forFeature([{ name: DatabaseCollectionNames.WORKSPACE, schema: WorkspaceSchema }])], 11 | providers: [WorkspaceQueryService, WorkspaceRepository], 12 | exports: [WorkspaceQueryService], 13 | }) 14 | export class WorkspaceModule {} 15 | -------------------------------------------------------------------------------- /src/modules/workspace/workspace.query-service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { InternalServerErrorException } from '../../exceptions/internal-server-error.exception'; 4 | 5 | import { Workspace } from './workspace.schema'; 6 | import { WorkspaceRepository } from './workspace.repository'; 7 | 8 | @Injectable() 9 | export class WorkspaceQueryService { 10 | constructor(private readonly workspaceRepository: WorkspaceRepository) {} 11 | 12 | async create(workspace: Workspace): Promise { 13 | try { 14 | return await this.workspaceRepository.create(workspace); 15 | } catch (error) { 16 | throw InternalServerErrorException.INTERNAL_SERVER_ERROR(error); 17 | } 18 | } 19 | 20 | async findById(workspaceId: string): Promise { 21 | try { 22 | return await this.workspaceRepository.findById(workspaceId); 23 | } catch (error) { 24 | throw InternalServerErrorException.INTERNAL_SERVER_ERROR(error); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/workspace/workspace.repository.ts: -------------------------------------------------------------------------------- 1 | import { FilterQuery, Model, ProjectionType, QueryOptions, UpdateQuery } from 'mongoose'; 2 | import { InjectModel } from '@nestjs/mongoose'; 3 | import { Injectable } from '@nestjs/common'; 4 | 5 | import { DatabaseCollectionNames } from '../../shared/enums/db.enum'; 6 | import { Workspace, WorkspaceDocument } from './workspace.schema'; 7 | 8 | @Injectable() 9 | export class WorkspaceRepository { 10 | constructor(@InjectModel(DatabaseCollectionNames.WORKSPACE) private workspaceModel: Model) {} 11 | 12 | async find(filter: FilterQuery, selectOptions?: ProjectionType): Promise { 13 | return this.workspaceModel.find(filter, selectOptions).lean(); 14 | } 15 | 16 | async findOne(filter: FilterQuery): Promise { 17 | return this.workspaceModel.findOne(filter).lean(); 18 | } 19 | 20 | async create(workspace: Workspace): Promise { 21 | return this.workspaceModel.create(workspace); 22 | } 23 | 24 | async findById(workspaceId: string): Promise { 25 | return this.workspaceModel.findById(workspaceId).lean(); 26 | } 27 | 28 | async findOneAndUpdate( 29 | filter: FilterQuery, 30 | update: UpdateQuery, 31 | options?: QueryOptions, 32 | ): Promise { 33 | return this.workspaceModel.findOneAndUpdate(filter, update, options).lean(); 34 | } 35 | 36 | async findByIdAndDelete(workspaceId: string): Promise { 37 | return this.workspaceModel.findByIdAndDelete(workspaceId).lean(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/modules/workspace/workspace.schema.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { HydratedDocument, Schema as MongooseSchema, Types } from 'mongoose'; 3 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 4 | 5 | import { DatabaseCollectionNames } from '../../shared/enums/db.enum'; 6 | import { Identifier } from '../../shared/types/schema.type'; 7 | 8 | @Schema({ 9 | timestamps: true, 10 | collection: DatabaseCollectionNames.WORKSPACE, 11 | }) 12 | export class Workspace { 13 | @ApiProperty({ 14 | description: 'The unique identifier of the workspace', 15 | example: '507f191e810c19729de860ea', 16 | }) 17 | @Prop({ 18 | type: MongooseSchema.Types.ObjectId, 19 | default: () => new Types.ObjectId(), 20 | }) 21 | _id?: Types.ObjectId; 22 | 23 | @ApiProperty({ 24 | description: 'The name of the workspace', 25 | example: 'My Workspace', 26 | }) 27 | @Prop({ 28 | type: MongooseSchema.Types.String, 29 | required: true, 30 | }) 31 | name: string; 32 | 33 | @ApiProperty({ 34 | description: 'Date of creation', 35 | }) 36 | @Prop() 37 | createdAt?: Date; 38 | 39 | @ApiProperty({ 40 | description: 'Date of last update', 41 | }) 42 | @Prop() 43 | updatedAt?: Date; 44 | } 45 | 46 | export type WorkspaceIdentifier = Identifier | Workspace; 47 | 48 | export type WorkspaceDocument = HydratedDocument; 49 | export const WorkspaceSchema = SchemaFactory.createForClass(Workspace); 50 | -------------------------------------------------------------------------------- /src/shared/enums/db.enum.ts: -------------------------------------------------------------------------------- 1 | export enum DatabaseCollectionNames { 2 | USER = 'users', 3 | WORKSPACE = 'workspaces', 4 | } 5 | -------------------------------------------------------------------------------- /src/shared/enums/index.ts: -------------------------------------------------------------------------------- 1 | export * from './log-level.enum'; 2 | export * from './node-env.enum'; 3 | export * from './db.enum'; 4 | -------------------------------------------------------------------------------- /src/shared/enums/log-level.enum.ts: -------------------------------------------------------------------------------- 1 | export enum LogLevel { 2 | SILENT = 'silent', 3 | TRACE = 'trace', 4 | DEBUG = 'debug', 5 | INFO = 'info', 6 | WARN = 'warn', 7 | ERROR = 'error', 8 | FATAL = 'fatal', 9 | } 10 | -------------------------------------------------------------------------------- /src/shared/enums/node-env.enum.ts: -------------------------------------------------------------------------------- 1 | export enum NodeEnv { 2 | DEVELOPMENT = 'development', 3 | TEST = 'test', 4 | PRODUCTION = 'production', 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './schema.type'; 2 | -------------------------------------------------------------------------------- /src/shared/types/schema.type.ts: -------------------------------------------------------------------------------- 1 | import { Types } from 'mongoose'; 2 | 3 | export type Identifier = Types.ObjectId | string; 4 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import { Test, TestingModule } from '@nestjs/testing'; 4 | 5 | import { AppModule } from '../src/app.module'; 6 | 7 | describe('AppController (e2e)', () => { 8 | let app: INestApplication; 9 | 10 | beforeEach(async () => { 11 | const moduleFixture: TestingModule = await Test.createTestingModule({ 12 | imports: [AppModule], 13 | }).compile(); 14 | 15 | app = moduleFixture.createNestApplication(); 16 | await app.init(); 17 | }); 18 | 19 | it('/ (GET)', () => { 20 | return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2019", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | }, 21 | "exclude": ["**/dist/**/*", "**/node_modules/**/*"] 22 | } 23 | --------------------------------------------------------------------------------