├── .gitignore ├── .prettierrc ├── README.md ├── iam └── OrdersTableIAM.yml ├── nest-cli.json ├── nodemon-debug.json ├── nodemon.json ├── package-lock.json ├── package.json ├── resources └── OrdersTable.yml ├── serverless.yml ├── src ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── main.ts ├── modules │ └── order │ │ ├── dto │ │ └── createOrder.dto.ts │ │ ├── order.controller.ts │ │ ├── order.module.ts │ │ └── order.service.ts └── repositories │ └── order.repository.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.build/ 3 | /.serverless/ 4 | /_warmup/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 100, 4 | "editor.formatOnSave": true, 5 | "proseWrap": "always", 6 | "tabWidth": 4, 7 | "requireConfig": false, 8 | "useTabs": false, 9 | "trailingComma": "all", 10 | "bracketSpacing": true, 11 | "jsxBracketSameLine": false, 12 | "semi": true 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | # Nest application example 13 | 14 | This example demonstrates how to setup a [Nest](https://github.com/nestjs/nest) application. 15 | 16 | ## Use Cases 17 | 18 | - Setup & deploy a [Nest Application starter](https://github.com/nestjs/typescript-starter) 19 | 20 | ## Running the app locally 21 | 22 | ```bash 23 | npm start 24 | ``` 25 | 26 | Which should result in: 27 | 28 | ```bash 29 | $ sls offline start 30 | Serverless: Compiling with Typescript... 31 | Serverless: Using local tsconfig.json 32 | Serverless: Typescript compiled. 33 | Serverless: Watching typescript files... 34 | Serverless: Starting Offline: dev/us-east-1. 35 | 36 | Serverless: Routes for main: 37 | Serverless: ANY /{proxy*} 38 | 39 | Serverless: Offline listening on http://localhost:3000 40 | ``` 41 | 42 | Then browse http://localhost:3000/hello 43 | 44 | The logs should be : 45 | 46 | ```bash 47 | Serverless: ANY /hello (λ: main) 48 | [Nest] 7956 - 2018-12-13 10:34:22 [NestFactory] Starting Nest application... +6933ms 49 | [Nest] 7956 - 2018-12-13 10:34:22 [InstanceLoader] AppModule dependencies initialized +4ms 50 | [Nest] 7956 - 2018-12-13 10:34:22 [RoutesResolver] AppController {/}: +2ms 51 | [Nest] 7956 - 2018-12-13 10:34:22 [RouterExplorer] Mapped {/hello, GET} route +1ms 52 | [Nest] 7956 - 2018-12-13 10:34:22 [NestApplication] Nest application successfully started +1ms 53 | Serverless: [200] {"statusCode":200,"body":"Hello World!","headers":{"x-powered-by":"Express","content-type":"text/html; charset=utf-8","content-length":"12","etag":"W/\"c-Lve95gjOVATpfV8EL5X4nxwjKHE\"","date":"Thu, 13 Dec 2018 09:34:22 GMT","connection":"keep-alive"},"isBase64Encoded":false} 54 | ``` 55 | 56 | ### Skiping cache invalidation 57 | 58 | Skiping cache invalidation is the same behavior as a deployed function 59 | 60 | ```bash 61 | npm start -- --skipCacheInvalidation 62 | ``` 63 | 64 | ## Deploy 65 | 66 | In order to deploy the endpoint, simply run: 67 | 68 | ```bash 69 | sls deploy 70 | ``` 71 | 72 | The expected result should be similar to: 73 | 74 | ```bash 75 | $ sls deploy 76 | Serverless: Compiling with Typescript... 77 | Serverless: Using local tsconfig.json 78 | Serverless: Typescript compiled. 79 | Serverless: Packaging service... 80 | Serverless: Excluding development dependencies... 81 | Serverless: Creating Stack... 82 | Serverless: Checking Stack create progress... 83 | ..... 84 | Serverless: Stack create finished... 85 | Serverless: Uploading CloudFormation file to S3... 86 | Serverless: Uploading artifacts... 87 | Serverless: Uploading service .zip file to S3 (32.6 MB)... 88 | Serverless: Validating template... 89 | Serverless: Updating Stack... 90 | Serverless: Checking Stack update progress... 91 | .............................. 92 | Serverless: Stack update finished... 93 | Service Information 94 | service: serverless-nest-example 95 | stage: dev 96 | region: us-east-1 97 | stack: serverless-nest-example-dev 98 | api keys: 99 | None 100 | endpoints: 101 | ANY - https://XXXXXXX.execute-api.us-east-1.amazonaws.com/dev/{proxy?} 102 | functions: 103 | main: serverless-nest-example-dev-main 104 | layers: 105 | None 106 | ``` 107 | 108 | ## Usage 109 | 110 | Send an HTTP request directly to the endpoint using a tool like curl 111 | 112 | ```bash 113 | curl https://XXXXXXX.execute-api.us-east-1.amazonaws.com/dev/hello 114 | ``` 115 | 116 | ## Tail logs 117 | 118 | ```bash 119 | sls logs --function main --tail 120 | ``` 121 | 122 | Enable the plugin 123 | 124 | ```yaml 125 | plugins: 126 | - serverless-plugin-typescript 127 | - serverless-plugin-optimize 128 | - serverless-offline 129 | ``` 130 | -------------------------------------------------------------------------------- /iam/OrdersTableIAM.yml: -------------------------------------------------------------------------------- 1 | OrdersTableIAM: 2 | Effect: Allow 3 | Action: 4 | - dynamodb:PutItem 5 | - dynamodb:Scan 6 | - dynamodb:GetItem 7 | - dynamodb:UpdateItem 8 | - dynamodb:Query 9 | Resource: 10 | - ${self:custom.OrdersTable.arn} 11 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /nodemon-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "node --inspect-brk -r ts-node/register -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-serverless-dynamodb", 3 | "version": "0.0.0", 4 | "description": "serverless app", 5 | "author": "ouistiti-dev", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.build.json", 9 | "format": "prettier --write \"src/**/*.ts\"", 10 | "start": "sls offline start", 11 | "lint": "tslint -p tsconfig.json -c tslint.json", 12 | "test": "jest", 13 | "test:watch": "jest --watch", 14 | "test:cov": "jest --coverage", 15 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 16 | "test:e2e": "jest --config ./test/jest-e2e.json" 17 | }, 18 | "dependencies": { 19 | "@nestjs/common": "^7.3.1", 20 | "@nestjs/core": "^7.3.1", 21 | "@nestjs/jwt": "^7.0.0", 22 | "@nestjs/mongoose": "^7.0.1", 23 | "@nestjs/passport": "^7.1.0", 24 | "@nestjs/platform-express": "^7.3.2", 25 | "@nestjs/serverless-core": "^0.2.1", 26 | "@nestjs/swagger": "^4.5.12", 27 | "aws-sdk": "^2.711.0", 28 | "aws-serverless-express": "^3.3.5", 29 | "bcryptjs": "^2.4.3", 30 | "class-transformer": "^0.3.1", 31 | "class-validator": "^0.12.2", 32 | "dotenv": "^8.2.0", 33 | "express": "^4.17.1", 34 | "mongoose": "^5.9.22", 35 | "mongoose-unique-validator": "^2.0.3", 36 | "passport": "^0.4.1", 37 | "passport-jwt": "^4.0.0", 38 | "reflect-metadata": "^0.1.12", 39 | "rimraf": "^2.6.2", 40 | "rxjs": "^6.2.2", 41 | "swagger-ui-express": "^4.1.4", 42 | "uuid": "^8.1.0", 43 | "typescript": "^3.9.6" 44 | }, 45 | "devDependencies": { 46 | "@hewmen/serverless-plugin-typescript": "^1.1.17", 47 | "@nestjs/testing": "^7.3.1", 48 | "@types/aws-lambda": "^8.10.15", 49 | "@types/express": "^4.16.0", 50 | "@types/jest": "^23.3.1", 51 | "@types/node": "^10.7.1", 52 | "@types/supertest": "^2.0.5", 53 | "@types/mongoose": "^5.7.30", 54 | "jest": "^26.1.0", 55 | "nodemon": "^2.0.4", 56 | "prettier": "^2.0.5", 57 | "serverless-offline": "^5.12.1", 58 | "serverless-plugin-optimize": "^4.0.2-rc.1", 59 | "serverless-plugin-typescript": "^1.1.9", 60 | "supertest": "^4.0.2", 61 | "ts-jest": "^26.1.1", 62 | "ts-loader": "^8.0.0", 63 | "ts-node": "^8.10.2", 64 | "tsconfig-paths": "^3.5.0", 65 | "tslint": "^6.1.2" 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 | "coverageDirectory": "../coverage", 79 | "testEnvironment": "node" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /resources/OrdersTable.yml: -------------------------------------------------------------------------------- 1 | OrdersTable: 2 | Type: AWS::DynamoDB::Table 3 | Properties: 4 | TableName: OrdersTable-${self:provider.stage} 5 | BillingMode: PAY_PER_REQUEST 6 | AttributeDefinitions: 7 | - AttributeName: id 8 | AttributeType: S 9 | KeySchema: 10 | - AttributeName: id 11 | KeyType: HASH 12 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: aws-nestjs-dynamodb 3 | 4 | plugins: 5 | - serverless-plugin-typescript 6 | - serverless-plugin-optimize 7 | - serverless-offline 8 | 9 | provider: 10 | name: aws 11 | tracing: 12 | apiGateway: true 13 | runtime: nodejs12.x 14 | memorySize: 256 15 | stage: ${opt:stage, 'dev'} 16 | region: us-east-1 17 | environment: 18 | ORDERS_TABLE_NAME: ${self:custom.OrdersTable.name} 19 | iamRoleStatements: 20 | - ${file(iam/OrdersTableIAM.yml):OrdersTableIAM} 21 | 22 | package: 23 | individually: true 24 | 25 | resources: 26 | Resources: 27 | OrdersTable: ${file(resources/Orderstable.yml):OrdersTable} 28 | 29 | functions: 30 | main: 31 | handler: src/main.handler 32 | events: 33 | - http: 34 | method: any 35 | path: /{proxy+} 36 | custom: 37 | OrdersTable: 38 | name: !Ref OrdersTable 39 | arn: !GetAtt OrdersTable.Arn 40 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get('hello') 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { OrderModule } from './modules/order/order.module'; 5 | 6 | @Module({ 7 | imports: [OrderModule], 8 | controllers: [AppController], 9 | providers: [AppService], 10 | }) 11 | export class AppModule {} 12 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello MUNDO!!!!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Handler, Context } from 'aws-lambda'; 2 | import { Server } from 'http'; 3 | import { createServer, proxy } from 'aws-serverless-express'; 4 | import { eventContext } from 'aws-serverless-express/middleware'; 5 | import { ExpressAdapter } from '@nestjs/platform-express'; 6 | import { NestFactory } from '@nestjs/core'; 7 | import { AppModule } from './app.module'; 8 | 9 | const express = require('express'); 10 | 11 | // NOTE: If you get ERR_CONTENT_DECODING_FAILED in your browser, this is likely 12 | // due to a compressed response (e.g. gzip) which has not been handled correctly 13 | // by aws-serverless-express and/or API Gateway. Add the necessary MIME types to 14 | // binaryMimeTypes below 15 | const binaryMimeTypes: string[] = []; 16 | 17 | let cachedServer: Server; 18 | 19 | process.on('unhandledRejection', (reason) => { 20 | console.error(reason); 21 | }); 22 | 23 | process.on('uncaughtException', (reason) => { 24 | console.error(reason); 25 | }); 26 | 27 | async function bootstrapServer(): Promise { 28 | if (!cachedServer) { 29 | try { 30 | const expressApp = express(); 31 | const nestApp = await NestFactory.create(AppModule, new ExpressAdapter(expressApp)); 32 | nestApp.use(eventContext()); 33 | await nestApp.init(); 34 | cachedServer = createServer(expressApp, undefined, binaryMimeTypes); 35 | } catch (error) { 36 | return Promise.reject(error); 37 | } 38 | } 39 | return cachedServer; 40 | } 41 | 42 | export const handler: Handler = async (event: any, context: Context) => { 43 | cachedServer = await bootstrapServer(); 44 | return proxy(cachedServer, event, context, 'PROMISE').promise; 45 | }; 46 | -------------------------------------------------------------------------------- /src/modules/order/dto/createOrder.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | 3 | export class CreateOrderDto { 4 | @IsNotEmpty() 5 | title: string; 6 | 7 | @IsNotEmpty() 8 | category: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/order/order.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, Body, Res, HttpStatus, Get, Req, Param } from '@nestjs/common'; 2 | import { OrderService } from './order.service'; 3 | import { CreateOrderDto } from './dto/createOrder.dto'; 4 | 5 | @Controller('order') 6 | export class OrderController { 7 | constructor(private orderService: OrderService) {} 8 | 9 | @Post('/createOrder') 10 | async createOrder(@Body() createOrderDto: CreateOrderDto, @Res() res: any) { 11 | try { 12 | const newOrder: any = await this.orderService.createOrder(createOrderDto); 13 | if (newOrder.ok) { 14 | return res.status(HttpStatus.CREATED).json({ 15 | ok: true, 16 | data: newOrder.data, 17 | }); 18 | } else { 19 | return res.status(HttpStatus.BAD_REQUEST).json({ 20 | ok: false, 21 | message: 'Error Trying to Create Order', 22 | }); 23 | } 24 | } catch (error) { 25 | return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ 26 | ok: false, 27 | message: 'Error Trying to reach DB', 28 | errors: error, 29 | }); 30 | } 31 | } 32 | 33 | @Get('/getOrderById/:id') 34 | async getOrderById(@Param('id') id: string, @Res() res: any) { 35 | try { 36 | const order: any = await this.orderService.getOrderById(id); 37 | if (order.ok) { 38 | return res.status(HttpStatus.OK).json({ 39 | ok: true, 40 | order: order.data, 41 | }); 42 | } else { 43 | return res.status(HttpStatus.BAD_REQUEST).json({ 44 | ok: false, 45 | message: 'Error Trying to Get Order', 46 | }); 47 | } 48 | } catch (error) { 49 | return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ 50 | ok: false, 51 | message: 'Error Trying to reach DB', 52 | errors: error, 53 | }); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/modules/order/order.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { OrderController } from './order.controller'; 3 | import { OrderService } from './order.service'; 4 | import { OrderRepository } from '../../repositories/order.repository'; 5 | 6 | @Module({ 7 | imports: [], 8 | controllers: [OrderController], 9 | providers: [OrderService, OrderRepository], 10 | }) 11 | export class OrderModule {} 12 | -------------------------------------------------------------------------------- /src/modules/order/order.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { CreateOrderDto } from './dto/createOrder.dto'; 3 | import { OrderRepository } from '../../repositories/order.repository'; 4 | 5 | @Injectable() 6 | export class OrderService { 7 | constructor(private orderRepository: OrderRepository) {} 8 | 9 | async createOrder(createOrderDto: CreateOrderDto) { 10 | const createdOffer = await this.orderRepository.createOrder(createOrderDto); 11 | return createdOffer; 12 | } 13 | 14 | async getOrderById(id) { 15 | const Order = await this.orderRepository.getOrderById(id); 16 | return Order; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/repositories/order.repository.ts: -------------------------------------------------------------------------------- 1 | import * as AWS from 'aws-sdk'; 2 | import { CreateOrderDto } from '../modules/order/dto/createOrder.dto'; 3 | import { InternalServerErrorException, NotFoundException } from '@nestjs/common'; 4 | import { v4 as uuid } from 'uuid'; 5 | 6 | export class OrderRepository { 7 | constructor() {} 8 | 9 | async createOrder(createOrderDto: CreateOrderDto) { 10 | const newOrder = { 11 | id: uuid(), 12 | title: createOrderDto.title, 13 | category: createOrderDto.category, 14 | }; 15 | 16 | try { 17 | await new AWS.DynamoDB.DocumentClient() 18 | .put({ 19 | TableName: process.env.ORDERS_TABLE_NAME, 20 | Item: newOrder, 21 | }) 22 | .promise(); 23 | } catch (error) { 24 | throw new InternalServerErrorException(error); 25 | } 26 | 27 | return { ok: true, data: newOrder }; 28 | } 29 | 30 | async getOrderById(id) { 31 | let order; 32 | try { 33 | const result = await new AWS.DynamoDB.DocumentClient() 34 | .get({ 35 | TableName: process.env.ORDERS_TABLE_NAME, 36 | Key: { id }, 37 | }) 38 | .promise(); 39 | 40 | order = result.Item; 41 | } catch (error) { 42 | throw new InternalServerErrorException(error); 43 | } 44 | 45 | if (!order) { 46 | throw new NotFoundException(`Order with ID "${id}" not found`); 47 | } 48 | 49 | return { ok: true, data: order }; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeAll(async () => { 10 | const moduleFixture = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/hello (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/hello') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["node_modules", "**/*.spec.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "allowSyntheticDefaultImports": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "baseUrl": "./" 15 | }, 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 150], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false 16 | }, 17 | "rulesDirectory": [] 18 | } 19 | --------------------------------------------------------------------------------