├── .github └── workflows │ └── backend.yml ├── .gitignore ├── README.md ├── api ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── Procfile ├── README.md ├── nest-cli.json ├── package-lock.json ├── package.json ├── src │ ├── app.module.ts │ ├── controller │ │ ├── auth.controller.spec.ts │ │ ├── auth.controller.ts │ │ ├── dto │ │ │ ├── authenticate-user-request.dto.ts │ │ │ ├── authenticate-user-response.dto.ts │ │ │ ├── create-user-request.dto.ts │ │ │ ├── create-user-response.dto.ts │ │ │ └── get-user-by-id-response.dto.ts │ │ ├── users.controller.spec.ts │ │ └── users.controller.ts │ ├── domain │ │ └── user.domain.ts │ ├── main.ts │ ├── repository │ │ ├── user.repository.spec.ts │ │ └── users.repository.ts │ ├── service │ │ ├── auth.service.spec.ts │ │ ├── auth.service.ts │ │ ├── user.service.spec.ts │ │ └── users.service.ts │ └── shared │ │ ├── auth.guard.ts │ │ ├── constants.ts │ │ ├── public.decorator.ts │ │ └── types.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json ├── archictecture.drawio ├── collections ├── authentication │ └── Login.bru ├── bruno.json ├── environments │ ├── AWS DEV.bru │ └── Local.bru └── users │ ├── Create user.bru │ ├── Get authenticated user.bru │ └── Get user by ID.bru └── infra ├── .gitignore ├── .prettierrc ├── cdktf.json ├── main.ts ├── package-lock.json ├── package.json ├── stacks └── project.stack.ts └── tsconfig.json /.github/workflows/backend.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Backend to Elastic Beanstalk 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | build: 7 | name: Build Application 8 | runs-on: ubuntu-latest 9 | defaults: 10 | run: 11 | working-directory: api 12 | steps: 13 | - name: Checkout source code 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: '20' 20 | 21 | - name: Install dependencies 22 | run: npm ci 23 | 24 | - name: Run tests 25 | run: npm test 26 | 27 | - name: Build 28 | run: npm run build 29 | 30 | - name: Upload build artifact 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: artifact 34 | path: | 35 | api/* 36 | !api/src 37 | !api/test 38 | !api/node_modules 39 | deploy-dev: 40 | name: Deploy DEV Environment 41 | runs-on: ubuntu-latest 42 | needs: build 43 | steps: 44 | - name: Download build artifact 45 | uses: actions/download-artifact@v4 46 | with: 47 | name: artifact 48 | path: artifact 49 | - name: Zip Artifact for Deployment 50 | run: | 51 | cd artifact 52 | zip -r ../service.zip ./* 53 | - name: Deploy to Elastic Beanstalk 54 | uses: einaregilsson/beanstalk-deploy@v22 55 | with: 56 | aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }} 57 | aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 58 | application_name: dev-api-rest-do-zero-a-aws-com-terraform 59 | environment_name: dev-api-rest-do-zero-a-aws-com-terraform 60 | region: us-east-1 61 | version_label: ${{ github.run_id }} 62 | deployment_package: service.zip 63 | use_existing_version_if_available: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # API REST com NestJS do Zero à AWS com Terraform e GitHub Actions 2 | 3 | Obrigado por aparecer! 4 | 5 | **Este conteúdo não é para iniciantes!** 😊 6 | 7 | Neste vídeo, vou te ensinar a desenvolver uma API REST com autenticação, utilizando **NestJS** e **TypeScript**. Além disso, vamos provisionar a infraestrutura na **AWS** utilizando **Terraform** e **GitHub Actions**. E o melhor: tudo do absoluto zero, instalando todas as ferramentas necessárias. **#VAMOSCODAR** 8 | 9 | ## Descrição 10 | 11 | Este é o repositório do vídeo **API REST com NestJS do Zero à AWS com Terraform e GitHub Actions**. 12 | 13 | ✅ [ASSISTA O VÍDEO](https://youtu.be/csWHIujcbKI) 🚀🚀🚀🚀 14 | 15 | Se gostou, não se esqueça de **se inscrever no canal, deixar like e compartilhar com outros devs**. 16 | 17 | ## Sumário 18 | 19 | - [Ferramentas Necessárias](#ferramentas-necessárias) 20 | - [Clonar o Projeto](#clonar-o-projeto) 21 | - [Iniciar o Projeto](#iniciar-o-projeto) 22 | - [Executar os Testes](#executar-os-testes) 23 | - [Deploy da Infraestrutura](#deploy-da-infraestrutura) 24 | - [Dúvidas](#dúvidas) 25 | 26 | ## Ferramentas Necessárias 27 | 28 | Certifique-se de ter as seguintes ferramentas instaladas: 29 | 30 | - **Node Version Manager (NVM)** 31 | ```bash 32 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash 33 | ``` 34 | 35 | - **Node.js versão 20** 36 | ```bash 37 | nvm install 20 38 | nvm use 20 39 | ``` 40 | 41 | - **NestJS CLI** 42 | ```bash 43 | npm install -g @nestjs/cli 44 | ``` 45 | 46 | - **AWS CLI** 47 | [Documentação de Instalação](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) 48 | 49 | - **DynamoDB Local** 50 | ```bash 51 | docker run -d -p 8000:8000 amazon/dynamodb-local 52 | ``` 53 | 54 | - **DynamoDB Admin** 55 | ```bash 56 | npm install -g dynamodb-admin 57 | ``` 58 | 59 | - **Terraform** 60 | [Documentação de Instalação](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli) 61 | 62 | - **CDKTF** 63 | ```bash 64 | npm install --global cdktf-cli@latest 65 | ``` 66 | 67 | ## Clonar o Projeto 68 | Clone o repositório para a sua máquina local: 69 | 70 | ```bash 71 | git clone git@github.com:petrusdemelodev/001-api-rest-com-nestjs-do-zero-a-aws-com-terraform.git 72 | cd 001-api-rest-com-nestjs-do-zero-a-aws-com-terraform 73 | ``` 74 | 75 | ## Iniciar o Projeto 76 | Navegue até a pasta da API, instale as dependências e inicie o servidor de desenvolvimento: 77 | 78 | ```bash 79 | cd api 80 | npm install 81 | npm run start 82 | ``` 83 | 84 | ## Executar os Testes 85 | Para rodar os testes, execute: 86 | 87 | ```bash 88 | npm run test 89 | ``` 90 | 91 | ## Deploy da Infraestrutura 92 | Para provisionar a infraestrutura na AWS, execute: 93 | 94 | ```bash 95 | cd infra 96 | cdktf apply dev-project-stack 97 | ``` 98 | 99 | # Dúvidas 100 | 101 | Deixe seu comentário no vídeo! 😊 102 | 103 | Se este repositório foi útil para você, por favor, deixe uma estrela ⭐ nele no GitHub. Isso ajuda a divulgar o projeto e motiva a criação de mais conteúdos como este. 104 | 105 | # Redes Sociais 106 | 107 | Me segue nas redes sociais 108 | 109 | [INSTAGRAM](https://instagram.com/petrusdemelodev) | [LINKEDIN](https://linkedin.com/in/petrusdemelo) | [TWITTER](https://x.com/petrusdemelodev) | [MEDIUM](https://medium.com/@petrusdemelodev) -------------------------------------------------------------------------------- /api/.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/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'error', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off' 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # dotenv environment variable files 39 | .env 40 | .env.development.local 41 | .env.test.local 42 | .env.production.local 43 | .env.local 44 | 45 | # temp directory 46 | .temp 47 | .tmp 48 | 49 | # Runtime data 50 | pids 51 | *.pid 52 | *.seed 53 | *.pid.lock 54 | 55 | # Diagnostic reports (https://nodejs.org/api/report.html) 56 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 57 | -------------------------------------------------------------------------------- /api/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /api/Procfile: -------------------------------------------------------------------------------- 1 | web: npm ci --production && npm run start:prod -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /api/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 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --debug --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 | "@aws-sdk/client-dynamodb": "^3.637.0", 24 | "@aws-sdk/util-dynamodb": "^3.637.0", 25 | "@fastify/static": "^7.0.4", 26 | "@nestjs/common": "^10.0.0", 27 | "@nestjs/core": "^10.0.0", 28 | "@nestjs/jwt": "^10.2.0", 29 | "@nestjs/platform-express": "^10.0.0", 30 | "@nestjs/platform-fastify": "^10.4.1", 31 | "@nestjs/swagger": "^7.4.0", 32 | "bcrypt": "^5.1.1", 33 | "class-transformer": "^0.5.1", 34 | "class-validator": "^0.14.1", 35 | "fastify": "^4.28.1", 36 | "reflect-metadata": "^0.2.0", 37 | "rxjs": "^7.8.1", 38 | "uuid": "^10.0.0" 39 | }, 40 | "devDependencies": { 41 | "@nestjs/cli": "^10.0.0", 42 | "@nestjs/schematics": "^10.0.0", 43 | "@nestjs/testing": "^10.0.0", 44 | "@types/bcrypt": "^5.0.2", 45 | "@types/express": "^4.17.17", 46 | "@types/jest": "^29.5.2", 47 | "@types/node": "^20.3.1", 48 | "@types/supertest": "^6.0.0", 49 | "@types/uuid": "^10.0.0", 50 | "@typescript-eslint/eslint-plugin": "^7.0.0", 51 | "@typescript-eslint/parser": "^7.0.0", 52 | "aws-sdk-client-mock": "^4.0.1", 53 | "aws-sdk-client-mock-jest": "^4.0.1", 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": "^7.0.0", 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": ".", 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 | "moduleNameMapper": { 84 | "^@root(|/.*)$": "/src/$1" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /api/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UsersController } from './controller/users.controller'; 3 | import { UsersService } from './service/users.service'; 4 | import { UsersRepository } from './repository/users.repository'; 5 | import { AuthController } from './controller/auth.controller'; 6 | import { AuthService } from './service/auth.service'; 7 | import { JwtModule } from '@nestjs/jwt'; 8 | import { AuthGuard } from './shared/auth.guard'; 9 | import { APP_GUARD } from '@nestjs/core'; 10 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 11 | 12 | const { LOCAL_DEVELOPMENT = false, AWS_REGION = 'us-east-1' } = process.env; 13 | 14 | const client = new DynamoDBClient({ 15 | region: LOCAL_DEVELOPMENT ? undefined : AWS_REGION, 16 | endpoint: LOCAL_DEVELOPMENT ? 'http://localhost:8000' : undefined, 17 | }); 18 | 19 | @Module({ 20 | imports: [ 21 | JwtModule.register({ 22 | global: true, 23 | }), 24 | ], 25 | controllers: [UsersController, AuthController], 26 | providers: [ 27 | UsersService, 28 | UsersRepository, 29 | AuthService, 30 | { 31 | provide: APP_GUARD, 32 | useClass: AuthGuard, 33 | }, 34 | { 35 | provide: DynamoDBClient, 36 | useValue: client, 37 | }, 38 | ], 39 | }) 40 | export class AppModule {} 41 | -------------------------------------------------------------------------------- /api/src/controller/auth.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { AuthService } from '@root/service/auth.service'; 2 | import { AuthController } from './auth.controller'; 3 | import { UnauthorizedException } from '@nestjs/common'; 4 | 5 | describe('AuthController', () => { 6 | let authController: AuthController; 7 | 8 | const login: jest.Mock = jest.fn(); 9 | const authServiceMock = { 10 | login, 11 | } as any; 12 | 13 | beforeAll(() => { 14 | authController = new AuthController(authServiceMock as AuthService); 15 | }); 16 | 17 | beforeEach(() => { 18 | jest.clearAllMocks(); 19 | }); 20 | 21 | describe('login', () => { 22 | it('should return an access token', async () => { 23 | // given 24 | const email = 'john.doe@gmail.com'; 25 | const password = 'password'; 26 | login.mockReturnValue('access-token'); 27 | 28 | // when 29 | const authResponse = await authController.login({ email, password }); 30 | 31 | // then 32 | expect(login).toHaveBeenCalledTimes(1); 33 | expect(login).toHaveBeenCalledWith({ email, password }); 34 | expect(authResponse).toEqual({ accessToken: 'access-token' }); 35 | }); 36 | 37 | it('should return an exception if we send wrong credentials', async () => { 38 | // given 39 | const email = 'john.doe@gmail.com'; 40 | const password = 'wrong-password'; 41 | login.mockRejectedValue(new UnauthorizedException('Invalid credentials')); 42 | 43 | // then 44 | await expect(authController.login({ email, password })).rejects.toThrow( 45 | UnauthorizedException, 46 | ); 47 | expect(login).toHaveBeenCalledTimes(1); 48 | expect(login).toHaveBeenCalledWith({ email, password }); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /api/src/controller/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common'; 2 | import { AuthenticateUserRequestDTO } from './dto/authenticate-user-request.dto'; 3 | import { ApiTags } from '@nestjs/swagger'; 4 | import { AuthService } from '@root/service/auth.service'; 5 | import { AuthenticateUserResponseDTO } from './dto/authenticate-user-response.dto'; 6 | import { Public } from '@root/shared/public.decorator'; 7 | 8 | @Controller('auth') 9 | @ApiTags('auth') 10 | export class AuthController { 11 | constructor(private readonly authService: AuthService) {} 12 | 13 | @Public() 14 | @Post('login') 15 | public async login( 16 | @Body() credentials: AuthenticateUserRequestDTO, 17 | ): Promise { 18 | const token = await this.authService.login({ 19 | email: credentials.email, 20 | password: credentials.password, 21 | }); 22 | 23 | return new AuthenticateUserResponseDTO(token); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /api/src/controller/dto/authenticate-user-request.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; 3 | 4 | export class AuthenticateUserRequestDTO { 5 | @ApiProperty({ 6 | name: 'email', 7 | description: 'email of the user', 8 | }) 9 | @IsEmail() 10 | @IsNotEmpty() 11 | public email: string; 12 | 13 | @ApiProperty({ 14 | name: 'password', 15 | description: 'password of the user', 16 | }) 17 | @IsNotEmpty() 18 | @IsString() 19 | public password: string; 20 | } 21 | -------------------------------------------------------------------------------- /api/src/controller/dto/authenticate-user-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class AuthenticateUserResponseDTO { 4 | @ApiProperty({ 5 | name: 'accessToken', 6 | description: 'JWT access token', 7 | }) 8 | public accessToken: string; 9 | 10 | constructor(accessToken: string) { 11 | this.accessToken = accessToken; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /api/src/controller/dto/create-user-request.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsEmail, IsNotEmpty } from 'class-validator'; 3 | 4 | export class CreateUserRequestDTO { 5 | @ApiProperty({ 6 | description: 'The name of the user', 7 | example: 'John Doe', 8 | }) 9 | @IsNotEmpty() 10 | public name: string; 11 | 12 | @ApiProperty({ 13 | description: 'The email of the user', 14 | example: 'john.doe@gmail.com', 15 | }) 16 | @IsNotEmpty() 17 | @IsEmail() 18 | public email: string; 19 | 20 | @ApiProperty({ 21 | description: 'Password of the user', 22 | example: '123456', 23 | }) 24 | @IsNotEmpty() 25 | public password: string; 26 | 27 | constructor(partial: Partial) { 28 | Object.assign(this, partial); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /api/src/controller/dto/create-user-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class CreateUserResponseDTO { 4 | @ApiProperty({ 5 | description: 'The id of the created user', 6 | example: '209c4620-8031-4f05-9c07-dbd095c1d407', 7 | }) 8 | public id: string; 9 | 10 | constructor(id: string) { 11 | this.id = id; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /api/src/controller/dto/get-user-by-id-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class GetUserByIDResponseDTO { 4 | @ApiProperty({ 5 | name: 'id', 6 | description: 'id of the user', 7 | example: '123e4567-e89b-12d3-a456-426614174000', 8 | }) 9 | public id: string; 10 | 11 | @ApiProperty({ 12 | name: 'name', 13 | description: 'name of the user', 14 | example: 'John Doe', 15 | }) 16 | public name: string; 17 | 18 | @ApiProperty({ 19 | name: 'email', 20 | description: 'email of the user', 21 | example: 'john.doe@gmail.com', 22 | }) 23 | public email: string; 24 | 25 | @ApiProperty({ 26 | name: 'createdAt', 27 | description: 'when the user was created', 28 | example: '2021-01-01T00:00:00.000Z', 29 | }) 30 | public createdAt: string; 31 | 32 | @ApiProperty({ 33 | name: 'updatedAt', 34 | description: 'when the user was last updated', 35 | example: '2021-01-01T00:00:00.000Z', 36 | }) 37 | public updatedAt: string; 38 | 39 | constructor(params: GetUserByIDResponseDTO) { 40 | this.id = params.id; 41 | this.name = params.name; 42 | this.email = params.email; 43 | this.createdAt = params.createdAt; 44 | this.updatedAt = params.updatedAt; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /api/src/controller/users.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { UsersService } from '@root/service/users.service'; 2 | import { CreateUserRequestDTO } from './dto/create-user-request.dto'; 3 | import { UsersController } from './users.controller'; 4 | import { User } from '@root/domain/user.domain'; 5 | import { AuthenticatedRequest } from '@root/shared/types'; 6 | 7 | describe('UsersController', () => { 8 | let usersController: UsersController; 9 | 10 | const createUser: jest.Mock = jest.fn(); 11 | const getUserByID: jest.Mock = jest.fn(); 12 | const userServiceMock = { 13 | createUser, 14 | getUserByID, 15 | } as any; 16 | 17 | beforeAll(() => { 18 | usersController = new UsersController(userServiceMock as UsersService); 19 | }); 20 | 21 | beforeEach(() => { 22 | jest.clearAllMocks(); 23 | }); 24 | 25 | describe('createUser', () => { 26 | it('should create an user', async () => { 27 | // given 28 | const body = new CreateUserRequestDTO({ 29 | email: 'john.doe@gmail.com', 30 | name: 'John Doe', 31 | password: 'password', 32 | }); 33 | createUser.mockReturnValue('random-user-id'); 34 | 35 | // when 36 | const createUserResponse = await usersController.createUser(body); 37 | 38 | // then 39 | expect(createUser).toHaveBeenCalledTimes(1); 40 | expect(createUser).toHaveBeenCalledWith({ 41 | email: body.email, 42 | name: body.name, 43 | password: body.password, 44 | }); 45 | expect(createUserResponse).toEqual({ id: 'random-user-id' }); 46 | }); 47 | }); 48 | 49 | describe('getUser', () => { 50 | it('should get an user by ID', async () => { 51 | // given 52 | const user = new User({ 53 | id: 'random-user-id', 54 | name: 'John Doe', 55 | email: 'john.doe@gmail.com', 56 | password: '123456', 57 | }); 58 | 59 | getUserByID.mockReturnValue(user); 60 | 61 | // when 62 | const getUserResponse = await usersController.getUserByID(user.id); 63 | 64 | // then 65 | expect(getUserByID).toHaveBeenCalledWith(user.id); 66 | expect(getUserResponse).toEqual({ 67 | id: user.id, 68 | name: user.name, 69 | email: user.email, 70 | createdAt: user.createdAt.toISOString(), 71 | updatedAt: user.updatedAt.toISOString(), 72 | }); 73 | }); 74 | }); 75 | 76 | describe('getMe', () => { 77 | it('should return a user from the authenticated request', async () => { 78 | // given 79 | const user = new User({ 80 | id: 'random-user-id', 81 | name: 'John Doe', 82 | email: 'john.doe@gmail.com', 83 | password: '123456', 84 | }); 85 | 86 | getUserByID.mockReturnValue(user); 87 | 88 | // when 89 | const getUserResponse = await usersController.getMe({ 90 | userID: user.id, 91 | } as any as AuthenticatedRequest); 92 | 93 | // then 94 | expect(getUserByID).toHaveBeenCalledWith(user.id); 95 | expect(getUserResponse).toEqual({ 96 | id: user.id, 97 | name: user.name, 98 | email: user.email, 99 | createdAt: user.createdAt.toISOString(), 100 | updatedAt: user.updatedAt.toISOString(), 101 | }); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /api/src/controller/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | HttpStatus, 6 | Param, 7 | Post, 8 | Req, 9 | } from '@nestjs/common'; 10 | import { ApiResponse, ApiTags } from '@nestjs/swagger'; 11 | import { UsersService } from '@root/service/users.service'; 12 | import { CreateUserRequestDTO } from './dto/create-user-request.dto'; 13 | import { CreateUserResponseDTO } from './dto/create-user-response.dto'; 14 | import { GetUserByIDResponseDTO } from './dto/get-user-by-id-response.dto'; 15 | import { AuthenticatedRequest } from '@root/shared/types'; 16 | import { Public } from '@root/shared/public.decorator'; 17 | 18 | @ApiTags('users') 19 | @Controller('users') 20 | export class UsersController { 21 | constructor(private readonly usersService: UsersService) {} 22 | 23 | @Public() 24 | @Post() 25 | @ApiResponse({ 26 | status: HttpStatus.CREATED, 27 | description: 'User created', 28 | type: CreateUserResponseDTO, 29 | }) 30 | public async createUser( 31 | @Body() createUserBody: CreateUserRequestDTO, 32 | ): Promise { 33 | const createdUserID = await this.usersService.createUser({ 34 | name: createUserBody.name, 35 | email: createUserBody.email, 36 | password: createUserBody.password, 37 | }); 38 | 39 | return new CreateUserResponseDTO(createdUserID); 40 | } 41 | 42 | @Get(':userID') 43 | @Public() 44 | @ApiResponse({ 45 | status: HttpStatus.OK, 46 | description: 'User found', 47 | type: GetUserByIDResponseDTO, 48 | }) 49 | public async getUserByID( 50 | @Param('userID') userID: string, 51 | ): Promise { 52 | const userResult = await this.usersService.getUserByID(userID); 53 | 54 | return new GetUserByIDResponseDTO({ 55 | id: userResult.id, 56 | name: userResult.name, 57 | email: userResult.email, 58 | createdAt: userResult.createdAt.toISOString(), 59 | updatedAt: userResult.updatedAt.toISOString(), 60 | }); 61 | } 62 | 63 | @Get('me') 64 | @ApiResponse({ 65 | status: HttpStatus.OK, 66 | description: 'User found', 67 | type: GetUserByIDResponseDTO, 68 | }) 69 | public async getMe( 70 | @Req() request: AuthenticatedRequest, 71 | ): Promise { 72 | return this.getUserByID(request.userID); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /api/src/domain/user.domain.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from 'uuid'; 2 | 3 | interface UserProps { 4 | name: string; 5 | email: string; 6 | password: string; 7 | } 8 | 9 | type UserUpdate = Partial & UserProps; 10 | 11 | export class User { 12 | public readonly id: string; 13 | public readonly name: string; 14 | public readonly email: string; 15 | public readonly createdAt: Date; 16 | public password: string; 17 | public updatedAt: Date; 18 | 19 | constructor(init: UserUpdate) { 20 | Object.assign( 21 | this, 22 | { 23 | id: uuid(), 24 | createdAt: new Date(), 25 | updatedAt: new Date(), 26 | }, 27 | init, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /api/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 4 | import { ValidationPipe } from '@nestjs/common'; 5 | import { FastifyAdapter } from '@nestjs/platform-fastify'; 6 | 7 | const { PORT = 3000 } = process.env; 8 | 9 | async function bootstrap(): Promise { 10 | const app = await NestFactory.create(AppModule, new FastifyAdapter()); 11 | app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true })); 12 | 13 | const config = new DocumentBuilder() 14 | .setTitle('API Rest NestJS do Zero a AWS com Terraform') 15 | .addBearerAuth() 16 | .setVersion('1.0') 17 | .build(); 18 | 19 | const swaggerCDN = 'https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.7.2'; 20 | const document = SwaggerModule.createDocument(app, config); 21 | 22 | SwaggerModule.setup('api', app, document, { 23 | customCssUrl: [`${swaggerCDN}/swagger-ui.css`], 24 | customJs: [ 25 | `${swaggerCDN}/swagger-ui-bundle.js`, 26 | `${swaggerCDN}/swagger-ui-standalone-preset.js`, 27 | ], 28 | }); 29 | 30 | await app.listen(PORT); 31 | console.log(`Server is running on: http://localhost:${PORT}`); 32 | console.log(`📄 Swagger is on: http://localhost:${PORT}/api`); 33 | } 34 | 35 | bootstrap(); 36 | -------------------------------------------------------------------------------- /api/src/repository/user.repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DynamoDBClient, 3 | GetItemCommand, 4 | PutItemCommand, 5 | QueryCommand, 6 | } from '@aws-sdk/client-dynamodb'; 7 | import { mockClient } from 'aws-sdk-client-mock'; 8 | import 'aws-sdk-client-mock-jest'; 9 | import { marshall } from '@aws-sdk/util-dynamodb'; 10 | import { UsersRepository } from './users.repository'; 11 | import { User } from '@root/domain/user.domain'; 12 | 13 | describe('UserRepository', () => { 14 | let repository: UsersRepository; 15 | const dynamoDBClient = new DynamoDBClient({}); 16 | const dynamodbClientMock = mockClient(dynamoDBClient); 17 | 18 | beforeAll(() => { 19 | repository = new UsersRepository(dynamoDBClient); 20 | }); 21 | 22 | beforeEach(() => { 23 | jest.clearAllMocks(); 24 | dynamodbClientMock.reset(); 25 | }); 26 | 27 | describe('create', () => { 28 | it('should create a user', async () => { 29 | // given 30 | const user = new User({ 31 | name: 'John Doe', 32 | email: 'john.doe@gmail.com', 33 | password: '123456', 34 | }); 35 | 36 | // when 37 | await repository.createUser(user); 38 | 39 | // then 40 | expect(dynamodbClientMock).toHaveReceivedCommandWith(PutItemCommand, { 41 | TableName: `dev-users`, 42 | Item: marshall({ 43 | id: user.id, 44 | name: user.name, 45 | email: user.email, 46 | password: user.password, 47 | createdAt: user.createdAt.toISOString(), 48 | updatedAt: user.updatedAt.toISOString(), 49 | }), 50 | }); 51 | }); 52 | }); 53 | 54 | describe('getByID', () => { 55 | it('should get a user by ID if user exists', async () => { 56 | // given 57 | const id = '27190ff5-e5bb-4800-b23a-191b34670904'; 58 | 59 | dynamodbClientMock.on(GetItemCommand).resolves({ 60 | Item: marshall({ 61 | id, 62 | name: 'John Doe', 63 | email: 'john.doe@gmail.com', 64 | password: '123456', 65 | createdAt: new Date('2024-06-01').toISOString(), 66 | updatedAt: new Date('2024-06-01').toISOString(), 67 | }), 68 | }); 69 | 70 | // when 71 | const user = await repository.getUserByID(id); 72 | 73 | // then 74 | expect(user).toBeInstanceOf(User); 75 | expect(user.id).toBe(id); 76 | expect(user.name).toBe('John Doe'); 77 | expect(user.email).toBe('john.doe@gmail.com'); 78 | expect(user.password).toBe('123456'); 79 | expect(user.createdAt).toEqual(new Date('2024-06-01')); 80 | expect(user.updatedAt).toEqual(new Date('2024-06-01')); 81 | expect(dynamodbClientMock).toHaveReceivedCommandWith(GetItemCommand, { 82 | TableName: `dev-users`, 83 | Key: marshall({ id }), 84 | }); 85 | }); 86 | 87 | it('should return undefined if user does not exist', async () => { 88 | // given 89 | const id = '27190ff5-e5bb-4800-b23a-191b34670904'; 90 | 91 | dynamodbClientMock.on(GetItemCommand).resolves({ 92 | Item: null, 93 | }); 94 | 95 | // when 96 | const user = await repository.getUserByID(id); 97 | 98 | // then 99 | expect(user).toBeUndefined(); 100 | }); 101 | }); 102 | 103 | describe('getByEmail', () => { 104 | it('should get a user by email if user exists', async () => { 105 | // given 106 | const email = 'john.doe@gmail.com'; 107 | 108 | dynamodbClientMock.on(QueryCommand).resolves({ 109 | Items: [ 110 | marshall({ 111 | id: '27190ff5-e5bb-4800-b23a-191b34670904', 112 | name: 'John Doe', 113 | email: 'john.doe@gmail.com', 114 | password: '123456', 115 | createdAt: new Date('2024-06-01').toISOString(), 116 | updatedAt: new Date('2024-06-01').toISOString(), 117 | }), 118 | ], 119 | }); 120 | 121 | // when 122 | const user = await repository.getUserByEmail(email); 123 | 124 | // then 125 | expect(user).toBeInstanceOf(User); 126 | expect(user.id).toBe('27190ff5-e5bb-4800-b23a-191b34670904'); 127 | expect(user.name).toBe('John Doe'); 128 | expect(user.email).toBe('john.doe@gmail.com'); 129 | expect(user.password).toBe('123456'); 130 | expect(user.createdAt).toEqual(new Date('2024-06-01')); 131 | expect(user.updatedAt).toEqual(new Date('2024-06-01')); 132 | expect(dynamodbClientMock).toHaveReceivedCommandWith(QueryCommand, { 133 | TableName: `dev-users`, 134 | IndexName: 'email_index', 135 | KeyConditionExpression: 'email = :email', 136 | ExpressionAttributeValues: { 137 | ':email': { S: email }, 138 | }, 139 | }); 140 | }); 141 | 142 | it('should return undefined when email does not exist in any user', async () => { 143 | // given 144 | const email = 'john.doe@gmail.com'; 145 | 146 | dynamodbClientMock.on(QueryCommand).resolves({ 147 | Items: [], 148 | }); 149 | 150 | // when 151 | const user = await repository.getUserByEmail(email); 152 | 153 | // then 154 | expect(user).toBeUndefined(); 155 | expect(dynamodbClientMock).toHaveReceivedCommandWith(QueryCommand, { 156 | TableName: `dev-users`, 157 | IndexName: 'email_index', 158 | KeyConditionExpression: 'email = :email', 159 | ExpressionAttributeValues: { 160 | ':email': { S: email }, 161 | }, 162 | }); 163 | }); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /api/src/repository/users.repository.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DynamoDBClient, 3 | GetItemCommand, 4 | PutItemCommand, 5 | QueryCommand, 6 | } from '@aws-sdk/client-dynamodb'; 7 | import { Injectable } from '@nestjs/common'; 8 | import { User } from '@root/domain/user.domain'; 9 | import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'; 10 | 11 | const { ENVIRONMENT = 'dev' } = process.env; 12 | 13 | interface UserRecord { 14 | id: string; 15 | email: string; 16 | password: string; 17 | name: string; 18 | createdAt: string; 19 | updatedAt: string; 20 | } 21 | 22 | @Injectable() 23 | export class UsersRepository { 24 | constructor(private readonly client: DynamoDBClient) {} 25 | 26 | public async createUser(user: User): Promise { 27 | const command = new PutItemCommand({ 28 | TableName: `${ENVIRONMENT}-users`, 29 | Item: marshall({ 30 | id: user.id, 31 | name: user.name, 32 | email: user.email, 33 | password: user.password, 34 | createdAt: user.createdAt.toISOString(), 35 | updatedAt: user.updatedAt.toISOString(), 36 | }), 37 | ConditionExpression: 'attribute_not_exists(id)', 38 | }); 39 | 40 | await this.client.send(command); 41 | } 42 | 43 | public async getUserByEmail(email: string): Promise { 44 | const command = new QueryCommand({ 45 | TableName: `${ENVIRONMENT}-users`, 46 | IndexName: 'email_index', 47 | KeyConditionExpression: 'email = :email', 48 | ExpressionAttributeValues: marshall({ ':email': email }), 49 | }); 50 | 51 | const response = await this.client.send(command); 52 | 53 | if (response.Items?.length === 0) { 54 | return undefined; 55 | } 56 | 57 | const userRecord = unmarshall(response.Items[0]) as UserRecord; 58 | return this.mapUserFromUserRecord(userRecord); 59 | } 60 | 61 | public async getUserByID(userID: string): Promise { 62 | const command = new GetItemCommand({ 63 | TableName: `${ENVIRONMENT}-users`, 64 | Key: marshall({ id: userID }), 65 | }); 66 | 67 | const response = await this.client.send(command); 68 | 69 | if (!response.Item) { 70 | return undefined; 71 | } 72 | 73 | const userRecord = unmarshall(response.Item) as UserRecord; 74 | return this.mapUserFromUserRecord(userRecord); 75 | } 76 | 77 | private mapUserFromUserRecord(record: UserRecord): User { 78 | return new User({ 79 | id: record.id, 80 | email: record.email, 81 | password: record.password, 82 | name: record.name, 83 | createdAt: new Date(record.createdAt), 84 | updatedAt: new Date(record.updatedAt), 85 | }); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /api/src/service/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { UnauthorizedException } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { JwtService } from '@nestjs/jwt'; 4 | import { UsersRepository } from '@root/repository/users.repository'; 5 | import { hash } from 'bcrypt'; 6 | 7 | describe('AuthService', () => { 8 | let authService: AuthService; 9 | 10 | const jwtServiceMock = { signAsync: jest.fn() }; 11 | const userRepositoryMock = { getUserByEmail: jest.fn() }; 12 | 13 | beforeAll(() => { 14 | authService = new AuthService( 15 | userRepositoryMock as any as UsersRepository, 16 | jwtServiceMock as any as JwtService, 17 | ); 18 | }); 19 | 20 | beforeEach(() => { 21 | jest.clearAllMocks(); 22 | }); 23 | 24 | describe('authenticateUser', () => { 25 | it('should create an access token for a correct user credentials', async () => { 26 | // given 27 | const { email, password } = { 28 | email: 'john.doe@gmail.com', 29 | password: 'password', 30 | }; 31 | 32 | const user = { 33 | id: 'random-user-id', 34 | email, 35 | password: await hash(password, 10), 36 | }; 37 | 38 | userRepositoryMock.getUserByEmail.mockReturnValue(user); 39 | jwtServiceMock.signAsync.mockReturnValue('access-token'); 40 | 41 | // when 42 | const token = await authService.login({ email, password }); 43 | 44 | // then 45 | expect(userRepositoryMock.getUserByEmail).toHaveBeenCalledTimes(1); 46 | expect(userRepositoryMock.getUserByEmail).toHaveBeenCalledWith(email); 47 | expect(jwtServiceMock.signAsync).toHaveBeenCalledTimes(1); 48 | expect(token).toEqual('access-token'); 49 | }); 50 | 51 | it('should return an exception for wrong credentials', async () => { 52 | // given 53 | const { email, password } = { 54 | email: 'john.doe@gmail.com', 55 | password: 'password', 56 | }; 57 | 58 | const user = { 59 | id: 'random-user-id', 60 | email, 61 | password: await hash(password, 10), 62 | }; 63 | 64 | userRepositoryMock.getUserByEmail.mockReturnValue(user); 65 | 66 | // when 67 | const token = authService.login({ 68 | email, 69 | password: 'wrong-password', 70 | }); 71 | 72 | // then 73 | expect(userRepositoryMock.getUserByEmail).toHaveBeenCalledTimes(1); 74 | expect(userRepositoryMock.getUserByEmail).toHaveBeenCalledWith(email); 75 | expect(jwtServiceMock.signAsync).toHaveBeenCalledTimes(0); 76 | await expect(token).rejects.toThrow(UnauthorizedException); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /api/src/service/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { UsersRepository } from '@root/repository/users.repository'; 4 | import { JWT_SECRET } from '@root/shared/constants'; 5 | import { TokenPayload } from '@root/shared/types'; 6 | import { compare } from 'bcrypt'; 7 | 8 | interface Credentials { 9 | email: string; 10 | password: string; 11 | } 12 | 13 | @Injectable() 14 | export class AuthService { 15 | constructor( 16 | private readonly userRepository: UsersRepository, 17 | private readonly jwtService: JwtService, 18 | ) {} 19 | 20 | public async login(credentials: Credentials): Promise { 21 | const { email, password } = credentials; 22 | 23 | const user = await this.userRepository.getUserByEmail(email); 24 | const passwordMatch = await compare(password, user?.password ?? ''); 25 | 26 | if (!user || !passwordMatch) { 27 | throw new UnauthorizedException('Invalid credentials'); 28 | } 29 | 30 | const payload: TokenPayload = { 31 | sub: user.id, 32 | name: user.name, 33 | email: user.email, 34 | exp: Math.floor(Date.now() / 1000) + 60 * 60, // 1 hour 35 | iat: Math.floor(Date.now() / 1000), 36 | aud: 'PetrusDeMeloDEV - Se inscreva no canal e ative o sininho', 37 | }; 38 | 39 | const token = await this.jwtService.signAsync(payload, { 40 | secret: JWT_SECRET, 41 | }); 42 | 43 | return token; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /api/src/service/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundException } from '@nestjs/common'; 2 | import { compare } from 'bcrypt'; 3 | import { UsersService } from './users.service'; 4 | import { UsersRepository } from '@root/repository/users.repository'; 5 | import { User } from '@root/domain/user.domain'; 6 | 7 | describe('UserService', () => { 8 | let service: UsersService; 9 | let userRepository: UsersRepository; 10 | const createUser: jest.Mock = jest.fn(); 11 | const getUserByEmail: jest.Mock = jest.fn(); 12 | const getUserByID: jest.Mock = jest.fn(); 13 | 14 | beforeAll(() => { 15 | userRepository = { 16 | createUser, 17 | getUserByEmail, 18 | getUserByID, 19 | } as any as UsersRepository; 20 | service = new UsersService(userRepository); 21 | }); 22 | 23 | beforeEach(() => { 24 | jest.clearAllMocks(); 25 | }); 26 | 27 | describe('createUser', () => { 28 | it('should create a user', async () => { 29 | // given 30 | const params = { 31 | name: 'John Doe', 32 | email: 'john.doe@gmail.com', 33 | password: 'password', 34 | }; 35 | 36 | // when 37 | await service.createUser(params); 38 | 39 | // then 40 | expect(userRepository.createUser).toHaveBeenCalledWith( 41 | expect.objectContaining({ 42 | name: params.name, 43 | email: params.email, 44 | password: expect.any(String), 45 | }), 46 | ); 47 | expect(userRepository.createUser).toHaveBeenCalledTimes(1); 48 | expect(createUser.mock.calls[0][0]).toBeInstanceOf(User); 49 | expect( 50 | compare(params.password, createUser.mock.calls[0][0].password), 51 | ).resolves.toBeTruthy(); 52 | }); 53 | }); 54 | 55 | describe('getUserByID', () => { 56 | it('should return a set of data from user', async () => { 57 | // given 58 | const user = new User({ 59 | name: 'John Doe', 60 | email: 'john.doe@gmail.com', 61 | password: 'password', 62 | }); 63 | getUserByID.mockResolvedValue(user); 64 | 65 | // when 66 | const result = await service.getUserByID(user.id); 67 | 68 | // then 69 | expect(userRepository.getUserByID).toHaveBeenCalledTimes(1); 70 | expect(userRepository.getUserByID).toHaveBeenCalledWith(user.id); 71 | expect(result).toEqual({ 72 | id: user.id, 73 | name: user.name, 74 | email: user.email, 75 | createdAt: user.createdAt, 76 | updatedAt: user.updatedAt, 77 | }); 78 | }); 79 | 80 | it('should throw an exception if user does not exist', async () => { 81 | // given 82 | getUserByID.mockResolvedValue(undefined); 83 | 84 | // then 85 | await expect(service.getUserByID('invalid-id')).rejects.toThrow( 86 | NotFoundException, 87 | ); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /api/src/service/users.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Injectable, 4 | NotFoundException, 5 | } from '@nestjs/common'; 6 | import { User } from '@root/domain/user.domain'; 7 | import { UsersRepository } from '@root/repository/users.repository'; 8 | import { hash } from 'bcrypt'; 9 | 10 | interface CreateUserParams { 11 | name: string; 12 | email: string; 13 | password: string; 14 | } 15 | 16 | interface IUser { 17 | id: string; 18 | name: string; 19 | email: string; 20 | createdAt: Date; 21 | updatedAt: Date; 22 | } 23 | 24 | const SALT_ROUNDS = 10; 25 | 26 | @Injectable() 27 | export class UsersService { 28 | constructor(private readonly usersRepository: UsersRepository) {} 29 | 30 | public async createUser(params: CreateUserParams): Promise { 31 | const userExists: User = await this.usersRepository.getUserByEmail( 32 | params.email, 33 | ); 34 | 35 | if (userExists) { 36 | throw new BadRequestException('Email already in use'); 37 | } 38 | 39 | const passwordHash = await hash(params.password, SALT_ROUNDS); 40 | 41 | const user = new User({ 42 | name: params.name, 43 | email: params.email, 44 | password: passwordHash, 45 | }); 46 | 47 | await this.usersRepository.createUser(user); 48 | return user.id; 49 | } 50 | 51 | public async getUserByID(userID: string): Promise { 52 | const user: User = await this.usersRepository.getUserByID(userID); 53 | 54 | if (!user) { 55 | throw new NotFoundException('User not found'); 56 | } 57 | 58 | return { 59 | id: user.id, 60 | name: user.name, 61 | email: user.email, 62 | createdAt: user.createdAt, 63 | updatedAt: user.updatedAt, 64 | }; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /api/src/shared/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | Injectable, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | import { Reflector } from '@nestjs/core'; 8 | import { JwtService } from '@nestjs/jwt'; 9 | import { FastifyRequest as Request } from 'fastify'; 10 | import { JWT_SECRET, PUBLIC_ENDPOINT_METADATA_KEY } from './constants'; 11 | import { TokenPayload } from './types'; 12 | 13 | @Injectable() 14 | export class AuthGuard implements CanActivate { 15 | constructor( 16 | private readonly jwtService: JwtService, 17 | private readonly reflector: Reflector, 18 | ) {} 19 | 20 | public async canActivate(context: ExecutionContext): Promise { 21 | const isPublic = this.reflector.getAllAndOverride( 22 | PUBLIC_ENDPOINT_METADATA_KEY, 23 | [context.getHandler(), context.getClass()], 24 | ); 25 | 26 | if (isPublic) { 27 | return true; 28 | } 29 | 30 | const request = context.switchToHttp().getRequest(); 31 | const token = this.extractTokenFromHeader(request); 32 | 33 | if (!token) { 34 | throw new UnauthorizedException(); 35 | } 36 | 37 | try { 38 | const payload: TokenPayload = await this.jwtService.verifyAsync(token, { 39 | secret: JWT_SECRET, 40 | }); 41 | 42 | request.userID = payload.sub; 43 | } catch { 44 | throw new UnauthorizedException(); 45 | } 46 | 47 | return true; 48 | } 49 | 50 | private extractTokenFromHeader(request: Request): string | undefined { 51 | const [type, token] = request.headers.authorization?.split(' ') ?? []; 52 | return type === 'Bearer' ? token : undefined; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /api/src/shared/constants.ts: -------------------------------------------------------------------------------- 1 | export const JWT_SECRET = 'G0fS,`~pga_o"X=}AL.B=;+8:[%13-'; 2 | export const PUBLIC_ENDPOINT_METADATA_KEY = 'public-endpoint'; 3 | -------------------------------------------------------------------------------- /api/src/shared/public.decorator.ts: -------------------------------------------------------------------------------- 1 | import { PUBLIC_ENDPOINT_METADATA_KEY } from './constants'; 2 | import { CustomDecorator, SetMetadata } from '@nestjs/common'; 3 | 4 | export const Public = (): CustomDecorator => 5 | SetMetadata(PUBLIC_ENDPOINT_METADATA_KEY, true); 6 | -------------------------------------------------------------------------------- /api/src/shared/types.ts: -------------------------------------------------------------------------------- 1 | import { FastifyRequest } from 'fastify'; 2 | 3 | export interface TokenPayload { 4 | sub: string; 5 | name: string; 6 | email: string; 7 | exp: number; 8 | iat: number; 9 | aud: string; 10 | } 11 | 12 | export type AuthenticatedRequest = { 13 | userID: string; 14 | } & FastifyRequest; 15 | -------------------------------------------------------------------------------- /api/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /api/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /api/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /api/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": "ES2021", 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 | "paths": { 21 | "@root": [ 22 | "src" 23 | ], 24 | "@root/*": [ 25 | "src/*" 26 | ] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /archictecture.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /collections/authentication/Login.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Login 3 | type: http 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: {{host}}/auth/login 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | { 15 | "email": "john.doe@gmail.com", 16 | "password": "123456" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /collections/bruno.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1", 3 | "name": "001-api-rest-com-nestjs-do-zero-a-aws", 4 | "type": "collection", 5 | "ignore": [ 6 | "node_modules", 7 | ".git" 8 | ] 9 | } -------------------------------------------------------------------------------- /collections/environments/AWS DEV.bru: -------------------------------------------------------------------------------- 1 | vars { 2 | host: awseb-e-p-AWSEBLoa-19F30FA4MU4DX-241478456.us-east-1.elb.amazonaws.com 3 | } 4 | -------------------------------------------------------------------------------- /collections/environments/Local.bru: -------------------------------------------------------------------------------- 1 | vars { 2 | host: http://localhost:3000 3 | } 4 | -------------------------------------------------------------------------------- /collections/users/Create user.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Create user 3 | type: http 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: {{host}}/users 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | { 15 | "name": "Fulano de Tal", 16 | "email": "fulanodetal@gmail.com", 17 | "password": "password" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /collections/users/Get authenticated user.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Get authenticated user 3 | type: http 4 | seq: 3 5 | } 6 | 7 | get { 8 | url: {{host}}/users/me 9 | body: none 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiM2U4OTllMi0zNzM2LTQ2YzQtOTMxNS0zYzI0NTNmMjY4MjEiLCJuYW1lIjoiSm9obiBEb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGdtYWlsLmNvbSIsImV4cCI6MTcyNDYxODU0MywiaWF0IjoxNzI0NjE0OTQzLCJhdWQiOiJQZXRydXNEZU1lbG9ERVYgLSBTZSBpbnNjcmV2YSBubyBjYW5hbCBlIGF0aXZlIG8gc2luaW5obyJ9.1B3fp5wvZqcI40_Js7FSuzxd4NSsp7GdbSCwINqXNs8 15 | } 16 | -------------------------------------------------------------------------------- /collections/users/Get user by ID.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Get user by ID 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: {{host}}/users/29a3687c-f4ff-4495-9212-5a5c29922bc9 9 | body: none 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /infra/.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | *.js 3 | node_modules 4 | cdktf.out 5 | cdktf.log 6 | *terraform.*.tfstate* 7 | .gen 8 | .terraform 9 | tsconfig.tsbuildinfo 10 | !jest.config.js 11 | !setup.js -------------------------------------------------------------------------------- /infra/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /infra/cdktf.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "typescript", 3 | "app": "npx ts-node main.ts", 4 | "projectId": "c09b9f92-73cf-4ad4-9e2d-eafcb3d12db2", 5 | "sendCrashReports": "false", 6 | "terraformProviders": [], 7 | "terraformModules": [], 8 | "context": { 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /infra/main.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'cdktf'; 2 | import { ProjectStack } from './stacks/project.stack'; 3 | 4 | const app = new App(); 5 | 6 | new ProjectStack(app, 'dev-project-stack', { 7 | environmentName: 'dev', 8 | region: 'us-east-1', 9 | }); 10 | 11 | app.synth(); 12 | -------------------------------------------------------------------------------- /infra/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "infra", 3 | "version": "1.0.0", 4 | "main": "main.js", 5 | "types": "main.ts", 6 | "license": "MPL-2.0", 7 | "private": true, 8 | "scripts": { 9 | "get": "cdktf get", 10 | "build": "tsc", 11 | "synth": "cdktf synth", 12 | "compile": "tsc --pretty", 13 | "watch": "tsc -w", 14 | "test": "jest", 15 | "test:watch": "jest --watch", 16 | "upgrade": "npm i cdktf@latest cdktf-cli@latest", 17 | "upgrade:next": "npm i cdktf@next cdktf-cli@next" 18 | }, 19 | "engines": { 20 | "node": ">=18.0" 21 | }, 22 | "dependencies": { 23 | "@cdktf/provider-aws": "^19.32.0", 24 | "cdktf": "^0.20.8", 25 | "constructs": "^10.3.0" 26 | }, 27 | "devDependencies": { 28 | "@types/jest": "^29.5.12", 29 | "@types/node": "^22.5.0", 30 | "jest": "^29.7.0", 31 | "ts-jest": "^29.2.5", 32 | "ts-node": "^10.9.2", 33 | "typescript": "^5.5.4", 34 | "@typescript-eslint/eslint-plugin": "^7.0.0", 35 | "@typescript-eslint/parser": "^7.0.0", 36 | "eslint": "^8.42.0", 37 | "eslint-config-prettier": "^9.0.0", 38 | "eslint-plugin-prettier": "^5.0.0", 39 | "prettier": "^3.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /infra/stacks/project.stack.ts: -------------------------------------------------------------------------------- 1 | import { DataAwsIamPolicyDocument } from '@cdktf/provider-aws/lib/data-aws-iam-policy-document'; 2 | import { DynamodbTable } from '@cdktf/provider-aws/lib/dynamodb-table'; 3 | import { ElasticBeanstalkApplication } from '@cdktf/provider-aws/lib/elastic-beanstalk-application'; 4 | import { ElasticBeanstalkEnvironment } from '@cdktf/provider-aws/lib/elastic-beanstalk-environment'; 5 | import { IamInstanceProfile } from '@cdktf/provider-aws/lib/iam-instance-profile'; 6 | import { IamPolicy } from '@cdktf/provider-aws/lib/iam-policy'; 7 | import { IamRole } from '@cdktf/provider-aws/lib/iam-role'; 8 | import { IamRolePolicyAttachment } from '@cdktf/provider-aws/lib/iam-role-policy-attachment'; 9 | import { IamUser } from '@cdktf/provider-aws/lib/iam-user'; 10 | import { IamUserPolicyAttachment } from '@cdktf/provider-aws/lib/iam-user-policy-attachment'; 11 | import { AwsProvider } from '@cdktf/provider-aws/lib/provider'; 12 | import { TerraformOutput, TerraformStack } from 'cdktf'; 13 | import { Construct } from 'constructs'; 14 | 15 | interface ProjectStackProps { 16 | environmentName: string; 17 | region: string; 18 | } 19 | 20 | export class ProjectStack extends TerraformStack { 21 | constructor(scope: Construct, id: string, props: ProjectStackProps) { 22 | super(scope, id); 23 | 24 | const { environmentName, region } = props; 25 | 26 | new AwsProvider(this, 'aws', { 27 | region, 28 | }); 29 | 30 | const dynamodbTable = new DynamodbTable(this, 'users_table', { 31 | name: `${environmentName}-users`, 32 | hashKey: 'id', 33 | attribute: [ 34 | { 35 | name: 'id', 36 | type: 'S', 37 | }, 38 | { 39 | name: 'email', 40 | type: 'S', 41 | }, 42 | ], 43 | billingMode: 'PAY_PER_REQUEST', 44 | globalSecondaryIndex: [ 45 | { 46 | name: 'email_index', 47 | hashKey: 'email', 48 | projectionType: 'ALL', 49 | }, 50 | ], 51 | }); 52 | 53 | /** 54 | * Application Permissions 55 | */ 56 | 57 | const trustedPolicyDocument = new DataAwsIamPolicyDocument( 58 | this, 59 | 'trusted_policy_document', 60 | { 61 | statement: [ 62 | { 63 | effect: 'Allow', 64 | actions: ['sts:AssumeRole'], 65 | principals: [ 66 | { 67 | type: 'Service', 68 | identifiers: ['ec2.amazonaws.com'], 69 | }, 70 | ], 71 | }, 72 | ], 73 | }, 74 | ); 75 | 76 | const permissionsPolicyDocument = new DataAwsIamPolicyDocument( 77 | this, 78 | 'permissions_policy_document', 79 | { 80 | statement: [ 81 | { 82 | effect: 'Allow', 83 | actions: ['dynamodb:*'], 84 | resources: [`${dynamodbTable.arn}*`], 85 | }, 86 | ], 87 | }, 88 | ); 89 | 90 | const iamPolicy = new IamPolicy(this, 'iam_policy', { 91 | name: `${environmentName}_application_policy`, 92 | policy: permissionsPolicyDocument.json, 93 | }); 94 | 95 | const iamRole = new IamRole(this, 'iam_role', { 96 | assumeRolePolicy: trustedPolicyDocument.json, 97 | name: `${environmentName}_application_role`, 98 | }); 99 | 100 | new IamRolePolicyAttachment(this, 'iam_role_policy_attachment', { 101 | policyArn: iamPolicy.arn, 102 | role: iamRole.name, 103 | }); 104 | 105 | const managedPolicies = [ 106 | 'arn:aws:iam::aws:policy/AdministratorAccess-AWSElasticBeanstalk', 107 | 'arn:aws:iam::aws:policy/AWSElasticBeanstalkMulticontainerDocker', 108 | 'arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier', 109 | 'arn:aws:iam::aws:policy/AWSElasticBeanstalkWorkerTier', 110 | 'arn:aws:iam::aws:policy/AmazonEC2FullAccess', 111 | ]; 112 | 113 | managedPolicies.forEach((policyArn, index) => { 114 | new IamRolePolicyAttachment(this, `role-policy-attachment-${index}`, { 115 | role: iamRole.name, 116 | policyArn: policyArn, 117 | }); 118 | }); 119 | 120 | /** 121 | * Application Elastic Beanstalk 122 | */ 123 | 124 | const application = new ElasticBeanstalkApplication(this, 'application', { 125 | description: 'api-rest-do-zero-a-aws-com-terraform', 126 | name: `${environmentName}-api-rest-do-zero-a-aws-com-terraform`, 127 | appversionLifecycle: { 128 | serviceRole: iamRole.arn, 129 | deleteSourceFromS3: true, 130 | }, 131 | }); 132 | 133 | const instanceProfile = new IamInstanceProfile(this, 'instance_profile', { 134 | name: 'aws-elasticbeanstalk-ec2-role', 135 | role: iamRole.name, 136 | }); 137 | 138 | const environment = new ElasticBeanstalkEnvironment(this, 'environment', { 139 | tier: 'WebServer', 140 | application: application.name, 141 | name: `${environmentName}-api-rest-do-zero-a-aws-com-terraform`, 142 | solutionStackName: '64bit Amazon Linux 2023 v6.2.0 running Node.js 20', 143 | tags: { 144 | Name: 'api-rest-do-zero-a-aws-com-terraform', 145 | Environment: environmentName, 146 | }, 147 | setting: [ 148 | { 149 | namespace: 'aws:autoscaling:launchconfiguration', 150 | name: 'IamInstanceProfile', 151 | value: instanceProfile.name, 152 | }, 153 | { 154 | namespace: 'aws:elasticbeanstalk:cloudwatch:logs', 155 | name: 'StreamLogs', 156 | value: 'true', 157 | }, 158 | { 159 | namespace: 'aws:elasticbeanstalk:cloudwatch:logs', 160 | name: 'RetentionInDays', 161 | value: '7', 162 | }, 163 | { 164 | namespace: 'aws:elasticbeanstalk:cloudwatch:logs', 165 | name: 'DeleteOnTerminate', 166 | value: 'false', 167 | }, 168 | { 169 | namespace: 'aws:elasticbeanstalk:application:environment', 170 | name: 'ENVIRONMENT', 171 | value: environmentName, 172 | }, 173 | { 174 | namespace: 'aws:elasticbeanstalk:application:environment', 175 | name: 'NO_COLOR', 176 | value: 'true', 177 | }, 178 | { 179 | namespace: 'aws:elasticbeanstalk:application:environment', 180 | name: 'AWS_REGION', 181 | value: region, 182 | }, 183 | ], 184 | }); 185 | 186 | new TerraformOutput(this, 'environment_url', { 187 | value: environment.endpointUrl, 188 | }); 189 | 190 | /** 191 | * Github Actions User and Permissions 192 | */ 193 | 194 | const githubActionsUser = new IamUser(this, 'github_actions_user', { 195 | name: `${environmentName}-github-actions`, 196 | }); 197 | 198 | const githubActionsPolicyDocument = new DataAwsIamPolicyDocument( 199 | this, 200 | 'github_actions_policy_document', 201 | { 202 | statement: [ 203 | { 204 | effect: 'Allow', 205 | actions: ['*'], 206 | resources: ['*'], 207 | }, 208 | ], 209 | }, 210 | ); 211 | 212 | const iamPolicyGithubActions = new IamPolicy( 213 | this, 214 | 'iam_policy_github_actions', 215 | { 216 | name: `${environmentName}_github_actions_policy`, 217 | policy: githubActionsPolicyDocument.json, 218 | }, 219 | ); 220 | 221 | new IamUserPolicyAttachment( 222 | this, 223 | 'iam_role_policy_attachment_github_actions', 224 | { 225 | policyArn: iamPolicyGithubActions.arn, 226 | user: githubActionsUser.name, 227 | }, 228 | ); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /infra/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "declaration": true, 5 | "experimentalDecorators": true, 6 | "inlineSourceMap": true, 7 | "inlineSources": true, 8 | "lib": [ 9 | "es2018" 10 | ], 11 | "module": "CommonJS", 12 | "noEmitOnError": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitAny": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "resolveJsonModule": true, 20 | "strict": true, 21 | "strictNullChecks": true, 22 | "strictPropertyInitialization": true, 23 | "stripInternal": true, 24 | "target": "ES2018", 25 | "incremental": true, 26 | "skipLibCheck": true 27 | }, 28 | "include": [ 29 | "**/*.ts" 30 | ], 31 | "exclude": [ 32 | "node_modules", 33 | "cdktf.out" 34 | ] 35 | } --------------------------------------------------------------------------------