├── src ├── authentication │ └── .gitkeep ├── common │ ├── guards │ │ └── .gitkeep │ ├── pipes │ │ └── .gitkeep │ ├── decorators │ │ └── .gitkeep │ ├── exceptions │ │ └── .gitkeep │ ├── interfaces │ │ └── .gitkeep │ ├── middleware │ │ └── .gitkeep │ ├── serializers │ │ └── .gitkeep │ ├── constants │ │ └── constants.ts │ ├── interceptors │ │ └── sentryio.interceptor.ts │ ├── helpers │ │ └── environment.helper.ts │ └── environments │ │ ├── .env │ │ └── development.env ├── config │ ├── cache │ │ └── .gitkeep │ ├── queue │ │ └── .gitkeep │ ├── session │ │ └── .gitkeep │ ├── secrets │ │ └── keyvault │ │ │ ├── configuration.ts │ │ │ ├── config.module.ts │ │ │ └── config.service.ts │ ├── storage │ │ └── azure │ │ │ ├── configuration.ts │ │ │ ├── config.service.ts │ │ │ └── config.module.ts │ ├── app │ │ ├── configuration.ts │ │ ├── config.service.ts │ │ └── config.module.ts │ ├── logger │ │ └── sentryio │ │ │ ├── configuration.ts │ │ │ ├── config.service.ts │ │ │ └── config.module.ts │ ├── database │ │ └── mssqlserver │ │ │ ├── configuration.ts │ │ │ ├── config.service.ts │ │ │ └── config.module.ts │ └── openapi │ │ └── swagger │ │ ├── configuration.ts │ │ ├── config.service.ts │ │ └── config.module.ts ├── providers │ ├── cache │ │ └── .gitkeep │ ├── mail │ │ └── .gitkeep │ ├── queue │ │ └── .gitkeep │ ├── storage │ │ └── .gitkeep │ └── database │ │ └── mssqlserver │ │ └── provider.module.ts ├── api │ └── health │ │ ├── health.controller.ts │ │ └── health.controller.spec.ts ├── app.module.ts └── main.ts ├── .prettierrc ├── tsconfig.build.json ├── nest-cli.json ├── test └── jest-e2e.json ├── .gitignore ├── tsconfig.json ├── .eslintrc.js ├── LICENSE ├── package.json └── README.md /src/authentication/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/common/guards/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/common/pipes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/config/cache/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/config/queue/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/config/session/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/providers/cache/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/providers/mail/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/providers/queue/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/common/decorators/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/common/exceptions/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/common/interfaces/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/common/middleware/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/common/serializers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/providers/storage/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "endOfLine": "auto" 5 | } -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/common/constants/constants.ts: -------------------------------------------------------------------------------- 1 | export const ENVIRONMENT_PATH = 'src/common/environments'; 2 | export const DEFAULT_PORT = 9000; 3 | -------------------------------------------------------------------------------- /src/config/secrets/keyvault/configuration.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('keyvault', () => ({ 4 | keyVaultUrl: process.env.KEYVAULT_URL, 5 | })); 6 | -------------------------------------------------------------------------------- /src/config/storage/azure/configuration.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('storage', () => ({ 4 | connectionString: process.env.AZURE_STORAGE, 5 | })); 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/config/app/configuration.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('app', () => ({ 4 | env: process.env.APP_ENV, 5 | name: process.env.APP_NAME, 6 | url: process.env.APP_URL, 7 | port: process.env.APP_PORT, 8 | })); 9 | -------------------------------------------------------------------------------- /src/config/logger/sentryio/configuration.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('logger', () => ({ 4 | dns: process.env.SENTRY_DNS, 5 | enable: process.env.SENTRY_ENABLE, 6 | release: process.env.npm_package_version, 7 | })); 8 | -------------------------------------------------------------------------------- /src/api/health/health.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { HealthCheck } from '@nestjs/terminus'; 3 | 4 | @Controller('health') 5 | export class HealthController { 6 | @Get() 7 | @HealthCheck() 8 | check(): string { 9 | return 'Service is up'; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/config/database/mssqlserver/configuration.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('db', () => ({ 4 | host: process.env.DB_HOST, 5 | port: process.env.DB_PORT, 6 | name: process.env.DB_NAME, 7 | user: process.env.DB_USER, 8 | password: process.env.DB_PASSWORD, 9 | })); 10 | -------------------------------------------------------------------------------- /src/config/openapi/swagger/configuration.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('swagger', () => ({ 4 | title: 'Swagger API Docs', 5 | description: 'API Documentation', 6 | version: 'V1.0', 7 | envname: 'local', 8 | url: process.env.APP_URL, 9 | port: process.env.APP_PORT, 10 | tag: 'API Docs', 11 | path: 'api-docs', 12 | })); 13 | -------------------------------------------------------------------------------- /src/config/storage/azure/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | 4 | @Injectable() 5 | export class StorageConfigService { 6 | constructor(private configService: ConfigService) {} 7 | 8 | get connectionString(): string { 9 | return this.configService.get('storage.connectionString') ?? ''; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /src/api/health/health.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { HealthController } from './health.controller'; 3 | 4 | describe('HealthController', () => { 5 | let controller: HealthController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [HealthController], 10 | }).compile(); 11 | 12 | controller = module.get(HealthController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/config/logger/sentryio/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | 4 | @Injectable() 5 | export class LoggerConfigService { 6 | constructor(private configService: ConfigService) {} 7 | 8 | get dns(): string { 9 | return this.configService.get('logger.dns') ?? ''; 10 | } 11 | 12 | get enable(): boolean { 13 | return this.configService.get('logger.enable') ?? false; 14 | } 15 | 16 | get release(): string { 17 | return this.configService.get('logger.release') ?? ''; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/common/interceptors/sentryio.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExecutionContext, 3 | Injectable, 4 | NestInterceptor, 5 | CallHandler, 6 | } from '@nestjs/common'; 7 | import * as Sentry from '@sentry/node'; 8 | import { Observable } from 'rxjs'; 9 | import { tap } from 'rxjs/operators'; 10 | 11 | @Injectable() 12 | export class SentryInterceptor implements NestInterceptor { 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | intercept(_context: ExecutionContext, next: CallHandler): Observable { 15 | return next.handle().pipe( 16 | tap((exception) => { 17 | Sentry.captureException(exception); 18 | }), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/common/helpers/environment.helper.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { resolve } from 'path'; 3 | 4 | export enum Environment { 5 | Development = 'development', 6 | Production = 'production', 7 | Staging = 'staging', 8 | Test = 'test', 9 | } 10 | 11 | export function getEnvironmentPath(dest: string): string { 12 | const env: string | undefined = process.env.APP_ENV; 13 | const fallback: string = resolve(`${dest}/.env`); 14 | const filename: string = env ? `${env}.env` : 'development.env'; 15 | let filePath: string = resolve(`${dest}/${filename}`); 16 | 17 | if (!existsSync(filePath)) { 18 | filePath = fallback; 19 | } 20 | 21 | return filePath; 22 | } 23 | -------------------------------------------------------------------------------- /src/config/app/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | 4 | @Injectable() 5 | export class AppConfigService { 6 | constructor(private configService: ConfigService) {} 7 | 8 | get name(): string { 9 | return this.configService.get('app.name', { infer: true }) ?? ''; 10 | } 11 | get env(): string { 12 | return this.configService.get('app.env', { infer: true }) ?? ''; 13 | } 14 | get url(): string { 15 | return this.configService.get('app.url', { infer: true }) ?? ''; 16 | } 17 | get port(): number { 18 | return Number(this.configService.get('app.port')); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "declaration": true, 6 | "removeComments": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "target": "ES2021", 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "baseUrl": "./", 14 | "incremental": true, 15 | "allowUnreachableCode": false, 16 | "allowUnusedLabels": false, 17 | "skipLibCheck": true, 18 | "strictBindCallApply": false, 19 | "forceConsistentCasingInFileNames": false, 20 | "noFallthroughCasesInSwitch": true, 21 | "noImplicitReturns": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/config/database/mssqlserver/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | 4 | @Injectable() 5 | export class DBConfigService { 6 | constructor(private configService: ConfigService) {} 7 | 8 | get host(): string { 9 | return this.configService.get('db.host') ?? ''; 10 | } 11 | 12 | get database(): string { 13 | return this.configService.get('db.name') ?? ''; 14 | } 15 | 16 | get user(): string { 17 | return this.configService.get('db.user') ?? ''; 18 | } 19 | 20 | get password(): string { 21 | return this.configService.get('db.password') ?? ''; 22 | } 23 | 24 | get port(): number { 25 | return Number(this.configService.get('db.port')); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.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: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/strict-type-checked', 11 | 'plugin:@typescript-eslint/stylistic-type-checked', 12 | 'plugin:prettier/recommended', 13 | ], 14 | root: true, 15 | env: { 16 | node: true, 17 | jest: true, 18 | }, 19 | ignorePatterns: ['.eslintrc.js'], 20 | rules: { 21 | '@typescript-eslint/interface-name-prefix': 'off', 22 | '@typescript-eslint/explicit-function-return-type': 'error', 23 | '@typescript-eslint/explicit-module-boundary-types': 'error', 24 | '@typescript-eslint/no-explicit-any': 'error', 25 | '@typescript-eslint/no-extraneous-class': 'off' 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/common/environments/.env: -------------------------------------------------------------------------------- 1 | # Application configuration 2 | # Valid environments: 'development', 'production', 'testing', 'staging 3 | # All sensitive information is stored in keyvualt secrets, so the environment variables will contain the name of the secret 4 | 5 | APP_ENV=development 6 | APP_NAME="Nestjs Project Structure Example" 7 | APP_URL=http://localhost 8 | APP_PORT=9000 9 | 10 | 11 | # Data base configuration 12 | DB_HOST=DATABASESERVER 13 | DB_PORT=1433 14 | DB_NAME=DATABASENAME 15 | DB_USER=DATABASEUSER 16 | DB_PASSWORD=DATABASEPASSWORD 17 | 18 | 19 | # Sentryio logger configuration 20 | SENTRY_DNS=SENTRYDNS 21 | SENTRY_ENABLE=false 22 | 23 | 24 | # Keyvault 25 | KEYVAULT_URL=https://xxxxx.vault.azure.net/ 26 | AZURE_TENANT_ID=xxxxxxxxxxxxxx 27 | AZURE_CLIENT_ID=xxxxxxxxxxxxxx 28 | AZURE_CLIENT_SECRET=xxxxxxxxxxxxxx 29 | 30 | # Azure storage connection string 31 | AZURE_STORAGE=STORAGECONNECTION -------------------------------------------------------------------------------- /src/common/environments/development.env: -------------------------------------------------------------------------------- 1 | 2 | # Application configuration 3 | # Valid environments: 'development', 'production', 'testing', 'staging 4 | # All sensitive information is stored in keyvualt secrets, so the environment variables will contain the name of the secret 5 | 6 | APP_ENV=development 7 | APP_NAME="Nestjs Project Structure Example" 8 | APP_URL=http://localhost 9 | APP_PORT=9000 10 | 11 | 12 | # Data base configuration 13 | DB_HOST=DATABASESERVER 14 | DB_PORT=1433 15 | DB_NAME=DATABASENAME 16 | DB_USER=DATABASEUSER 17 | DB_PASSWORD=DATABASEPASSWORD 18 | 19 | 20 | # Sentryio logger configuration 21 | SENTRY_DNS=SENTRYDNS 22 | SENTRY_ENABLE=false 23 | 24 | 25 | # Keyvault 26 | KEYVAULT_URL=https://xxxxx.vault.azure.net/ 27 | AZURE_TENANT_ID=xxxxxxxxxxxxxx 28 | AZURE_CLIENT_ID=xxxxxxxxxxxxxx 29 | AZURE_CLIENT_SECRET=xxxxxxxxxxxxxx 30 | 31 | # Azure storage connection string 32 | AZURE_STORAGE=STORAGECONNECTION -------------------------------------------------------------------------------- /src/config/storage/azure/config.module.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi'; 2 | import { Module } from '@nestjs/common'; 3 | import { ConfigModule, ConfigService } from '@nestjs/config'; 4 | import configuration from './configuration'; 5 | import { StorageConfigService } from './config.service'; 6 | import { getEnvironmentPath } from 'src/common/helpers/environment.helper'; 7 | import { ENVIRONMENT_PATH } from 'src/common/constants/constants'; 8 | 9 | const envFilePath: string = getEnvironmentPath(ENVIRONMENT_PATH); 10 | 11 | @Module({ 12 | imports: [ 13 | ConfigModule.forRoot({ 14 | envFilePath, 15 | load: [configuration], 16 | validationSchema: Joi.object({ 17 | AZURE_STORAGE: Joi.string().required(), 18 | }), 19 | }), 20 | ], 21 | providers: [ConfigService, StorageConfigService], 22 | exports: [ConfigService, StorageConfigService], 23 | }) 24 | export class StorageConfigModule {} 25 | -------------------------------------------------------------------------------- /src/config/secrets/keyvault/config.module.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi'; 2 | import { Module } from '@nestjs/common'; 3 | import { ConfigModule, ConfigService } from '@nestjs/config'; 4 | 5 | import configuration from './configuration'; 6 | import { KeyVaultConfigService } from './config.service'; 7 | import { getEnvironmentPath } from 'src/common/helpers/environment.helper'; 8 | import { ENVIRONMENT_PATH } from 'src/common/constants/constants'; 9 | 10 | const envFilePath: string = getEnvironmentPath(ENVIRONMENT_PATH); 11 | 12 | @Module({ 13 | imports: [ 14 | ConfigModule.forRoot({ 15 | envFilePath, 16 | load: [configuration], 17 | validationSchema: Joi.object({ 18 | KEYVAULT_URL: Joi.string().required(), 19 | }), 20 | }), 21 | ], 22 | providers: [ConfigService, KeyVaultConfigService], 23 | exports: [ConfigService, KeyVaultConfigService], 24 | }) 25 | export class KeyVaultConfigModule {} 26 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppConfigModule } from './config/app/config.module'; 3 | import { DBConfigModule } from './config/database/mssqlserver/config.module'; 4 | import { MSSqlServerDatabaseProviderModule } from './providers/database/mssqlserver/provider.module'; 5 | import { LoggerConfigModule } from './config/logger/sentryio/config.module'; 6 | import { SwaggerConfigModule } from './config/openapi/swagger/config.module'; 7 | import { HealthController } from './api/health/health.controller'; 8 | import { KeyVaultConfigModule } from './config/secrets/keyvault/config.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | AppConfigModule, 13 | DBConfigModule, 14 | MSSqlServerDatabaseProviderModule, 15 | LoggerConfigModule, 16 | SwaggerConfigModule, 17 | KeyVaultConfigModule, 18 | ], 19 | controllers: [HealthController], 20 | providers: [], 21 | }) 22 | export class AppModule {} 23 | -------------------------------------------------------------------------------- /src/config/logger/sentryio/config.module.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi'; 2 | import { Module } from '@nestjs/common'; 3 | import { ConfigModule, ConfigService } from '@nestjs/config'; 4 | import configuration from './configuration'; 5 | import { LoggerConfigService } from './config.service'; 6 | import { getEnvironmentPath } from 'src/common/helpers/environment.helper'; 7 | import { ENVIRONMENT_PATH } from 'src/common/constants/constants'; 8 | 9 | const envFilePath: string = getEnvironmentPath(ENVIRONMENT_PATH); 10 | 11 | @Module({ 12 | imports: [ 13 | ConfigModule.forRoot({ 14 | envFilePath, 15 | load: [configuration], 16 | validationSchema: Joi.object({ 17 | SENTRY_DNS: Joi.string(), 18 | SENTRY_ENABLE: Joi.boolean().default(false), 19 | }), 20 | }), 21 | ], 22 | providers: [ConfigService, LoggerConfigService], 23 | exports: [ConfigService, LoggerConfigService], 24 | }) 25 | export class LoggerConfigModule {} 26 | -------------------------------------------------------------------------------- /src/config/database/mssqlserver/config.module.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi'; 2 | import { Module } from '@nestjs/common'; 3 | import { ConfigModule, ConfigService } from '@nestjs/config'; 4 | import configuration from './configuration'; 5 | import { DBConfigService } from './config.service'; 6 | import { getEnvironmentPath } from 'src/common/helpers/environment.helper'; 7 | import { ENVIRONMENT_PATH } from 'src/common/constants/constants'; 8 | 9 | const envFilePath: string = getEnvironmentPath(ENVIRONMENT_PATH); 10 | 11 | @Module({ 12 | imports: [ 13 | ConfigModule.forRoot({ 14 | envFilePath, 15 | load: [configuration], 16 | validationSchema: Joi.object({ 17 | DB_NAME: Joi.string(), 18 | DB_HOST: Joi.string(), 19 | DB_PORT: Joi.number().default(1433), 20 | DB_USER: Joi.string(), 21 | DB_PASSWORD: Joi.string(), 22 | }), 23 | }), 24 | ], 25 | providers: [ConfigService, DBConfigService], 26 | exports: [ConfigService, DBConfigService], 27 | }) 28 | export class DBConfigModule {} 29 | -------------------------------------------------------------------------------- /src/config/app/config.module.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi'; 2 | import { Module } from '@nestjs/common'; 3 | import { ConfigModule, ConfigService } from '@nestjs/config'; 4 | import configuration from './configuration'; 5 | import { AppConfigService } from './config.service'; 6 | import { getEnvironmentPath } from 'src/common/helpers/environment.helper'; 7 | import { DEFAULT_PORT, ENVIRONMENT_PATH } from 'src/common/constants/constants'; 8 | 9 | const envFilePath: string = getEnvironmentPath(ENVIRONMENT_PATH); 10 | 11 | @Module({ 12 | imports: [ 13 | ConfigModule.forRoot({ 14 | envFilePath, 15 | load: [configuration], 16 | validationSchema: Joi.object({ 17 | APP_NAME: Joi.string(), 18 | APP_ENV: Joi.string() 19 | .valid('development', 'production', 'testing', 'staging') 20 | .default('development'), 21 | APP_URL: Joi.string(), 22 | APP_PORT: Joi.number().default(DEFAULT_PORT), 23 | }), 24 | }), 25 | ], 26 | providers: [ConfigService, AppConfigService], 27 | exports: [ConfigService, AppConfigService], 28 | }) 29 | export class AppConfigModule {} 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Fabian A Becerra M 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. 22 | -------------------------------------------------------------------------------- /src/config/openapi/swagger/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { DEFAULT_PORT } from 'src/common/constants/constants'; 4 | 5 | @Injectable() 6 | export class SwaggerConfigService { 7 | constructor(private configService: ConfigService) {} 8 | 9 | get title(): string { 10 | return this.configService.get('swagger.title') ?? ''; 11 | } 12 | get description(): string { 13 | return this.configService.get('swagger.description') ?? ''; 14 | } 15 | get version(): string { 16 | return this.configService.get('swagger.version') ?? ''; 17 | } 18 | get name(): string { 19 | return this.configService.get('swagger.envname') ?? ''; 20 | } 21 | get url(): string { 22 | return this.configService.get('swagger.url') ?? ''; 23 | } 24 | get port(): number { 25 | return this.configService.get('swagger.port') ?? DEFAULT_PORT; 26 | } 27 | get tag(): string { 28 | return this.configService.get('swagger.tag') ?? ''; 29 | } 30 | get path(): string { 31 | return this.configService.get('swagger.path') ?? ''; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/config/openapi/swagger/config.module.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi'; 2 | import { INestApplication, Module } from '@nestjs/common'; 3 | import { ConfigModule, ConfigService } from '@nestjs/config'; 4 | import configuration from './configuration'; 5 | import { SwaggerConfigService } from './config.service'; 6 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 7 | 8 | @Module({ 9 | imports: [ 10 | ConfigModule.forRoot({ 11 | load: [configuration], 12 | validationSchema: Joi.object({ 13 | APP_URL: Joi.string(), 14 | }), 15 | }), 16 | ], 17 | providers: [ConfigService, SwaggerConfigService], 18 | exports: [ConfigService, SwaggerConfigService], 19 | }) 20 | export class SwaggerConfigModule { 21 | constructor(private readonly configService: SwaggerConfigService) {} 22 | 23 | setup(app: INestApplication): void { 24 | const url: URL = new URL(this.configService.url); 25 | url.port = this.configService.port.toString(); 26 | 27 | const options = new DocumentBuilder() 28 | .setTitle(this.configService.title) 29 | .setDescription(this.configService.description) 30 | .setVersion(this.configService.version) 31 | .addServer(url.href, this.configService.name) 32 | .addTag(this.configService.tag) 33 | .build(); 34 | 35 | const document = SwaggerModule.createDocument(app, options); 36 | SwaggerModule.setup(this.configService.path, app, document); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/providers/database/mssqlserver/provider.module.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseType } from 'typeorm'; 2 | import { Module } from '@nestjs/common'; 3 | import { TypeOrmModule, TypeOrmModuleAsyncOptions } from '@nestjs/typeorm'; 4 | import { DBConfigModule } from 'src/config/database/mssqlserver/config.module'; 5 | import { DBConfigService } from 'src/config/database/mssqlserver/config.service'; 6 | import { KeyVaultConfigService } from 'src/config/secrets/keyvault/config.service'; 7 | import { KeyVaultConfigModule } from 'src/config/secrets/keyvault/config.module'; 8 | 9 | @Module({ 10 | imports: [ 11 | TypeOrmModule.forRootAsync({ 12 | imports: [DBConfigModule, KeyVaultConfigModule], 13 | useFactory: async ( 14 | dbConfigService: DBConfigService, 15 | keyvaultService: KeyVaultConfigService, 16 | // eslint-disable-next-line @typescript-eslint/require-await 17 | ) => ({ 18 | type: 'mssql' as DatabaseType, 19 | host: await keyvaultService.getKeyVaultSecret(dbConfigService.host), 20 | port: dbConfigService.port, 21 | username: await keyvaultService.getKeyVaultSecret(dbConfigService.user), 22 | password: await keyvaultService.getKeyVaultSecret( 23 | dbConfigService.password, 24 | ), 25 | database: await keyvaultService.getKeyVaultSecret( 26 | dbConfigService.database, 27 | ), 28 | autoLoadEntities: true, 29 | synchronize: false, 30 | }), 31 | inject: [DBConfigService, KeyVaultConfigService], 32 | } as TypeOrmModuleAsyncOptions), 33 | ], 34 | }) 35 | export class MSSqlServerDatabaseProviderModule {} 36 | -------------------------------------------------------------------------------- /src/config/secrets/keyvault/config.service.ts: -------------------------------------------------------------------------------- 1 | import { DefaultAzureCredential } from '@azure/identity'; 2 | import { SecretClient } from '@azure/keyvault-secrets'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { ConfigService } from '@nestjs/config'; 5 | 6 | @Injectable() 7 | export class KeyVaultConfigService { 8 | private secretCache: Map; 9 | private credential: DefaultAzureCredential; 10 | private client: SecretClient; 11 | 12 | constructor(private configService: ConfigService) { 13 | this.secretCache = new Map(); 14 | 15 | this.credential = new DefaultAzureCredential(); 16 | this.client = new SecretClient( 17 | this.configService.get('keyvault.keyVaultUrl') ?? '', 18 | this.credential, 19 | ); 20 | } 21 | 22 | get keyVaultUrl(): string { 23 | return this.configService.get('keyvault.keyVaultUrl') ?? ''; 24 | } 25 | 26 | async getKeyVaultSecret(secretKey: string): Promise { 27 | if (this.secretCache.has(secretKey)) { 28 | const secretFromCache = this.secretCache.get(secretKey) ?? ''; 29 | return Promise.resolve(secretFromCache); 30 | } 31 | 32 | try { 33 | const secret = await this.client.getSecret(secretKey); 34 | const secretFromKeyvault = secret.value ?? ''; 35 | 36 | if (secretFromKeyvault.length === 0) { 37 | throw new Error("Key doesn't exist"); 38 | } 39 | 40 | this.secretCache.set(secretKey, secretFromKeyvault); 41 | 42 | return Promise.resolve(secretFromKeyvault); 43 | } catch (error) { 44 | console.log(error); 45 | throw error; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { ValidationPipe } from '@nestjs/common'; 3 | import * as Sentry from '@sentry/node'; 4 | import helmet from 'helmet'; 5 | 6 | import { AppModule } from './app.module'; 7 | import { AppConfigService } from './config/app/config.service'; 8 | import { LoggerConfigService } from './config/logger/sentryio/config.service'; 9 | import { SwaggerConfigModule } from './config/openapi/swagger/config.module'; 10 | import { SwaggerConfigService } from './config/openapi/swagger/config.service'; 11 | import { KeyVaultConfigService } from './config/secrets/keyvault/config.service'; 12 | 13 | async function bootstrap(): Promise { 14 | const app = await NestFactory.create(AppModule); 15 | 16 | const appConfig: AppConfigService = app.get(AppConfigService); 17 | const sentryConfig: LoggerConfigService = app.get(LoggerConfigService); 18 | const swaggerConfig: SwaggerConfigService = app.get(SwaggerConfigService); 19 | const keyVaultService: KeyVaultConfigService = app.get(KeyVaultConfigService); 20 | 21 | app.setGlobalPrefix('api'); 22 | app.useGlobalPipes( 23 | new ValidationPipe({ 24 | whitelist: true, 25 | forbidNonWhitelisted: true, 26 | }), 27 | ); 28 | 29 | app.use(helmet()); 30 | 31 | Sentry.init({ 32 | dsn: await keyVaultService.getKeyVaultSecret(sentryConfig.dns), 33 | enabled: sentryConfig.enable, 34 | release: sentryConfig.release, 35 | environment: appConfig.env, 36 | }); 37 | 38 | if (appConfig.env == 'development') { 39 | const swagger: SwaggerConfigModule = new SwaggerConfigModule(swaggerConfig); 40 | swagger.setup(app); 41 | } 42 | 43 | await app.listen(appConfig.port); 44 | } 45 | bootstrap() 46 | .then(() => { 47 | console.log('Done'); 48 | }) 49 | .catch(() => new Error('Something fail loading the app')); 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nestjs Project Structure Example", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "Creative Common", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 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 | "dependencies": { 23 | "@azure/identity": "^3.3.1", 24 | "@azure/keyvault-secrets": "^4.7.0", 25 | "@nestjs/common": "^10.0.0", 26 | "@nestjs/config": "^3.1.1", 27 | "@nestjs/core": "^10.0.0", 28 | "@nestjs/platform-express": "^10.0.0", 29 | "@nestjs/swagger": "^7.1.12", 30 | "@nestjs/terminus": "^10.1.1", 31 | "@nestjs/typeorm": "^10.0.0", 32 | "@sentry/node": "^7.70.0", 33 | "class-transformer": "^0.5.1", 34 | "class-validator": "^0.14.0", 35 | "helmet": "^7.0.0", 36 | "install": "^0.13.0", 37 | "joi": "^17.10.2", 38 | "mssql": "^9.3.2", 39 | "npm": "^10.1.0", 40 | "reflect-metadata": "^0.1.13", 41 | "rxjs": "^7.8.1", 42 | "typeorm": "^0.3.17" 43 | }, 44 | "devDependencies": { 45 | "@nestjs/cli": "^10.1.18", 46 | "@nestjs/schematics": "^10.0.0", 47 | "@nestjs/testing": "^10.0.0", 48 | "@types/express": "^4.17.17", 49 | "@types/jest": "^29.5.2", 50 | "@types/node": "^20.3.1", 51 | "@types/supertest": "^2.0.12", 52 | "@typescript-eslint/eslint-plugin": "^6.0.0", 53 | "@typescript-eslint/parser": "^6.0.0", 54 | "eslint": "^8.42.0", 55 | "eslint-config-prettier": "^9.0.0", 56 | "eslint-plugin-prettier": "^5.0.0", 57 | "jest": "^29.5.0", 58 | "prettier": "^3.0.0", 59 | "source-map-support": "^0.5.21", 60 | "supertest": "^6.3.3", 61 | "ts-jest": "^29.1.0", 62 | "ts-loader": "^9.4.3", 63 | "ts-node": "^10.9.1", 64 | "tsconfig-paths": "^4.2.0", 65 | "typescript": "^5.1.3" 66 | }, 67 | "jest": { 68 | "moduleFileExtensions": [ 69 | "js", 70 | "json", 71 | "ts" 72 | ], 73 | "rootDir": "src", 74 | "testRegex": ".*\\.spec\\.ts$", 75 | "transform": { 76 | "^.+\\.(t|j)s$": "ts-jest" 77 | }, 78 | "collectCoverageFrom": [ 79 | "**/*.(t|j)s" 80 | ], 81 | "coverageDirectory": "../coverage", 82 | "testEnvironment": "node" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

NestJs Architecture Demo Project

3 | 4 | 5 | This project shows a suggested structure and architecture for a medium sized project that should be able to grow easily. Includes some examples of configuration services and consumption of Azure services and Sentry, requested by students. 6 | 7 | ## Introduction 8 | 9 | Whenever I teach introductory courses in any programming language or framework, the same question arises: 10 | 11 | **How should I structure the project?** 12 | 13 | And it is certainly a question that can be easy or difficult to answer depending on the situation. 14 | 15 | In an ideal world, we should thoroughly understand the business requirements, growth expectations, quality attributes, and many other important aspects of software design, and based on that, define an architecture or a set of appropriate intermediate architectures, have a clear roadmap, and so on. 16 | 17 | This is not always the case when we talk about small and medium customers, and today it is very common to find many new startups entering the world of software. 18 | 19 | Yes, some programming languages include templates that give us a decent starting point, some others are so basic that they're barely useful, and others, like NodeJs, don't include any, leaving a newbie feeling lost. 20 | 21 | Here we will see a simple proposal using NestJs, which came up as a way to answer this question in one of my recent training courses, it is a very flexible base and easy to adapt to any need. 22 | 23 | ## About the project 24 | 25 | This project is a highly scalable backend application built in the Nodejs ecosystem, it has been built in [NestJs](https://github.com/nestjs/nest) in order to provide a high level of abstraction, mature design patterns, adaptability, flexibility and a combination of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Reactive Programming) in a single development tool. 26 | 27 | The application is built with and fully supports [TypeScript](https://www.typescriptlang.org/), a high-level programming language which takes [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) to the next level by adding static types and class-based objects, as well as other powerful features. 28 | 29 | The [MS SqlServer](https://www.microsoft.com/es-co/sql-server/sql-server-downloads) database engine is used, the database model is relational, 30 | and we use [TypeOrm](https://typeorm.io/) as a data access mechanism. 31 | 32 | The software architecture has been designed on the fundamentals of [Multitier architecture](https://en.wikipedia.org/wiki/Multitier_architecture) in order to provide broad characteristics, and capacities for growth and adaptation to changes that may occur in the future. 33 | 34 | The layered architecture style is one of the most common architectural styles. The idea behind Multitier or Layered Architecture is that modules or components with similar functionalities are organized into horizontal layers. As a result, each layer performs a specific role within the application. 35 | 36 | 37 | ## Structure 38 | 39 | Next, we will explore the sections that make up the project, the functionality of each section, and explain how it works with examples where possible. 40 |
41 |
42 | 43 | ### Config Section 44 | 45 | Let’s begin with the initialization of our environment variables. I am using the package [@nestjs/config](https://docs.nestjs.com/techniques/configuration) and [joi](https://joi.dev/) in this case. 46 | 47 | The **config/** folder consists of different sections of variable types to be loaded into our environment. 48 | 49 | ``` 50 | src/config 51 | ├── app 52 | │ ├── config.module.ts 53 | │ ├── config.service.ts 54 | │ └── configuration.ts 55 | ├── cache 56 | │ └── [...] 57 | ├── database 58 | │ ├── mssqlserver 59 | │ │ ├── config.module.ts 60 | │ │ ├── config.service.ts 61 | │ │ └── configuration.ts 62 | │ ├── mongo 63 | │ │ ├── [...] 64 | │ ├── mariadb 65 | │ │ └── [...] 66 | │ └── postgres 67 | │ └── [...] 68 | ├── health 69 | │ └── [...] 70 | ├── logger 71 | │ └── sentryio 72 | │ └── [...] 73 | ├── openapi 74 | │ └── swagger 75 | │ └── [...] 76 | ├── queue 77 | │ └── [...] 78 | ├── secrets 79 | │ └── keyvault 80 | │ └── [...] 81 | ├── session 82 | │ └── [...] 83 | └── storage 84 | └── azure 85 | └── [...] 86 | ``` 87 | 88 | Note: Here [...] means same as other config folders. 89 | 90 | Getting values from an environment file isn’t as simple as creating a .env file, but NestJS is one of the few Node frameworks that makes creating config variables very easy. It provides with a very object-oriented, modular and structured way to deal with this. 91 | 92 | - **Environment variables** 93 | 94 | I am going to take config/app as an example, but you can 95 | follow a similar approach for creating other configs. Let’s take the following 96 | as part of your environment variable file (.env). 97 | 98 | ``` 99 | # Application Configuration 100 | APP_ENV=development 101 | APP_NAME="My App" 102 | APP_URL=http://localhost:9000 103 | APP_PORT=9000 104 | ``` 105 |
106 |
107 | 108 | - **configuration.ts file** 109 | 110 | This is just going to be a function initializing your variables with a name. 111 | 112 | ``` 113 | import { registerAs } from '@nestjs/config'; 114 | export default registerAs('app', () => ({ 115 | env: process.env.APP_ENV, 116 | name: process.env.APP_NAME, 117 | url: process.env.APP_URL, 118 | port: process.env.APP_PORT, 119 | })); 120 | ``` 121 | 122 | **The name 'app' needs to be unique for each configuration.** 123 |
124 |
125 | - **configuration.service.ts file** 126 | 127 | This is just a simple class with getter based class functions. 128 | 129 | ``` 130 | import { Injectable } from '@nestjs/common'; 131 | import { ConfigService } from '@nestjs/config'; 132 | 133 | @Injectable() 134 | export class AppConfigService { 135 | constructor(private configService: ConfigService) {} 136 | 137 | get name(): string { 138 | return this.configService.get('app.name', { infer: true }) ?? ''; 139 | } 140 | get env(): string { 141 | return this.configService.get('app.env', { infer: true }) ?? ''; 142 | } 143 | get url(): string { 144 | return this.configService.get('app.url', { infer: true }) ?? ''; 145 | } 146 | get port(): number { 147 | return Number(this.configService.get('app.port')); 148 | } 149 | } 150 | ``` 151 | The idea is that they are exclusively configuration services, and that if the solution grows, we can even separate them as an independent module or as microservices that can be consumed by other modules of the solution. 152 |
153 |
154 | 155 | - **configuration.module.ts file** 156 | 157 | Here we basically import NestJS’s main ConfigModule and add your validationSchema. You can read more about it [here](https://docs.nestjs.com/techniques/configuration#schema-validation) 158 | 159 | ``` 160 | import * as Joi from 'joi'; 161 | import { Module } from '@nestjs/common'; 162 | import { ConfigModule, ConfigService } from '@nestjs/config'; 163 | import configuration from './configuration'; 164 | import { AppConfigService } from './config.service'; 165 | 166 | @Module({ 167 | imports: [ 168 | ConfigModule.forRoot({ 169 | load: [configuration], 170 | validationSchema: Joi.object({ 171 | APP_NAME: Joi.string(), 172 | APP_ENV: Joi.string() 173 | .valid('development', 'production', 'testing', 'staging') 174 | .default('development'), 175 | APP_URL: Joi.string(), 176 | APP_PORT: Joi.number().default(9000), 177 | }), 178 | }), 179 | ], 180 | providers: [ConfigService, AppConfigService], 181 | exports: [ConfigService, AppConfigService], 182 | }) 183 | export class AppConfigModule {} 184 | ``` 185 |
186 |
187 | 188 | ### Providers Section 189 | 190 | Providers are basically going to be the core modules that initiate a connection between the app and the provider engine (for eg. database). The main idea of a provider is that it can be injected as a dependency. 191 | 192 | ``` 193 | src/providers 194 | ├── cache 195 | │ └── redis 196 | │ └── [...] 197 | ├── database 198 | │ ├── mssqlserver 199 | │ │ └── provider.module.ts 200 | │ ├── mongo 201 | │ │ └── [...] 202 | │ ├── mariadb 203 | │ │ └── [...] 204 | │ └── postgres 205 | │ └── [...] 206 | ├── mail 207 | │ └── smtp 208 | │ └── [...] 209 | ├── queue 210 | │ └── redis 211 | │ └── [...] 212 | └── storage 213 | └── [...] 214 | ``` 215 | Note: Here [...] means same as other config folders. 216 | 217 | The provider.module.ts for each file would look something like this: 218 | 219 | ``` 220 | import { DatabaseType } from 'typeorm'; 221 | import { Module } from '@nestjs/common'; 222 | import { TypeOrmModule, TypeOrmModuleAsyncOptions } from '@nestjs/typeorm'; 223 | import { DBConfigModule } from 'src/config/database/mssqlserver/config.module'; 224 | import { DBConfigService } from 'src/config/database/mssqlserver/config.service'; 225 | 226 | @Module({ 227 | imports: [ 228 | TypeOrmModule.forRootAsync({ 229 | imports: [DBConfigModule], 230 | useFactory: async ( 231 | dbConfigService: DBConfigService, 232 | ) => ({ 233 | type: 'mssql' as DatabaseType, 234 | host: dbConfigService.hos), 235 | port: dbConfigService.port, 236 | username: dbConfigService.use), 237 | password: dbConfigService.password, 238 | database: dbConfigService.database, 239 | autoLoadEntities: true, 240 | synchronize: false, 241 | }), 242 | inject: [DBConfigService], 243 | } as TypeOrmModuleAsyncOptions), 244 | ], 245 | }) 246 | export class MSSqlServerDatabaseProviderModule {} 247 | ``` 248 |
249 |
250 | 251 | ### Api Section 252 | 253 | **src/api** folder will simply be the parent folder that contains all model related data. 254 | 255 | By default, NestJs is designed with a 3-tier architecture as a base, so when we use the templates through the CLI, we easily see that 3 key elements will be created: 256 | 257 | 1. Controllers: A controller’s sole purpose is to receive requests for the application and deal with routes. 258 | 259 | 2. Services: This part of the block should only include business logic. For example, all the CRUD operations and methods to determine how data can be created, stored and updated. Those are the service files. 260 | 261 | 3. Data Access Layer: This layer takes care and provides logic to access data stored in persistent storage of some kind. Those are the entity and dto files. 262 | 263 | ``` 264 | src/models 265 | ├── health 266 | │ └── health.controller.ts 267 | └── users 268 | ├── dto 269 | │ └── create-user.dto.ts 270 | │ └── update-user.dto.ts 271 | ├── entities 272 | │ └── user.entity.ts 273 | ├── interfaces 274 | │ └── user.interface.ts 275 | ├── serializers 276 | │ └── user.serializer.ts 277 | ├── users.controller.ts 278 | ├── users.module.ts 279 | ├── users.repository.ts 280 | └── users.service.ts 281 | ``` 282 | Note: In this project, the health controller returns a status of 200 so that the Azure balancer works without problems. 283 |
284 |
285 | 286 | ### Common Files Section 287 | 288 | **src/common** folder has the most number of directories here. I use this common folder to literally fill it in with any extra classes/files that might commonly be used by other modules in your app. 289 | 290 | There is not much to explain here, except the fact that we are using NestJs core fundamentals elements like [guards](https://docs.nestjs.com/guards), [pipes](https://docs.nestjs.com/pipes), [decorators](https://docs.nestjs.com/custom-decorators), etc, in this folder, and some other common constants, interfaces and helpers. 291 | 292 | ``` 293 | src/common 294 | ├── constants 295 | ├── decorators 296 | ├── environments 297 | ├── exceptions 298 | ├── guards 299 | ├── helpers 300 | ├── interceptors 301 | ├── interfaces 302 | ├── middleware 303 | ├── pipes 304 | └── serializers 305 | ``` 306 | 307 | As I always say in class, be careful with the things that are put in this section, respect good practices, document properly and be aware of the use of this section, review very well before adding more things, it is very easy for it to become the project's garbage repository. 308 |
309 |
310 | 311 | ## Azure 312 | 313 | As part of the training, some [Azure services](https://azure.microsoft.com/en-us) were taken into account, the use of secrets in [Azure KeyVault](https://azure.microsoft.com/en-us/products/key-vault), the [SQLServer database](https://azure.microsoft.com/en-us/products/azure-sql/managed-instance) and [Blob Storage](https://azure.microsoft.com/en-us/products/storage/blobs) were added. 314 | 315 | Therefore, the sensitive information usually found in the environment variables was moved to the KeyVault, and the name of the secret is stored in the environment variables, support for reading the KeyVault and recovering the information contained in it was added. 316 | 317 | Regarding the storage, a mechanism has been created to allow access to the vault to store or read the files stored there. 318 | 319 | Since a balancer was used in Azure, the health service was added, given the time limitations of training, only a status of 200 is returned to indicate to the balancer that the instance is up, but ideally it should be complemented by indicating the real health status of the solution. If you want to add to this point, I recommend reading the [documentation](https://docs.nestjs.com/recipes/terminus). 320 |
321 |
322 | 323 | ## Others 324 | 325 | [OpenAPI](https://www.openapis.org/) support has been added through [Swagger](https://docs.nestjs.com/openapi/introduction), but its use is limited to the development environment. 326 | 327 | [Helmet](https://docs.nestjs.com/security/helmet) support has been added for security reasons. 328 | 329 | [Sentry.io](https://sentry.io/welcome/) support has been added as a log manager, an interceptor has been created that is used in the controllers. 330 | 331 | Good practices were an important part of the training; to reinforce them, the project was configured with [eslint](https://eslint.org/) in [strict mode](https://typescript-eslint.io/linting/configs/), the necessary configurations were added to the [compiler options](https://www.typescriptlang.org/tsconfig), and the [Prettier](https://prettier.io/) plugins were added. 332 |
333 |
334 | 335 | ## Development environment 336 | 337 | A development environment based on NodeJs is required. 338 | 339 | ## Installation 340 | 341 | ```bash 342 | $ npm install 343 | ``` 344 | 345 | ## Running the app 346 | 347 | ```bash 348 | # development 349 | $ npm run start 350 | 351 | # watch mode 352 | $ npm run start:dev 353 | 354 | # production mode 355 | $ npm run start:prod 356 | 357 | # local default url and port 358 | localhost:9000 359 | 360 | # default API route (prefix) 361 | localhost:9000/api/ 362 | 363 | # Swagger web interface (development environment only) 364 | localhost:9000/api-docs 365 | ``` 366 | 367 | ## Test 368 | 369 | ```bash 370 | # unit tests 371 | $ npm run test 372 | 373 | # watch mode 374 | $ npm run test:watch 375 | 376 | # e2e tests 377 | $ npm run test:e2e 378 | 379 | # test coverage 380 | $ npm run test:cov 381 | ``` 382 | 383 | ## Support 384 | 385 | Please note that this project is not a template as such, and given the great speed with which some components and projects in the JavaScript ecosystem evolve or die, it is possible that this project as it stands today will be completely useless at some point, especially that dedicated to the consumption of Azure services, for this reason my recommendation is to take into account the directory structure, and the configuration services, and be especially careful when it comes to Azure. 386 | 387 | On the other hand, it was built based on the Azure services that the client provided me, and since I do not have an Azure account, I doubt that I can update the project with each change in the Microsoft libraries for Nodejs. 388 | 389 | 390 | ## Stay in touch 391 | 392 | I speak Spanish in my daily life and English at a professional level, so you can expect spelling or grammatical errors, please inform me if you see any and if possible explain why, this will make it easier for me to continue learning and one day master English to perfection! 393 | 394 | [Fabian A. Becerra M.](https://github.com/fabecerram) 395 | 396 | ## License 397 | 398 | Code and documentation copyright 2019-2023 the authors. Code released under the MIT License. 399 | --------------------------------------------------------------------------------