├── .gitignore ├── 01-stripe-events ├── source-code │ ├── .eslintrc.js │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ └── charge-processor │ │ │ └── lambda.ts │ ├── template.yaml │ ├── tsconfig.json │ ├── webpack.config.js │ └── yarn.lock └── video-source │ ├── architecture.png │ ├── done.mov │ ├── episode.png │ ├── folder-structure.png │ ├── intro.png │ ├── output.png │ ├── outro.png │ ├── sar.jpg │ ├── source.md │ ├── stripe-setup.png │ └── stripe.png ├── 02-testing-serverless-apps └── video-source │ ├── 01-alex.jpeg │ ├── 02-meet-alex.jpeg │ ├── 03-friends.jpeg │ ├── 04-friends.jpeg │ ├── 05-friends.jpeg │ ├── 06-project.jpeg │ ├── 07-project.jpeg │ ├── 08-project.jpeg │ ├── 09-team.jpeg │ ├── 10-process.jpeg │ ├── 11-process.jpeg │ ├── 12-tools.jpeg │ ├── 13-what-to-test.jpeg │ ├── 14-testing-pyramid.jpeg │ ├── 15-testing-pyramid.jpeg │ ├── 16-what-to-test.jpeg │ ├── 17-risks.jpeg │ ├── 18-architecture.jpeg │ ├── 19-architecture.jpeg │ ├── 20-next-episode.png │ ├── episode.png │ ├── intro.png │ ├── outro.png │ └── source.md ├── 03-testing-serverless-apps-part-2 ├── source-code │ ├── .eslintrc.js │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── common │ │ │ ├── booksdb-repository.ts │ │ │ └── tests │ │ │ │ └── booksdb-repository.test.ts │ │ └── get-books │ │ │ ├── events │ │ │ └── event.json │ │ │ ├── lambda.ts │ │ │ ├── lib │ │ │ └── main.ts │ │ │ └── tests │ │ │ └── main.test.ts │ ├── template.yaml │ ├── tsconfig.json │ ├── webpack.config.js │ └── yarn.lock └── video-source │ ├── 01-risks.jpeg │ ├── 02-architecture.jpeg │ ├── 03-architecture.jpeg │ ├── 04-architecture.jpeg │ ├── 05-architecture.jpeg │ ├── 06-testing-pyramid.jpeg │ ├── 07-ui-tests.jpeg │ ├── 08-code.jpeg │ ├── 09-folder-structure.png │ ├── 10-diagram.png │ ├── 11-sam-deploy.jpeg │ ├── 12-testing.jpeg │ ├── episode.png │ ├── intro.png │ ├── outro.png │ └── source.md ├── 04-api-gateway-cors ├── source-code │ ├── .eslintrc.js │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ └── hello-world │ │ │ └── lambda.ts │ ├── template.yaml │ ├── tsconfig.json │ ├── webpack.config.js │ └── yarn.lock └── video-source │ ├── 01-whats-cors.png │ ├── 02-cors.png │ ├── 03-cors-request.png │ ├── 04-cors-error.png │ ├── 05-cors-simple-request.png │ ├── 06-cors-preflight.png │ ├── 07-cors-request.png │ ├── 08-aws-cors.png │ ├── 09-folder-structure.png │ ├── 10-sam-deploy.png │ ├── 11-browser.png │ ├── 12-browser.png │ ├── 13-browser.png │ ├── 14-browser.png │ ├── 15-browser.png │ ├── 16-browser.png │ ├── episode.png │ ├── intro.png │ ├── outro.png │ └── source.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build 3 | samconfig.toml 4 | node_modules 5 | info 6 | raw-assets 7 | *.sdxml -------------------------------------------------------------------------------- /01-stripe-events/source-code/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "project": "tsconfig.json", 14 | "sourceType": "module" 15 | }, 16 | "plugins": [ 17 | "@typescript-eslint", 18 | "@typescript-eslint/tslint" 19 | ], 20 | "rules": { 21 | "@typescript-eslint/array-type": "error", 22 | "@typescript-eslint/ban-types": "off", 23 | "@typescript-eslint/indent": [ 24 | "error", 25 | 2 26 | ], 27 | "@typescript-eslint/interface-name-prefix": "off", 28 | "@typescript-eslint/member-delimiter-style": [ 29 | "error", 30 | { 31 | "multiline": { 32 | "delimiter": "none", 33 | "requireLast": true 34 | }, 35 | "singleline": { 36 | "delimiter": "semi", 37 | "requireLast": false 38 | } 39 | } 40 | ], 41 | "@typescript-eslint/no-explicit-any": "off", 42 | "@typescript-eslint/no-parameter-properties": "off", 43 | "@typescript-eslint/no-use-before-define": "off", 44 | "@typescript-eslint/prefer-for-of": "error", 45 | "@typescript-eslint/prefer-function-type": "error", 46 | "@typescript-eslint/quotes": [ 47 | "error", 48 | "single", 49 | { 50 | "avoidEscape": true 51 | } 52 | ], 53 | "@typescript-eslint/semi": [ 54 | "error", 55 | "never" 56 | ], 57 | "@typescript-eslint/unified-signatures": "error", 58 | "camelcase": "error", 59 | "comma-dangle": [ 60 | "error", 61 | { 62 | "objects": "always-multiline", 63 | "arrays": "always-multiline", 64 | "functions": "never" 65 | } 66 | ], 67 | "complexity": "off", 68 | "constructor-super": "error", 69 | "dot-notation": "error", 70 | "eqeqeq": [ 71 | "error", 72 | "smart" 73 | ], 74 | "guard-for-in": "error", 75 | "id-blacklist": [ 76 | "error", 77 | "any", 78 | "Number", 79 | "number", 80 | "String", 81 | "string", 82 | "Boolean", 83 | "boolean", 84 | "Undefined", 85 | "undefined" 86 | ], 87 | "id-match": "error", 88 | "max-classes-per-file": "off", 89 | "max-len": [ 90 | "error", 91 | { 92 | "code": 180 93 | } 94 | ], 95 | "new-parens": "error", 96 | "no-bitwise": "error", 97 | "no-caller": "error", 98 | "no-cond-assign": "error", 99 | "no-console": "off", 100 | "no-debugger": "error", 101 | "no-empty": "error", 102 | "no-eval": "error", 103 | "no-fallthrough": "off", 104 | "no-invalid-this": "off", 105 | "no-new-wrappers": "error", 106 | "no-shadow": [ 107 | "error", 108 | { 109 | "hoist": "all" 110 | } 111 | ], 112 | "no-throw-literal": "error", 113 | "no-trailing-spaces": "error", 114 | "no-undef-init": "error", 115 | "no-underscore-dangle": "error", 116 | "no-unsafe-finally": "error", 117 | "no-unused-expressions": "error", 118 | "no-unused-labels": "error", 119 | "object-shorthand": "error", 120 | "one-var": [ 121 | "off", 122 | "never" 123 | ], 124 | "radix": "error", 125 | "spaced-comment": "error", 126 | "use-isnan": "error", 127 | "valid-typeof": "off", 128 | "@typescript-eslint/tslint/config": [ 129 | "error", 130 | { 131 | "rules": { 132 | "jsdoc-format": true, 133 | "no-reference-import": true 134 | } 135 | } 136 | ] 137 | } 138 | }; 139 | -------------------------------------------------------------------------------- /01-stripe-events/source-code/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | samconfig.toml 3 | node_modules 4 | info 5 | -------------------------------------------------------------------------------- /01-stripe-events/source-code/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2020 Gojko Adzic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /01-stripe-events/source-code/README.md: -------------------------------------------------------------------------------- 1 | # AWS Serverless Application Model TypeScript template 2 | 3 | A simple AWS SAM template with TypeScript. 4 | 5 | ## Folder structure 6 | 7 | This project has the following folder structure: 8 | 9 | ```bash 10 | . 11 | ├── README.md # This file 12 | ├── build # Build folder 13 | │   └── hello-world # Each function has its own folder 14 | │   ├── lambda.js 15 | │   └── lambda.js.map # And each function has sourcemaps 16 | ├── jest.config.js # Jest configuration 17 | ├── package.json 18 | ├── src # Source code for all functions 19 | │   └── hello-world # Sample function 20 | │   ├── events # Sample event for local invocationn 21 | │   │ └── event.json 22 | │   ├── lambda.ts # Main file 23 | │   ├── lib # Rest of code, including function business logic 24 | │   │ └── main.ts 25 | │   └── tests # Tests for business logic and all important files 26 | │   └── main.test.ts 27 | ├── template.yaml # Main CloudFormation file 28 | ├── webpack.config.js # Webpack config 29 | ├── yarn.lock 30 | └── .eslintrc.js # ESLint config 31 | ``` 32 | 33 | ## Usage 34 | 35 | To use this template, make sure you have the following prerequisites: 36 | 37 | - AWS profile 38 | - AWS SAM installed and configured 39 | - Node.js version 8 or more (version 12 is recommended) 40 | 41 | ### Initialize a new project 42 | 43 | To create a new project using this template, create a new folder, navigate to your new folder in your terminal, and run the following command: 44 | 45 | ```bash 46 | sam init --location gh:serverlesspub/sam-ts 47 | ``` 48 | 49 | This will create a new AWS SAM project with the folder structure explained above. 50 | 51 | ### Build TypeScript 52 | 53 | To build TypeScript, run the following command: 54 | 55 | ```bash 56 | npm run build 57 | ``` 58 | 59 | If you want to build a project and run the webpack bundle analyzer, run the following command: 60 | 61 | ```bash 62 | npm run build-analyze 63 | ``` 64 | 65 | ### Deploy 66 | 67 | To deploy the project, run the following command: 68 | 69 | ```bash 70 | sam deploy --guided 71 | ``` 72 | 73 | This will run an interactive deployment process, save your configuration to the `samconfig.toml` file, and deploy the project to your AWS account. 74 | 75 | _NOTE: The `samconfig.toml` file is on git ignore list._ 76 | 77 | ### Run automated tests 78 | 79 | To run Jest tests, use the following command: 80 | 81 | ```bash 82 | npm run test 83 | ``` 84 | 85 | This command will run ESLint, and if there are no linting issues, it'll run Jest tests. 86 | 87 | If you want to run ESLint without tests, use the following command: 88 | 89 | ```bash 90 | npm run lint 91 | ``` 92 | 93 | ### Test and debug using SAM Local 94 | 95 | TBA 96 | 97 | ## License 98 | 99 | MIT, see [LICENSE](LICENSE). -------------------------------------------------------------------------------- /01-stripe-events/source-code/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [ 3 | "./src" 4 | ], 5 | transform: { 6 | "^.+\\.tsx?$": "ts-jest" 7 | } 8 | } -------------------------------------------------------------------------------- /01-stripe-events/source-code/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-sam-ts-template", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "watch": "NODE_ENV=dev webpack --watch --mode=development", 8 | "build": "NODE_ENV=prod BUNDLE_ANALYZER=false webpack --mode=production --progress", 9 | "build-analyze": "NODE_ENV=prod BUNDLE_ANALYZER=true webpack --mode=production --progress", 10 | "lint": "eslint -c .eslintrc.js --ext ts src", 11 | "pretest": "npm run lint", 12 | "test": "jest", 13 | "coverage": "jest --coverage" 14 | }, 15 | "keywords": [], 16 | "author": "Slobodan Stojanović (http://slobodan.me)", 17 | "license": "MIT", 18 | "dependencies": { 19 | "@types/aws-lambda": "^8.10.46", 20 | "@types/node": "^13.9.1", 21 | "aws-lambda": "^1.0.5", 22 | "aws-sdk": "^2.639.0", 23 | "aws-xray-sdk": "^2.5.0", 24 | "source-map-support": "^0.5.16", 25 | "tslint": "^6.1.0" 26 | }, 27 | "devDependencies": { 28 | "@types/jest": "^25.1.4", 29 | "@typescript-eslint/eslint-plugin": "^2.23.0", 30 | "@typescript-eslint/eslint-plugin-tslint": "^2.23.0", 31 | "@typescript-eslint/parser": "^2.23.0", 32 | "eslint": "^6.8.0", 33 | "eslint-plugin-prefer-arrow": "^1.1.7", 34 | "eslint-plugin-prefer-arrow-functions": "^3.0.1", 35 | "jest": "^25.1.0", 36 | "source-map-loader": "^0.2.4", 37 | "speed-measure-webpack-plugin": "^1.3.1", 38 | "ts-jest": "^25.2.1", 39 | "ts-loader": "^6.2.1", 40 | "ts-node": "^8.6.2", 41 | "typescript": "^3.8.3", 42 | "webpack": "^4.42.0", 43 | "webpack-bundle-analyzer": "^3.6.1", 44 | "webpack-cli": "^3.3.11" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /01-stripe-events/source-code/src/charge-processor/lambda.ts: -------------------------------------------------------------------------------- 1 | // Allow CloudWatch to read source maps 2 | import 'source-map-support/register' 3 | 4 | // Import event types from @types/aws-lambda 5 | import { ScheduledEvent } from 'aws-lambda' 6 | 7 | // Export handler function 8 | export async function handler(event: ScheduledEvent): Promise { 9 | // Log the event 10 | console.log(event) 11 | return true 12 | } -------------------------------------------------------------------------------- /01-stripe-events/source-code/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Vacation Tracker API 4 | 5 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 6 | Globals: 7 | Function: 8 | Runtime: nodejs12.x 9 | Tracing: Active 10 | 11 | Resources: 12 | PaymentEventBus: 13 | Type: AWS::Events::EventBus 14 | Properties: 15 | Name: StripeEventBus 16 | 17 | StripeWebhook: 18 | Type: AWS::Serverless::Application 19 | Properties: 20 | Location: 21 | ApplicationId: arn:aws:serverlessrepo:us-east-1:721177882564:applications/generic-webhook-to-eventbridge 22 | SemanticVersion: 1.3.4 23 | Parameters: 24 | EventBusName: !Ref PaymentEventBus 25 | EventSource: stripe-events 26 | 27 | ChargeSucceededProcessor: 28 | Type: AWS::Serverless::Function 29 | Properties: 30 | CodeUri: build/charge-processor 31 | Handler: lambda.handler 32 | Events: 33 | OnChargeSucceeded: 34 | Type: CloudWatchEvent 35 | Properties: 36 | EventBusName: !Ref PaymentEventBus 37 | Pattern: 38 | detail: 39 | body: 40 | type: 41 | - charge.succeeded 42 | 43 | Outputs: 44 | StripeWebhookUrl: 45 | Description: Webhook URL 46 | Value: !GetAtt StripeWebhook.Outputs.WebhookApiUrl 47 | 48 | ChargeSucceededProcessor: 49 | Description: Function logical ID 50 | Value: !Ref ChargeSucceededProcessor -------------------------------------------------------------------------------- /01-stripe-events/source-code/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*.ts" 4 | ], 5 | "compilerOptions": { 6 | "incremental": true, 7 | "allowJs": true, 8 | "target": "es2017", 9 | "module": "commonjs", 10 | "strict": true, 11 | "baseUrl": "./", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "types": [ 16 | "node", 17 | "jest" 18 | ], 19 | "esModuleInterop": true, 20 | "inlineSourceMap": true, 21 | "resolveJsonModule": true 22 | } 23 | } -------------------------------------------------------------------------------- /01-stripe-events/source-code/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const glob = require('glob') 3 | const webpack = require('webpack') 4 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 5 | const SpeedMeasurePlugin = require('speed-measure-webpack-plugin') 6 | const smp = new SpeedMeasurePlugin() 7 | 8 | // Credits: https://hackernoon.com/webpack-creating-dynamically-named-outputs-for-wildcarded-entry-files-9241f596b065 9 | const entryArray = glob.sync('./src/**/lambda.ts') 10 | const entryObject = entryArray.reduce((acc, item) => { 11 | let name = path.dirname(item.replace('./src/', '')) 12 | // conforms with Webpack entry API 13 | // Example: { test: './src/test/lambda.ts' } 14 | acc[name] = item 15 | return acc 16 | }, {}) 17 | 18 | const config = smp.wrap({ 19 | entry: entryObject, 20 | devtool: 'source-map', 21 | target: 'node', 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.tsx?$/, 26 | use: [ 27 | { 28 | loader: 'ts-loader', 29 | options: { 30 | transpileOnly: true, 31 | experimentalWatchApi: true 32 | } 33 | } 34 | ], 35 | exclude: /node_modules/ 36 | } 37 | ] 38 | }, 39 | optimization: { 40 | minimize: false 41 | }, 42 | plugins: [new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)], 43 | resolve: { 44 | extensions: ['.ts', '.js'], 45 | symlinks: false, 46 | cacheWithContext: false 47 | }, 48 | // Output directive will generate build//lambda.js 49 | output: { 50 | filename: '[name]/lambda.js', 51 | path: path.resolve(__dirname, 'build'), 52 | devtoolModuleFilenameTemplate: '[absolute-resource-path]', 53 | // credits to Rich Buggy!!! 54 | libraryTarget: 'commonjs2' 55 | } 56 | }) 57 | 58 | if (process.env.BUNDLE_ANALYZER && process.env.BUNDLE_ANALYZER === 'true') { 59 | config.plugins.push(new BundleAnalyzerPlugin({ 60 | analyzerMode: 'static', 61 | reportFilename: '../info/bundle-report.html', 62 | openAnalyzer: false 63 | })) 64 | } 65 | 66 | module.exports = config 67 | -------------------------------------------------------------------------------- /01-stripe-events/video-source/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/01-stripe-events/video-source/architecture.png -------------------------------------------------------------------------------- /01-stripe-events/video-source/done.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/01-stripe-events/video-source/done.mov -------------------------------------------------------------------------------- /01-stripe-events/video-source/episode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/01-stripe-events/video-source/episode.png -------------------------------------------------------------------------------- /01-stripe-events/video-source/folder-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/01-stripe-events/video-source/folder-structure.png -------------------------------------------------------------------------------- /01-stripe-events/video-source/intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/01-stripe-events/video-source/intro.png -------------------------------------------------------------------------------- /01-stripe-events/video-source/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/01-stripe-events/video-source/output.png -------------------------------------------------------------------------------- /01-stripe-events/video-source/outro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/01-stripe-events/video-source/outro.png -------------------------------------------------------------------------------- /01-stripe-events/video-source/sar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/01-stripe-events/video-source/sar.jpg -------------------------------------------------------------------------------- /01-stripe-events/video-source/source.md: -------------------------------------------------------------------------------- 1 | --- 2 | size: 720p 3 | subtitles: auto 4 | background: 5 | vendor: uplifting-2 6 | volume: 0.5 7 | --- 8 | 9 | ![](intro.png) 10 | 11 | Welcome to Five Minutes Serverless! 12 | 13 | --- 14 | 15 | ![](episode.png) 16 | 17 | In this episode, we will build a Stripe events processor using AWS SAM, EventBridge, and TypeScript. 18 | 19 | --- 20 | 21 | ![](stripe.png) 22 | 23 | Stripe is one of the most popular payment processors. Many of us use it in our applications. 24 | 25 | (pause: 1) 26 | 27 | Stripe has an excellent API, and it can send us events using webhooks. 28 | 29 | For example, it can notify us when it generates a new invoice or process a payment. 30 | 31 | --- 32 | 33 | ![](architecture.png) 34 | 35 | In this video, we'll build a serverless application that receives webhook events from Stripe and sends them to the EventBridge event bus for further processing. 36 | 37 | --- 38 | 39 | (font-size: 42) 40 | 41 | ```md 42 | **Prerequisites:** 43 | 44 | - An active AWS account 45 | - AWS SAM installed 46 | - Node.js version 8+ (version 12 is recommended) 47 | - Stripe dev account 48 | ``` 49 | 50 | Before we begin, make sure you have the following prerequisites. 51 | 52 | (duration: 5) 53 | 54 | --- 55 | 56 | ```bash 57 | mkdir stripe-events 58 | cd stripe-events 59 | ``` 60 | 61 | Let's start by creating a new folder. Feel free to pick the name you like. We'll call it "stripe-events." 62 | 63 | (pause: 1) 64 | 65 | Then navigate to your new folder using your terminal. 66 | 67 | --- 68 | 69 | (font-size: 40) 70 | 71 | ```bash 72 | sam init --location gh:serverlesspub/sam-ts 73 | ``` 74 | 75 | You can initialize an AWS SAM project by running the following command from your new folder using your terminal. 76 | 77 | --- 78 | 79 | ![](folder-structure.png) 80 | 81 | This command will create a new AWS SAM project with a Hello World function written in TypeScript. 82 | 83 | --- 84 | 85 | ```bash 86 | npm install 87 | npm run build 88 | sam deploy --guided 89 | ``` 90 | 91 | Let's try to build and deploy this project to confirm that everything works as expected. 92 | 93 | (pause: 1) 94 | 95 | To build a project, run the "npm run build" command from your project folder in your terminal. 96 | 97 | (pause: 1) 98 | 99 | Then run the "aws sam --guided" command to deploy the SAM project to your AWS account. 100 | 101 | --- 102 | 103 | (font-size: 42) 104 | 105 | ```bash 106 | version = 0.1 107 | [default] 108 | [default.deploy] 109 | [default.deploy.parameters] 110 | stack_name = "stripe-events" 111 | s3_bucket = "aws-sam-cli-managed-..." 112 | s3_prefix = "stripe-events" 113 | region = "us-east-1" 114 | capabilities = "CAPABILITY_IAM" 115 | ``` 116 | 117 | SAM will guide you through the deployment process and create the "samconfig.toml" file with your deployment configuration. 118 | 119 | (pause: 1) 120 | 121 | The SAM config file will look similar to the file you see right now. 122 | 123 | --- 124 | 125 | (font-size: 36) 126 | 127 | ```bash 128 | capabilities = "CAPABILITY_IAM CAPABILITY_AUTO_EXPAND" 129 | ``` 130 | 131 | As we'll use Serverless Application Repository in a minute, let's edit the SAM config file. 132 | 133 | (pause: 1) 134 | 135 | Add CAPABILITY _ AUTO _ EXPAND to the list of capabilities, to allow SAM to deploy SAR apps. 136 | 137 | --- 138 | 139 | ![](architecture.png) 140 | 141 | Now that everything is ready, let's build our application. 142 | 143 | --- 144 | 145 | (callout: 146 | cx: 838 147 | cy: 346 148 | size: 100) 149 | 150 | ![](architecture.png) 151 | 152 | (transition: crossfade 1) 153 | 154 | First, create an EventBridge event bus. 155 | 156 | --- 157 | 158 | ```yaml 159 | PaymentEventBus: 160 | Type: AWS::Events::EventBus 161 | Properties: 162 | Name: StripeEventBus 163 | ``` 164 | 165 | To do so, add the following code to the Resources section of your template.yaml file. 166 | 167 | --- 168 | 169 | ![](architecture.png) 170 | 171 | --- 172 | 173 | (callout: 174 | type: rectangle 175 | left: 310 176 | top: 225 177 | right: 723 178 | bottom: 468) 179 | 180 | ![](architecture.png) 181 | 182 | (transition: crossfade 1) 183 | 184 | Then, add the generic webhook to EventBridge application from Serverless Application Repository. 185 | 186 | --- 187 | 188 | ![](sar.jpg) 189 | 190 | You can find the Generic webhook to EventBridge application on Serverless Application Repository. 191 | 192 | --- 193 | 194 | (font-size: 36) 195 | 196 | ```yaml 197 | StripeWebhook: 198 | Type: AWS::Serverless::Application 199 | Properties: 200 | Location: 201 | ApplicationId: arn:aws:serverlessrepo: 202 | us-east-1:721177882564:applications/ 203 | generic-webhook-to-eventbridge 204 | SemanticVersion: 1.3.4 205 | Parameters: 206 | EventBusName: !Ref PaymentEventBus 207 | EventSource: stripe-events 208 | ``` 209 | 210 | You can also add SAR app by adding the following code to the Resources section of your template file. 211 | 212 | --- 213 | 214 | ![](architecture.png) 215 | 216 | --- 217 | 218 | (callout: 219 | cx: 1050 220 | cy: 346 221 | size: 100) 222 | 223 | ![](architecture.png) 224 | 225 | (transition: crossfade 1) 226 | 227 | Finally, create a Lambda function to handle Stripe event. 228 | 229 | --- 230 | 231 | (font-size: 32) 232 | 233 | ```yaml 234 | ChargeSucceededProcessor: 235 | Type: AWS::Serverless::Function 236 | Properties: 237 | CodeUri: build/charge-processor 238 | Handler: lambda.handler 239 | Events: 240 | OnChargeSucceeded: 241 | Type: CloudWatchEvent 242 | Properties: 243 | EventBusName: !Ref PaymentEventBus 244 | Pattern: 245 | detail: 246 | body: 247 | type: 248 | - charge.succeeded 249 | ``` 250 | 251 | Add the following code to the Resources section of your template file. 252 | 253 | --- 254 | 255 | ```yaml 256 | OnChargeSucceeded: 257 | Type: CloudWatchEvent 258 | Properties: 259 | EventBusName: !Ref PaymentEventBus 260 | Pattern: 261 | detail: 262 | body: 263 | type: 264 | - charge.succeeded 265 | ``` 266 | 267 | The most interesting part of our function is the event. 268 | 269 | (pause: 1) 270 | 271 | This tells EventBridge to run our function only when it receives the event with charge succeeded type in event body. 272 | 273 | (pause: 1) 274 | 275 | We are also using the CloudWatchEvent as a trigger type because CloudWatch events are now part of EventBridge, and SAM does not have a separate EventBrige trigger type at the moment. 276 | 277 | --- 278 | 279 | (font-size: 32) 280 | 281 | ```yaml 282 | Outputs: 283 | StripeWebhookUrl: 284 | Description: Webhook URL 285 | Value: !GetAtt StripeWebhook.Outputs.WebhookApiUrl 286 | 287 | ChargeSucceededProcessor: 288 | Description: Function logical ID 289 | Value: !Ref ChargeSucceededProcessor 290 | ``` 291 | 292 | At the bottom of your template file, add the following snippet to export the webhook URL and function logical ID. 293 | 294 | (pause: 1) 295 | 296 | We need the webhook URL for Stripe, and function logical ID to be able to read logs using SAM. 297 | 298 | --- 299 | 300 | (font-size: 26) 301 | 302 | ```typescript 303 | // Allow CloudWatch to read source maps 304 | import 'source-map-support/register' 305 | 306 | // Import event types from @types/aws-lambda 307 | import { ScheduledEvent } from 'aws-lambda' 308 | 309 | // Export handler function 310 | export async function handler(event: ScheduledEvent): Promise { 311 | // Log the event 312 | console.log(event) 313 | return true 314 | } 315 | ``` 316 | 317 | The next step is to create a Lambda function code. 318 | 319 | Create the "charge-processor" folder in the source folder, and create the "lambda.ts" file inside with the following content. 320 | 321 | (pause: 1) 322 | 323 | Let's analyze it line by line. 324 | 325 | --- 326 | 327 | (font-size: 32) 328 | 329 | ```typescript 330 | // Allow CloudWatch to read source maps 331 | import 'source-map-support/register' 332 | ``` 333 | 334 | At the top of the lambda.ts file, add source map support. Without this line, CloudWatch would not be able to show human-readable error stack traces. 335 | 336 | --- 337 | 338 | (font-size: 32) 339 | 340 | ```typescript 341 | // Import event types from @types/aws-lambda 342 | import { ScheduledEvent } from 'aws-lambda' 343 | ``` 344 | 345 | Then import the type from the "aws-lambda" module. 346 | 347 | We use the "ScheduledEvent" because that's the trigger type for the CloudWatch Events and EventBridge triggers. 348 | 349 | --- 350 | 351 | (font-size: 23) 352 | 353 | ```typescript 354 | // Export handler function 355 | export async function handler(event: ScheduledEvent): Promise { 356 | // Log the event 357 | console.log(event) 358 | return true 359 | } 360 | ``` 361 | 362 | Finally, export the handler function that will log the event. 363 | 364 | --- 365 | 366 | ```bash 367 | npm run build 368 | sam deploy 369 | ``` 370 | 371 | Let's build and deploy the project again. You do not need a guided flag anymore because you have a SAM config file. 372 | 373 | --- 374 | 375 | ![](output.png) 376 | 377 | When the deployment finishes, SAM will output the URL of the Stripe webhook. 378 | 379 | (duration: 4) 380 | 381 | --- 382 | 383 | ![](stripe-setup.png) 384 | 385 | To test our solution, go to Stripe and add a new webhook. Use the URL from SAM deployment output, subscribe the webhook to the charge succeeded event, and send a test event. 386 | 387 | --- 388 | 389 | (font-size: 42) 390 | 391 | ```bash 392 | sam logs --name LOGICAL_ID --region REGION 393 | ``` 394 | 395 | Confirm that your function works by running the following command from your terminal. 396 | 397 | (pause: 1) 398 | 399 | Make sure you replace LOGICAL_ID with your function logical ID, and REGION with AWS region you use. 400 | 401 | (pause: 1) 402 | 403 | This command will print the error log from CloudWatch log stream to your terminal. 404 | 405 | --- 406 | 407 | ![](done.mov) 408 | 409 | That's it! 410 | 411 | (pause: 1) 412 | 413 | We built a simple Stripe webhook handler. It does not have a complex business logic, however it has a scalable structure that allows you to build the logic you need. 414 | 415 | --- 416 | 417 | (font-size: 32) 418 | 419 | ```md 420 | **Credits:** 421 | 422 | - I built this video using Video Puppet 423 | - I made diagrams using SimpleDiagrams 4 424 | 425 | For source code, visit the following repository: 426 | 427 | [https://github.com/serverlesspub/five-minutes-serverless](https://github.com/serverlesspub/five-minutes-serverless) 428 | 429 | Video by @slobodan_ 430 | ``` 431 | 432 | Thanks for watching the first episode of Five Minutes Serverless! 433 | 434 | (pause: 1) 435 | 436 | Feel free to send me feedback or questions on twitter. 437 | 438 | (duration: 8) 439 | 440 | --- 441 | 442 | ![](outro.png) 443 | 444 | (duration: 5) -------------------------------------------------------------------------------- /01-stripe-events/video-source/stripe-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/01-stripe-events/video-source/stripe-setup.png -------------------------------------------------------------------------------- /01-stripe-events/video-source/stripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/01-stripe-events/video-source/stripe.png -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/01-alex.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/01-alex.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/02-meet-alex.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/02-meet-alex.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/03-friends.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/03-friends.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/04-friends.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/04-friends.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/05-friends.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/05-friends.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/06-project.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/06-project.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/07-project.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/07-project.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/08-project.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/08-project.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/09-team.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/09-team.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/10-process.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/10-process.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/11-process.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/11-process.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/12-tools.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/12-tools.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/13-what-to-test.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/13-what-to-test.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/14-testing-pyramid.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/14-testing-pyramid.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/15-testing-pyramid.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/15-testing-pyramid.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/16-what-to-test.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/16-what-to-test.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/17-risks.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/17-risks.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/18-architecture.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/18-architecture.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/19-architecture.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/19-architecture.jpeg -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/20-next-episode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/20-next-episode.png -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/episode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/episode.png -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/intro.png -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/outro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/02-testing-serverless-apps/video-source/outro.png -------------------------------------------------------------------------------- /02-testing-serverless-apps/video-source/source.md: -------------------------------------------------------------------------------- 1 | --- 2 | size: 720p 3 | subtitles: auto 4 | background: 5 | vendor: uplifting-2 6 | volume: 0.5 7 | --- 8 | 9 | ![](intro.png) 10 | 11 | Welcome to Five Minutes Serverless! 12 | 13 | --- 14 | 15 | ![](episode.png) 16 | 17 | In this episode, we talk about testing serverless applications. 18 | 19 | --- 20 | 21 | ![](01-alex.jpeg) 22 | 23 | --- 24 | 25 | ![](02-meet-alex.jpeg) 26 | 27 | Meet Alex! 28 | 29 | He is a JavaScript developer, focused on Node. 30 | 31 | --- 32 | 33 | ![](03-friends.jpeg) 34 | 35 | (transition: crossfade 1) 36 | 37 | --- 38 | 39 | ![](04-friends.jpeg) 40 | 41 | Over the last couple of months, his good friends, Anna and Jeff, are always talking about that serverless thingy. 42 | 43 | (transition: crossfade 1) 44 | 45 | --- 46 | 47 | (voice: mario) 48 | 49 | ![](05-friends.jpeg) 50 | 51 | 52 | 53 | Mamma mia! 54 | 55 | 56 | 57 | --- 58 | 59 | 60 | ![](05-friends.jpeg) 61 | 62 | Even though they are annoying from time to time, he likes the idea of serverless apps. 63 | 64 | (transition: crossfade 1) 65 | 66 | --- 67 | 68 | ![](06-project.jpeg) 69 | 70 | (transition: crossfade 1) 71 | 72 | --- 73 | 74 | ![](07-project.jpeg) 75 | 76 | At some point, Alex and his team got a new project. 77 | 78 | (transition: crossfade 1) 79 | 80 | --- 81 | 82 | ![](08-project.jpeg) 83 | 84 | After some analysis, Alex thought that it would be the perfect fit for serverless. 85 | 86 | They decided to give it a try. The project wasn’t too big, and the risk was low. 87 | 88 | (transition: crossfade 1) 89 | 90 | --- 91 | 92 | ![](09-team.jpeg) 93 | 94 | The team read about serverless, and they got an idea how to structure their new app. 95 | 96 | But no one was sure how they should fit serverless into their common development process. 97 | 98 | --- 99 | 100 | ![](10-process.jpeg) 101 | 102 | At that moment, their process looks like this: 103 | 104 | First, they analyze a new feature. 105 | 106 | For less complex features, they start with the code, then they run it locally and add some tests in the end. 107 | 108 | For more complex features, they do their version of TDD: they start with tests, then write the code, and test it locally. 109 | 110 | (pause: 1) 111 | 112 | When the feature is ready, it goes to the C.I. tool that deploys it to the testing environment. 113 | 114 | (pause: 1) 115 | 116 | Then the QA team takes a new feature for another round of manual testing. If everything looks good, the app goes through C.I. to production. 117 | 118 | --- 119 | 120 | (callout: 121 | type: rectangle 122 | left: 615 123 | top: 175 124 | right: 765 125 | bottom: 265) 126 | 127 | ![](11-process.jpeg) 128 | 129 | The teams biggest challenge are automated tests. 130 | 131 | (transition: crossfade 1) 132 | 133 | --- 134 | 135 | ![](11-process.jpeg) 136 | 137 | (transition: crossfade 1) 138 | 139 | Alex and his team just switched to Jest for testing their Node applications. 140 | 141 | They still do a lot of front end, so they want to use the same tools for the full stack whenever they can. 142 | 143 | Can they use Jest for testing serverless apps too? 144 | 145 | --- 146 | 147 | ![](12-tools.jpeg) 148 | 149 | After a quick investigation, they realized that they can use their favorite Node testing tools. 150 | 151 | Jest, Jasmine, Mocha, and others work fine with serverless. 152 | 153 | --- 154 | 155 | ![](13-what-to-test.jpeg) 156 | 157 | But, what should they test? 158 | 159 | --- 160 | 161 | ![](14-testing-pyramid.jpeg) 162 | 163 | With their Node apps, Alex and his team follows the three-tier test automation pyramid. 164 | 165 | As the test pyramid defines, they have: 166 | 167 | --- 168 | 169 | (callout: 170 | type: rectangle 171 | left: 170 172 | top: 490 173 | right: 1000 174 | bottom: 615) 175 | 176 | ![](14-testing-pyramid.jpeg) 177 | 178 | A lot of unit tests, because they are the cheapest, and fastest to write and run 179 | 180 | --- 181 | 182 | (callout: 183 | type: rectangle 184 | left: 170 185 | top: 380 186 | right: 1100 187 | bottom: 510) 188 | 189 | ![](14-testing-pyramid.jpeg) 190 | 191 | Fewer integration tests, because they are more expensive, and they take more time to run 192 | 193 | --- 194 | 195 | (callout: 196 | type: rectangle 197 | left: 170 198 | top: 250 199 | right: 1000 200 | bottom: 405) 201 | 202 | ![](14-testing-pyramid.jpeg) 203 | 204 | A few UI tests, because they are the most expensive. They requires some Graphic User Interface tool, and slowest to run 205 | 206 | --- 207 | 208 | (callout: 209 | type: rectangle 210 | left: 170 211 | top: 65 212 | right: 1100 213 | bottom: 260) 214 | 215 | ![](14-testing-pyramid.jpeg) 216 | 217 | Besides these, they also have manual session-based testing, done by their QA team. 218 | 219 | --- 220 | 221 | ![](14-testing-pyramid.jpeg) 222 | 223 | How does serverless affect the test automation pyramid? 224 | 225 | --- 226 | 227 | ![](15-testing-pyramid.jpeg) 228 | 229 | The serverless test pyramid looks less like the Egyptian, and more like the Mayan pyramids. 230 | 231 | --- 232 | 233 | (callout: 234 | type: rectangle 235 | left: 170 236 | top: 490 237 | right: 990 238 | bottom: 610) 239 | 240 | ![](15-testing-pyramid.jpeg) 241 | 242 | The unit tests layer is not affected a lot. Unit tests are still the cheapest to write and run. 243 | 244 | --- 245 | 246 | (callout: 247 | type: rectangle 248 | left: 170 249 | top: 390 250 | right: 1090 251 | bottom: 505) 252 | 253 | ![](15-testing-pyramid.jpeg) 254 | 255 | Integration tests layer becomes more important than ever, because serverless apps relies heavily on integrations. 256 | 257 | It is also cheaper, because having a serverless database just for testing is cheap. 258 | 259 | In a serverless “test pyramid” you need to have more integration tests. 260 | 261 | --- 262 | 263 | (callout: 264 | type: rectangle 265 | left: 170 266 | top: 250 267 | right: 980 268 | bottom: 400) 269 | 270 | ![](15-testing-pyramid.jpeg) 271 | 272 | UI tests layer is also cheaper and faster, because of cheaper parallelization. 273 | 274 | --- 275 | 276 | (callout: 277 | type: rectangle 278 | left: 170 279 | top: 65 280 | right: 1100 281 | bottom: 260) 282 | 283 | ![](15-testing-pyramid.jpeg) 284 | 285 | Manual testing layer stays the same. 286 | 287 | --- 288 | 289 | ![](16-what-to-test.jpeg) 290 | 291 | Alex and his team finally had some idea where to focus. 292 | 293 | The next problem was how to write a function to test them more easily. 294 | 295 | --- 296 | 297 | ![](17-risks.jpeg) 298 | 299 | You need to think about the following risks while you are writing a serverless function: 300 | 301 | (pause: 1) 302 | 303 | Configuration risks. For example, are the database and table correct? Or, do you have access rights? 304 | 305 | (pause: 1) 306 | 307 | Technical workflow risks. Are you parsing and using the incoming request as you should? Or, are you handling successful responses and errors correctly? 308 | 309 | (pause: 1) 310 | 311 | Business logic risks. Did you follow all the business logic rules that your application has? 312 | 313 | (pause: 1) 314 | 315 | Integration risks. Are you reading the incoming request structure correctly? Or are you storing the order to the database correctly? 316 | 317 | (pause: 1) 318 | 319 | To confirm that your serverless function is working as expected, you need to test all these risks. 320 | 321 | --- 322 | 323 | ![](18-architecture.jpeg) 324 | 325 | To make the app more testable, the clear solution is to break up your function into several smaller ones. 326 | 327 | One of the great ways to do so is applying Hexagonal Architecture to your serverless functions. 328 | 329 | --- 330 | 331 | ![](19-architecture.jpeg) 332 | 333 | Hexagonal Architecture, or Ports and Adapters, allows an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases. 334 | 335 | --- 336 | 337 | ![](20-next-episode.png) 338 | 339 | How to apply hexagonal architecture to your serverless functions? 340 | 341 | (pause: 1) 342 | 343 | Stay tuned for our next episode to see the practical example using AWS Lambda, Node, and Jest. 344 | 345 | (pause: 5) 346 | 347 | --- 348 | 349 | (font-size: 32) 350 | 351 | ```md 352 | **Credits:** 353 | 354 | - I built this video using Video Puppet 355 | - I made diagrams using SimpleDiagrams 4 356 | - Thanks to my friends Aleksandar (@simalexan) and Gojko (@gojkoadzic) for helping me with this video 357 | 358 | The next episode will be released early next week! 359 | 360 | Video by @slobodan_ 361 | ``` 362 | 363 | Thanks for watching the second episode of Five Minutes Serverless! 364 | 365 | (pause: 1) 366 | 367 | Feel free to send me feedback or questions on twitter. 368 | 369 | (duration: 8) 370 | 371 | --- 372 | 373 | ![](outro.png) 374 | 375 | (duration: 5) -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/source-code/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "project": "tsconfig.json", 14 | "sourceType": "module" 15 | }, 16 | "plugins": [ 17 | "@typescript-eslint", 18 | "@typescript-eslint/tslint" 19 | ], 20 | "rules": { 21 | "@typescript-eslint/array-type": "error", 22 | "@typescript-eslint/ban-types": "off", 23 | "@typescript-eslint/indent": [ 24 | "error", 25 | 2 26 | ], 27 | "@typescript-eslint/interface-name-prefix": "off", 28 | "@typescript-eslint/member-delimiter-style": [ 29 | "error", 30 | { 31 | "multiline": { 32 | "delimiter": "none", 33 | "requireLast": true 34 | }, 35 | "singleline": { 36 | "delimiter": "semi", 37 | "requireLast": false 38 | } 39 | } 40 | ], 41 | "@typescript-eslint/no-explicit-any": "off", 42 | "@typescript-eslint/no-parameter-properties": "off", 43 | "@typescript-eslint/no-use-before-define": "off", 44 | "@typescript-eslint/prefer-for-of": "error", 45 | "@typescript-eslint/prefer-function-type": "error", 46 | "@typescript-eslint/quotes": [ 47 | "error", 48 | "single", 49 | { 50 | "avoidEscape": true 51 | } 52 | ], 53 | "@typescript-eslint/semi": [ 54 | "error", 55 | "never" 56 | ], 57 | "@typescript-eslint/unified-signatures": "error", 58 | "camelcase": "error", 59 | "comma-dangle": [ 60 | "error", 61 | { 62 | "objects": "always-multiline", 63 | "arrays": "always-multiline", 64 | "functions": "never" 65 | } 66 | ], 67 | "complexity": "off", 68 | "constructor-super": "error", 69 | "dot-notation": "error", 70 | "eqeqeq": [ 71 | "error", 72 | "smart" 73 | ], 74 | "guard-for-in": "error", 75 | "id-blacklist": [ 76 | "error", 77 | "any", 78 | "Number", 79 | "number", 80 | "String", 81 | "string", 82 | "Boolean", 83 | "boolean", 84 | "Undefined", 85 | "undefined" 86 | ], 87 | "id-match": "error", 88 | "max-classes-per-file": "off", 89 | "max-len": [ 90 | "error", 91 | { 92 | "code": 180 93 | } 94 | ], 95 | "new-parens": "error", 96 | "no-bitwise": "error", 97 | "no-caller": "error", 98 | "no-cond-assign": "error", 99 | "no-console": "off", 100 | "no-debugger": "error", 101 | "no-empty": "error", 102 | "no-eval": "error", 103 | "no-fallthrough": "off", 104 | "no-invalid-this": "off", 105 | "no-new-wrappers": "error", 106 | "no-shadow": [ 107 | "error", 108 | { 109 | "hoist": "all" 110 | } 111 | ], 112 | "no-throw-literal": "error", 113 | "no-trailing-spaces": "error", 114 | "no-undef-init": "error", 115 | "no-underscore-dangle": "error", 116 | "no-unsafe-finally": "error", 117 | "no-unused-expressions": "error", 118 | "no-unused-labels": "error", 119 | "object-shorthand": "error", 120 | "one-var": [ 121 | "off", 122 | "never" 123 | ], 124 | "radix": "error", 125 | "spaced-comment": "error", 126 | "use-isnan": "error", 127 | "valid-typeof": "off", 128 | "@typescript-eslint/tslint/config": [ 129 | "error", 130 | { 131 | "rules": { 132 | "jsdoc-format": true, 133 | "no-reference-import": true 134 | } 135 | } 136 | ] 137 | } 138 | }; 139 | -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/source-code/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | samconfig.toml 3 | node_modules 4 | info 5 | -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/source-code/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2020 Gojko Adzic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/source-code/README.md: -------------------------------------------------------------------------------- 1 | # AWS Serverless Application Model TypeScript template 2 | 3 | A simple AWS SAM template with TypeScript. 4 | 5 | ## Folder structure 6 | 7 | This project has the following folder structure: 8 | 9 | ```bash 10 | . 11 | ├── README.md # This file 12 | ├── build # Build folder 13 | │   └── hello-world # Each function has its own folder 14 | │   ├── lambda.js 15 | │   └── lambda.js.map # And each function has sourcemaps 16 | ├── jest.config.js # Jest configuration 17 | ├── package.json 18 | ├── src # Source code for all functions 19 | │   └── hello-world # Sample function 20 | │   ├── events # Sample event for local invocationn 21 | │   │ └── event.json 22 | │   ├── lambda.ts # Main file 23 | │   ├── lib # Rest of code, including function business logic 24 | │   │ └── main.ts 25 | │   └── tests # Tests for business logic and all important files 26 | │   └── main.test.ts 27 | ├── template.yaml # Main CloudFormation file 28 | ├── webpack.config.js # Webpack config 29 | ├── yarn.lock 30 | └── .eslintrc.js # ESLint config 31 | ``` 32 | 33 | ## Usage 34 | 35 | To use this template, make sure you have the following prerequisites: 36 | 37 | - AWS profile 38 | - AWS SAM installed and configured 39 | - Node.js version 8 or more (version 12 is recommended) 40 | 41 | ### Initialize a new project 42 | 43 | To create a new project using this template, create a new folder, navigate to your new folder in your terminal, and run the following command: 44 | 45 | ```bash 46 | sam init --location gh:serverlesspub/sam-ts 47 | ``` 48 | 49 | This will create a new AWS SAM project with the folder structure explained above. 50 | 51 | ### Build TypeScript 52 | 53 | To build TypeScript, run the following command: 54 | 55 | ```bash 56 | npm run build 57 | ``` 58 | 59 | If you want to build a project and run the webpack bundle analyzer, run the following command: 60 | 61 | ```bash 62 | npm run build-analyze 63 | ``` 64 | 65 | ### Deploy 66 | 67 | To deploy the project, run the following command: 68 | 69 | ```bash 70 | sam deploy --guided 71 | ``` 72 | 73 | This will run an interactive deployment process, save your configuration to the `samconfig.toml` file, and deploy the project to your AWS account. 74 | 75 | _NOTE: The `samconfig.toml` file is on git ignore list._ 76 | 77 | ### Run automated tests 78 | 79 | To run Jest tests, use the following command: 80 | 81 | ```bash 82 | npm run test 83 | ``` 84 | 85 | This command will run ESLint, and if there are no linting issues, it'll run Jest tests. 86 | 87 | If you want to run ESLint without tests, use the following command: 88 | 89 | ```bash 90 | npm run lint 91 | ``` 92 | 93 | ### Test and debug using SAM Local 94 | 95 | TBA 96 | 97 | ## License 98 | 99 | MIT, see [LICENSE](LICENSE). -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/source-code/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [ 3 | "./src" 4 | ], 5 | transform: { 6 | "^.+\\.tsx?$": "ts-jest" 7 | } 8 | } -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/source-code/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-sam-ts-template", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "watch": "NODE_ENV=dev webpack --watch --mode=development", 8 | "build": "NODE_ENV=prod BUNDLE_ANALYZER=false webpack --mode=production --progress", 9 | "build-analyze": "NODE_ENV=prod BUNDLE_ANALYZER=true webpack --mode=production --progress", 10 | "lint": "eslint -c .eslintrc.js --ext ts src", 11 | "pretest": "npm run lint", 12 | "test": "jest", 13 | "coverage": "jest --coverage" 14 | }, 15 | "keywords": [], 16 | "author": "Slobodan Stojanović (http://slobodan.me)", 17 | "license": "MIT", 18 | "dependencies": { 19 | "@types/aws-lambda": "^8.10.46", 20 | "@types/node": "^13.9.1", 21 | "aws-lambda": "^1.0.5", 22 | "aws-sdk": "^2.639.0", 23 | "aws-xray-sdk": "^2.5.0", 24 | "source-map-support": "^0.5.16", 25 | "tslint": "^6.1.0" 26 | }, 27 | "devDependencies": { 28 | "@types/jest": "^25.1.4", 29 | "@typescript-eslint/eslint-plugin": "^2.23.0", 30 | "@typescript-eslint/eslint-plugin-tslint": "^2.23.0", 31 | "@typescript-eslint/parser": "^2.23.0", 32 | "eslint": "^6.8.0", 33 | "eslint-plugin-prefer-arrow": "^1.1.7", 34 | "eslint-plugin-prefer-arrow-functions": "^3.0.1", 35 | "jest": "^25.1.0", 36 | "source-map-loader": "^0.2.4", 37 | "speed-measure-webpack-plugin": "^1.3.1", 38 | "ts-jest": "^25.2.1", 39 | "ts-loader": "^6.2.1", 40 | "ts-node": "^8.6.2", 41 | "typescript": "^3.8.3", 42 | "webpack": "^4.42.0", 43 | "webpack-bundle-analyzer": "^3.6.1", 44 | "webpack-cli": "^3.3.11" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/source-code/src/common/booksdb-repository.ts: -------------------------------------------------------------------------------- 1 | import { DocumentClient } from 'aws-sdk/clients/dynamodb' 2 | const documentClient = new DocumentClient() 3 | 4 | export interface IBook { 5 | id: string 6 | name: string 7 | authors: string[] 8 | } 9 | 10 | interface IDocumentClient { 11 | scan(scanInput: DocumentClient.ScanInput): { 12 | promise(): Promise 13 | } 14 | } 15 | 16 | export class BooksDbRepository { 17 | public tableName: string 18 | public dc: IDocumentClient 19 | 20 | constructor(tableName: string, dc: IDocumentClient = documentClient) { 21 | this.tableName = tableName 22 | this.dc = dc 23 | } 24 | 25 | public async get(): Promise { 26 | const params = { 27 | TableName: this.tableName, 28 | } 29 | 30 | const result = await this.dc.scan(params).promise() 31 | 32 | return (result.Items as IBook[]) || [] 33 | } 34 | } -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/source-code/src/common/tests/booksdb-repository.test.ts: -------------------------------------------------------------------------------- 1 | import { BooksDbRepository } from '../booksdb-repository' 2 | import DynamoDb, { DocumentClient } from 'aws-sdk/clients/dynamodb' 3 | const dynamoDb = new DynamoDb() 4 | const documentClient = new DocumentClient() 5 | 6 | describe('Books DB Repository', () => { 7 | describe('unit', () => { 8 | test('should invoke dc.scan when get method is invoked', async () => { 9 | const dcMock = { 10 | scan: jest.fn().mockReturnValue({ 11 | promise: () => Promise.resolve({ Items: [] }), 12 | }), 13 | } 14 | const booksDb = new BooksDbRepository('tableName', dcMock) 15 | await booksDb.get() 16 | 17 | expect(dcMock.scan).toHaveBeenCalledTimes(1) 18 | expect(dcMock.scan).toHaveBeenCalledWith({ 19 | TableName: 'tableName', 20 | }) 21 | }) 22 | }) 23 | 24 | describe('integration', () => { 25 | const tableName = 'test-dynamodb-table-getBooks' 26 | 27 | beforeAll(async () => { 28 | const params = { 29 | AttributeDefinitions: [{ 30 | AttributeName: 'id', 31 | AttributeType: 'S', 32 | }], 33 | KeySchema: [{ 34 | AttributeName: 'id', 35 | KeyType: 'HASH', 36 | }], 37 | BillingMode: 'PAY_PER_REQUEST', 38 | TableName: tableName, 39 | } 40 | 41 | await dynamoDb.createTable(params).promise() 42 | 43 | await dynamoDb.waitFor('tableExists', { 44 | TableName: tableName, 45 | }).promise() 46 | }, 60 * 1000) 47 | 48 | afterAll(async () => { 49 | await dynamoDb.deleteTable({ 50 | TableName: tableName, 51 | }).promise() 52 | 53 | await dynamoDb.waitFor('tableNotExists', { 54 | TableName: tableName, 55 | }).promise() 56 | }, 60 * 1000) 57 | 58 | test('should scan data from the table', async () => { 59 | await documentClient.put({ 60 | TableName: tableName, 61 | Item: { 62 | id: '978-1617294723', 63 | title: 'Serverless Applications with Node.js', 64 | authors: ['Aleksandar Simović', 'Slobodan Stojanović'], 65 | }, 66 | }).promise() 67 | 68 | const booksDb = new BooksDbRepository(tableName) 69 | const result = await booksDb.get() 70 | 71 | expect(result).toEqual([{ 72 | id: '978-1617294723', 73 | title: 'Serverless Applications with Node.js', 74 | authors: ['Aleksandar Simović', 'Slobodan Stojanović'], 75 | }]) 76 | }, 60 * 1000) 77 | }) 78 | }) -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/source-code/src/get-books/events/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": 1, 3 | "b": 2 4 | } -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/source-code/src/get-books/lambda.ts: -------------------------------------------------------------------------------- 1 | // Allow CloudWatch to read source maps 2 | import 'source-map-support/register' 3 | 4 | // Enable AWS X-Ray 5 | import { captureHTTPsGlobal } from 'aws-xray-sdk' 6 | import https from 'https' 7 | captureHTTPsGlobal(https, true) 8 | 9 | import { APIGatewayProxyResult } from 'aws-lambda' 10 | import { getBooks } from './lib/main' 11 | import { BooksDbRepository } from '../common/booksdb-repository' 12 | 13 | export async function handler(): Promise { 14 | try { 15 | if (!process.env.TABLE_NAME) { 16 | throw new Error('TABLE_NAME environment variable is required') 17 | } 18 | const booksDb = new BooksDbRepository(process.env.TABLE_NAME) 19 | 20 | const books = await getBooks(booksDb) 21 | 22 | return { 23 | statusCode: 200, 24 | body: JSON.stringify(books), 25 | } 26 | } catch(err) { 27 | return { 28 | statusCode: 400, 29 | body: err.toString(), 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/source-code/src/get-books/lib/main.ts: -------------------------------------------------------------------------------- 1 | interface IBook { 2 | id: string 3 | name: string 4 | authors: string[] 5 | } 6 | 7 | interface IBooksDb { 8 | get(): Promise 9 | } 10 | 11 | export async function getBooks(booksDb: IBooksDb): Promise { 12 | return await booksDb.get() 13 | } 14 | -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/source-code/src/get-books/tests/main.test.ts: -------------------------------------------------------------------------------- 1 | import { getBooks } from '../lib/main' 2 | 3 | describe('Get Books', () => { 4 | describe('unit', () => { 5 | test('should invoke BooksDb.get', async () => { 6 | const bookDbMock = { 7 | get: jest.fn(), 8 | } 9 | 10 | await getBooks(bookDbMock) 11 | 12 | expect(bookDbMock.get).toHaveBeenCalledTimes(1) 13 | expect(bookDbMock.get).toHaveBeenCalledWith() 14 | }) 15 | }) 16 | 17 | describe('integration', () => { 18 | class LocalDb { 19 | private data: any[] = [] 20 | 21 | // eslint-disable-next-line @typescript-eslint/require-await 22 | async get(): Promise { 23 | return this.data 24 | } 25 | 26 | // eslint-disable-next-line @typescript-eslint/require-await 27 | async add(item: number): Promise { 28 | this.data.push(item) 29 | } 30 | } 31 | 32 | test('should invoke BooksDb.get', async () => { 33 | const localDb = new LocalDb() 34 | 35 | localDb.add(1) 36 | localDb.add(2) 37 | localDb.add(3) 38 | const result = await getBooks(localDb) 39 | 40 | expect(result).toEqual([1, 2, 3]) 41 | }) 42 | }) 43 | }) -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/source-code/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Vacation Tracker API 4 | 5 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 6 | Globals: 7 | Function: 8 | Runtime: nodejs12.x 9 | Tracing: Active 10 | 11 | Resources: 12 | BooksTable: 13 | Type: AWS::DynamoDB::Table 14 | Properties: 15 | AttributeDefinitions: 16 | - AttributeName: "id" 17 | AttributeType: "S" 18 | KeySchema: 19 | - AttributeName: "id" 20 | KeyType: "HASH" 21 | BillingMode: PAY_PER_REQUEST 22 | 23 | GetBooks: 24 | Type: AWS::Serverless::Function 25 | Properties: 26 | CodeUri: build/hello-world 27 | Handler: lambda.handler 28 | Policies: 29 | - DynamoDBReadPolicy: 30 | TableName: !Ref BooksTable 31 | Environment: 32 | Variables: 33 | TABLE_NAME: !Ref BooksTable 34 | Events: 35 | GetResource: 36 | Type: Api 37 | Properties: 38 | Path: /books 39 | Method: GET 40 | 41 | Outputs: 42 | ApiUrl: 43 | Description: API URL 44 | Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/" 45 | -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/source-code/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*.ts" 4 | ], 5 | "compilerOptions": { 6 | "incremental": true, 7 | "allowJs": true, 8 | "target": "es2017", 9 | "module": "commonjs", 10 | "strict": true, 11 | "baseUrl": "./", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "types": [ 16 | "node", 17 | "jest" 18 | ], 19 | "esModuleInterop": true, 20 | "inlineSourceMap": true, 21 | "resolveJsonModule": true 22 | } 23 | } -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/source-code/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const glob = require('glob') 3 | const webpack = require('webpack') 4 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 5 | const SpeedMeasurePlugin = require('speed-measure-webpack-plugin') 6 | const smp = new SpeedMeasurePlugin() 7 | 8 | // Credits: https://hackernoon.com/webpack-creating-dynamically-named-outputs-for-wildcarded-entry-files-9241f596b065 9 | const entryArray = glob.sync('./src/**/lambda.ts') 10 | const entryObject = entryArray.reduce((acc, item) => { 11 | let name = path.dirname(item.replace('./src/', '')) 12 | // conforms with Webpack entry API 13 | // Example: { test: './src/test/lambda.ts' } 14 | acc[name] = item 15 | return acc 16 | }, {}) 17 | 18 | const config = smp.wrap({ 19 | entry: entryObject, 20 | devtool: 'source-map', 21 | target: 'node', 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.tsx?$/, 26 | use: [ 27 | { 28 | loader: 'ts-loader', 29 | options: { 30 | transpileOnly: true, 31 | experimentalWatchApi: true 32 | } 33 | } 34 | ], 35 | exclude: /node_modules/ 36 | } 37 | ] 38 | }, 39 | optimization: { 40 | minimize: false 41 | }, 42 | plugins: [new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)], 43 | resolve: { 44 | extensions: ['.ts', '.js'], 45 | symlinks: false, 46 | cacheWithContext: false 47 | }, 48 | // Output directive will generate build//lambda.js 49 | output: { 50 | filename: '[name]/lambda.js', 51 | path: path.resolve(__dirname, 'build'), 52 | devtoolModuleFilenameTemplate: '[absolute-resource-path]', 53 | // credits to Rich Buggy!!! 54 | libraryTarget: 'commonjs2' 55 | } 56 | }) 57 | 58 | if (process.env.BUNDLE_ANALYZER && process.env.BUNDLE_ANALYZER === 'true') { 59 | config.plugins.push(new BundleAnalyzerPlugin({ 60 | analyzerMode: 'static', 61 | reportFilename: '../info/bundle-report.html', 62 | openAnalyzer: false 63 | })) 64 | } 65 | 66 | module.exports = config 67 | -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/video-source/01-risks.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/03-testing-serverless-apps-part-2/video-source/01-risks.jpeg -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/video-source/02-architecture.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/03-testing-serverless-apps-part-2/video-source/02-architecture.jpeg -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/video-source/03-architecture.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/03-testing-serverless-apps-part-2/video-source/03-architecture.jpeg -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/video-source/04-architecture.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/03-testing-serverless-apps-part-2/video-source/04-architecture.jpeg -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/video-source/05-architecture.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/03-testing-serverless-apps-part-2/video-source/05-architecture.jpeg -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/video-source/06-testing-pyramid.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/03-testing-serverless-apps-part-2/video-source/06-testing-pyramid.jpeg -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/video-source/07-ui-tests.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/03-testing-serverless-apps-part-2/video-source/07-ui-tests.jpeg -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/video-source/08-code.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/03-testing-serverless-apps-part-2/video-source/08-code.jpeg -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/video-source/09-folder-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/03-testing-serverless-apps-part-2/video-source/09-folder-structure.png -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/video-source/10-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/03-testing-serverless-apps-part-2/video-source/10-diagram.png -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/video-source/11-sam-deploy.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/03-testing-serverless-apps-part-2/video-source/11-sam-deploy.jpeg -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/video-source/12-testing.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/03-testing-serverless-apps-part-2/video-source/12-testing.jpeg -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/video-source/episode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/03-testing-serverless-apps-part-2/video-source/episode.png -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/video-source/intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/03-testing-serverless-apps-part-2/video-source/intro.png -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/video-source/outro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/03-testing-serverless-apps-part-2/video-source/outro.png -------------------------------------------------------------------------------- /03-testing-serverless-apps-part-2/video-source/source.md: -------------------------------------------------------------------------------- 1 | --- 2 | size: 720p 3 | subtitles: auto 4 | background: 5 | vendor: uplifting-2 6 | volume: 0.5 7 | --- 8 | 9 | ![](intro.png) 10 | 11 | Welcome to Five Minutes Serverless! 12 | 13 | --- 14 | 15 | ![](episode.png) 16 | 17 | In this episode, we talk about testing serverless applications using hexagonal architecture. 18 | 19 | --- 20 | 21 | ![](01-risks.jpeg) 22 | 23 | As we saw in the previous episode, when writing a serverless function, we need to think about configuration, technical workflow, business logic, and integration risks. 24 | 25 | --- 26 | 27 | ![](02-architecture.jpeg) 28 | 29 | We need good architecture to handle them all. 30 | 31 | --- 32 | 33 | ![](03-architecture.jpeg) 34 | 35 | One of the great ways to do so is applying Hexagonal Architecture, or Ports and Adapters, to our serverless functions. 36 | 37 | --- 38 | 39 | ![](04-architecture.jpeg) 40 | 41 | Hexagonal Architecture allows an application to equally be driven by users, programs, automated test or batch scripts, 42 | 43 | and to be developed and tested in isolation from its eventual run-time devices and databases. 44 | 45 | Let's see how does that applies to our serverless functions. 46 | 47 | --- 48 | 49 | ![](05-architecture.jpeg) 50 | 51 | Alex and his team use AWS, and they ended up with the following structure: 52 | 53 | Function business logic exposes few "ports." For example, one for an incoming event, one for permanent storage, and one for notifications. 54 | 55 | They have two adapters for the event that triggers a function, one for the real AWS Lambda trigger and another one for local testing. 56 | 57 | They have several adapters for permanent storage and notifications, for example, DynamoDB table adapter and in-memory adapter. 58 | 59 | --- 60 | 61 | ![](06-testing-pyramid.jpeg) 62 | 63 | Before we continue, let's go back to our serverless testing pyramid and see how this architecture affects all tiers. 64 | 65 | --- 66 | 67 | 68 | (callout: 69 | type: rectangle 70 | left: 170 71 | top: 490 72 | right: 990 73 | bottom: 610) 74 | 75 | ![](06-testing-pyramid.jpeg) 76 | 77 | Unit tests are the same, but easier to write. Alex can simply use a mock as an adapter to test the function business layer in isolation. 78 | 79 | --- 80 | 81 | (callout: 82 | type: rectangle 83 | left: 170 84 | top: 390 85 | right: 1090 86 | bottom: 505) 87 | 88 | ![](06-testing-pyramid.jpeg) 89 | 90 | Integration tests benefit a lot from Hexagonal Architecture. They were able to test integrations that they own thoroughly. Third-party integrations are simulated with other adapters. 91 | 92 | --- 93 | 94 | (callout: 95 | type: rectangle 96 | left: 170 97 | top: 250 98 | right: 980 99 | bottom: 400) 100 | 101 | ![](06-testing-pyramid.jpeg) 102 | 103 | As Alex and his team were building a back end for the app, the UI test tier was not relevant. However, they can use serverless to improve these tests, too. 104 | 105 | --- 106 | 107 | ![](07-ui-tests.jpeg) 108 | 109 | UI tests are expensive and slow because they run in the browser. Serverless is cheap, and it scales fast. 110 | 111 | Alex and his team can run a browser in AWS Lambda, to gain cheap parallelization and make their UI tests cheaper and faster. 112 | 113 | --- 114 | 115 | ![](10-diagram.png) 116 | 117 | Let's see the code example. We'll build a simple Books API with an API Gateway, Lambda, and DynamoDB table. 118 | 119 | --- 120 | 121 | (font-size: 42) 122 | 123 | ```md 124 | **Prerequisites:** 125 | 126 | - An active AWS account 127 | - AWS SAM installed 128 | - Node.js version 8+ (version 12 is recommended) 129 | ``` 130 | 131 | Before we begin, make sure you have the following prerequisites. 132 | 133 | You also need a basic knowledge of Node, Jest, and TypeScript. 134 | 135 | --- 136 | 137 | ```bash 138 | mkdir testing-example 139 | cd testing-example 140 | ``` 141 | 142 | Start by creating a new folder and navigating to it using your terminal. We'll call it "testing-example." 143 | 144 | --- 145 | 146 | (font-size: 40) 147 | 148 | ```bash 149 | sam init --location gh:serverlesspub/sam-ts 150 | ``` 151 | 152 | You can initialize an AWS SAM project the same way we did in the first episode. 153 | 154 | Run the following command from your new folder using your terminal. 155 | 156 | --- 157 | 158 | ![](09-folder-structure.png) 159 | 160 | This command will create a new AWS SAM project with a Hello World function written in TypeScript. 161 | 162 | --- 163 | 164 | (font-size: 40) 165 | 166 | ```yaml 167 | GetBooks: 168 | Type: AWS::Serverless::Function 169 | Properties: 170 | CodeUri: build/get-books 171 | Handler: lambda.handler 172 | Events: 173 | GetResource: 174 | Type: Api 175 | Properties: 176 | Path: /books 177 | Method: GET 178 | ``` 179 | 180 | Open template.yaml file in your favorite code editor and replace Hello World function with the following GetBooks function. 181 | 182 | Add an API event trigger with GET method and /books path. 183 | 184 | --- 185 | 186 | (font-size: 21) 187 | 188 | ```yaml 189 | Outputs: 190 | ApiUrl: 191 | Description: API URL 192 | Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/" 193 | ``` 194 | 195 | Also, modify the outputs to output the API URL. ServerlessRestApi is an implicit API created by our Get Books function. SAM will deploy the Prod stage by default. 196 | 197 | --- 198 | 199 | (font-size: 42) 200 | 201 | ```yaml 202 | BooksTable: 203 | Type: AWS::DynamoDB::Table 204 | Properties: 205 | AttributeDefinitions: 206 | - AttributeName: "id" 207 | AttributeType: "S" 208 | KeySchema: 209 | - AttributeName: "id" 210 | KeyType: "HASH" 211 | BillingMode: PAY_PER_REQUEST 212 | ``` 213 | 214 | Then add a DynamoDB table to the Resources section of your template file. 215 | 216 | Books table can look similar to the following CloudFormation resource. 217 | 218 | --- 219 | 220 | (font-size: 28) 221 | 222 | ```yaml 223 | GetBooks: 224 | Type: AWS::Serverless::Function 225 | Properties: 226 | CodeUri: build/hello-world 227 | Handler: lambda.handler 228 | Policies: 229 | - DynamoDBReadPolicy: 230 | TableName: !Ref BooksTable 231 | Environment: 232 | Variables: 233 | TABLE_NAME: !Ref BooksTable 234 | Events: 235 | GetResource: 236 | Type: Api 237 | Properties: 238 | Path: /books 239 | Method: GET 240 | ``` 241 | 242 | Before we can write the code, pass a table name in an environment variable, and add a policy that allows your GetBooks function to read data from the database. 243 | 244 | To do so, use the "DynamoDBReadPolicy" SAM Policy Template. 245 | 246 | --- 247 | 248 | (font-size: 21) 249 | 250 | ```typescript 251 | import { DocumentClient } from 'aws-sdk/clients/dynamodb' 252 | const documentClient = new DocumentClient() 253 | 254 | export interface IBook { 255 | id: string 256 | name: string 257 | authors: string[] 258 | } 259 | 260 | export class BooksDbRepository { 261 | public tableName: string 262 | public dc: DocumentClient 263 | 264 | constructor(tableName: string, dc = documentClient) { 265 | this.tableName = tableName 266 | this.dc = dc 267 | } 268 | 269 | public async get() { 270 | const params = { 271 | TableName: this.tableName 272 | }; 273 | 274 | const result = await this.dc.scan(params).promise() 275 | 276 | return (result.Items as IBook[]) || [] 277 | } 278 | } 279 | ``` 280 | 281 | The infrastructure is ready. Let's add code. 282 | 283 | Start with the Books database repository. Create the "common" folder in the source folder. Then create the "booksdb-repository.ts" file in that folder. 284 | 285 | This file represents the Books database repository that will communicate with DynamoDB and should have the following content. 286 | 287 | I think you should avoid scanning the DynamoDB table whenever you can, but we'll use "scan" in this video just to demonstrate how to test a function. 288 | 289 | --- 290 | 291 | (font-size: 24) 292 | 293 | ```typescript 294 | interface IBook { 295 | id: string 296 | name: string 297 | authors: string[] 298 | } 299 | 300 | import { BooksDbRepository } from '../../common/booksdb-repository' 301 | 302 | export async function getBooks(booksDb: BooksDbRepository): Promise { 303 | return await booksDb.get() 304 | } 305 | ``` 306 | 307 | Update the "main.ts" file in the "lib" folder of Get Books function to have the following content. 308 | 309 | --- 310 | 311 | (font-size: 28) 312 | 313 | ```typescript 314 | import { APIGatewayProxyResult } from 'aws-lambda' 315 | import { getBooks } from './lib/main' 316 | import { BooksDbRepository } from '../common/booksdb-repository' 317 | ``` 318 | 319 | Finally, update the "lambda.ts" file to import the following modules and types. 320 | 321 | --- 322 | 323 | (font-size: 26) 324 | 325 | ```typescript 326 | export async function handler(): Promise { 327 | try { 328 | if (!process.env.TABLE_NAME) { 329 | throw new Error('TABLE_NAME environment variable is required') 330 | } 331 | const booksDb = new BooksDbRepository(process.env.TABLE_NAME) 332 | 333 | const books = await getBooks(booksDb) 334 | 335 | return { 336 | statusCode: 200, 337 | body: JSON.stringify(books) 338 | } 339 | } catch(err) { 340 | return { 341 | statusCode: 400, 342 | body: err.toString() 343 | } 344 | } 345 | } 346 | ``` 347 | 348 | And update handler function to have the following content. 349 | 350 | --- 351 | 352 | ```bash 353 | npm install 354 | npm run build 355 | sam deploy --guided 356 | ``` 357 | 358 | Build and deploy this project to confirm that everything works as expected. To do so, run the commands you see on your screen. 359 | 360 | --- 361 | 362 | ![](11-sam-deploy.jpeg) 363 | 364 | When the deployment finishes, SAM will output our API URL. 365 | 366 | If you visit the URL with /books path in the end, it should return an empty array. 367 | 368 | --- 369 | 370 | ![](12-testing.jpeg) 371 | 372 | Now that our API is working let's switch to testing. 373 | 374 | --- 375 | 376 | (font-size: 42) 377 | 378 | ```typescript 379 | import { getBooks } from '../lib/main' 380 | 381 | describe('Get Books', () => { 382 | describe('unit', () => { 383 | // Unit tests 384 | }) 385 | 386 | describe('integration', () => { 387 | // Integration tests 388 | }) 389 | }) 390 | ``` 391 | 392 | Open the "main.tests.ts" file of your Get Books function, and replace the content with the following code. 393 | 394 | --- 395 | 396 | (font-size: 32) 397 | 398 | ```typescript 399 | describe('unit', () => { 400 | test('should invoke BooksDb.get', async () => { 401 | const bookDbMock = { 402 | get: jest.fn(), 403 | } 404 | 405 | await getBooks(bookDbMock) 406 | 407 | expect(bookDbMock.get).toHaveBeenCalledTimes(1) 408 | expect(bookDbMock.get).toHaveBeenCalledWith() 409 | }) 410 | }) 411 | ``` 412 | 413 | For unit tests, you can create a Jest Mock with the same interface and pass it to the Get Books function. 414 | 415 | --- 416 | 417 | (font-size: 32) 418 | 419 | ```typescript 420 | class LocalDb { 421 | private data: any[] = [] 422 | 423 | // eslint-disable-next-line @typescript-eslint/require-await 424 | async get(): Promise { 425 | return this.data 426 | } 427 | 428 | // eslint-disable-next-line @typescript-eslint/require-await 429 | async add(item: number): Promise { 430 | this.data.push(item) 431 | } 432 | } 433 | ``` 434 | 435 | For integrations tests, create a local database adapter, because we do not need to test each function against the DynamoDb table. 436 | 437 | --- 438 | 439 | (font-size: 32) 440 | 441 | ```typescript 442 | describe('integration', () => { 443 | test('should invoke BooksDb.get', async () => { 444 | const localDb = new LocalDb() 445 | 446 | localDb.add(1) 447 | localDb.add(2) 448 | localDb.add(3) 449 | const result = await getBooks(localDb) 450 | 451 | expect(result).toEqual([1, 2, 3]) 452 | }) 453 | }) 454 | ``` 455 | 456 | With a local database, integration tests are simple. Pass a Local Database Repository instead of Books DB Repository, and check the results. 457 | 458 | --- 459 | 460 | ```bash 461 | npm t 462 | ``` 463 | 464 | Run the following command from your terminal to run your tests. 465 | 466 | --- 467 | 468 | (font-size: 32) 469 | 470 | ```typescript 471 | describe('integration', () => { 472 | beforeAll(async () => { 473 | // Create test DB 474 | }) 475 | 476 | afterAll(async () => { 477 | // Destroy test DB 478 | }) 479 | 480 | // Tests 481 | }) 482 | ``` 483 | 484 | Integration tests for Books Database repository are even more important, and slightly more complicated because we create the DynamoDB table at the beginning and destroy it at the end of the test suite. 485 | 486 | --- 487 | 488 | (font-size: 24) 489 | 490 | ```typescript 491 | beforeAll(async () => { 492 | const params = { 493 | AttributeDefinitions: [{ 494 | AttributeName: 'id', 495 | AttributeType: 'S', 496 | }], 497 | KeySchema: [{ 498 | AttributeName: 'id', 499 | KeyType: 'HASH', 500 | }], 501 | BillingMode: 'PAY_PER_REQUEST', 502 | TableName: tableName, 503 | } 504 | 505 | await dynamoDb.createTable(params).promise() 506 | 507 | await dynamoDb.waitFor('tableExists', { 508 | TableName: tableName, 509 | }).promise() 510 | }, 60 * 1000) 511 | ``` 512 | 513 | We can create a DynamoDB table in the "beforeAll" section using the following code. Just keep in mind that creating can take some time, so increase the timeout to 60 seconds. 514 | 515 | --- 516 | 517 | (font-size: 24) 518 | 519 | ```typescript 520 | afterAll(async () => { 521 | await dynamoDb.deleteTable({ 522 | TableName: tableName, 523 | }).promise() 524 | 525 | await dynamoDb.waitFor('tableNotExists', { 526 | TableName: tableName, 527 | }).promise() 528 | }, 60 * 1000) 529 | ``` 530 | 531 | Destroy the table after the test suite is done with the following code. 532 | 533 | --- 534 | 535 | ```md 536 | https://github.com/serverlesspub/five-minutes-serverless 537 | ``` 538 | 539 | The full example is available on the following Github repository. 540 | 541 | (duration: 5) 542 | 543 | --- 544 | 545 | (font-size: 32) 546 | 547 | ```md 548 | **Credits:** 549 | 550 | - I built this video using Video Puppet 551 | - I made diagrams using SimpleDiagrams 4 552 | 553 | For source code, visit the following repository: 554 | 555 | [https://github.com/serverlesspub/five-minutes-serverless](https://github.com/serverlesspub/five-minutes-serverless) 556 | 557 | Video by @slobodan_ 558 | ``` 559 | 560 | Thanks for watching the first episode of Five Minutes Serverless! 561 | 562 | (pause: 1) 563 | 564 | Feel free to send me feedback or questions on twitter. 565 | 566 | --- 567 | 568 | ![](outro.png) 569 | 570 | (duration: 4) 571 | -------------------------------------------------------------------------------- /04-api-gateway-cors/source-code/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "project": "tsconfig.json", 14 | "sourceType": "module" 15 | }, 16 | "plugins": [ 17 | "@typescript-eslint", 18 | "@typescript-eslint/tslint" 19 | ], 20 | "rules": { 21 | "@typescript-eslint/array-type": "error", 22 | "@typescript-eslint/ban-types": "off", 23 | "@typescript-eslint/indent": [ 24 | "error", 25 | 2 26 | ], 27 | "@typescript-eslint/interface-name-prefix": "off", 28 | "@typescript-eslint/member-delimiter-style": [ 29 | "error", 30 | { 31 | "multiline": { 32 | "delimiter": "none", 33 | "requireLast": true 34 | }, 35 | "singleline": { 36 | "delimiter": "semi", 37 | "requireLast": false 38 | } 39 | } 40 | ], 41 | "@typescript-eslint/no-explicit-any": "off", 42 | "@typescript-eslint/no-parameter-properties": "off", 43 | "@typescript-eslint/no-use-before-define": "off", 44 | "@typescript-eslint/prefer-for-of": "error", 45 | "@typescript-eslint/prefer-function-type": "error", 46 | "@typescript-eslint/quotes": [ 47 | "error", 48 | "single", 49 | { 50 | "avoidEscape": true 51 | } 52 | ], 53 | "@typescript-eslint/semi": [ 54 | "error", 55 | "never" 56 | ], 57 | "@typescript-eslint/unified-signatures": "error", 58 | "camelcase": "error", 59 | "comma-dangle": [ 60 | "error", 61 | { 62 | "objects": "always-multiline", 63 | "arrays": "always-multiline", 64 | "functions": "never" 65 | } 66 | ], 67 | "complexity": "off", 68 | "constructor-super": "error", 69 | "dot-notation": "error", 70 | "eqeqeq": [ 71 | "error", 72 | "smart" 73 | ], 74 | "guard-for-in": "error", 75 | "id-blacklist": [ 76 | "error", 77 | "any", 78 | "Number", 79 | "number", 80 | "String", 81 | "string", 82 | "Boolean", 83 | "boolean", 84 | "Undefined", 85 | "undefined" 86 | ], 87 | "id-match": "error", 88 | "max-classes-per-file": "off", 89 | "max-len": [ 90 | "error", 91 | { 92 | "code": 180 93 | } 94 | ], 95 | "new-parens": "error", 96 | "no-bitwise": "error", 97 | "no-caller": "error", 98 | "no-cond-assign": "error", 99 | "no-console": "off", 100 | "no-debugger": "error", 101 | "no-empty": "error", 102 | "no-eval": "error", 103 | "no-fallthrough": "off", 104 | "no-invalid-this": "off", 105 | "no-new-wrappers": "error", 106 | "no-shadow": [ 107 | "error", 108 | { 109 | "hoist": "all" 110 | } 111 | ], 112 | "no-throw-literal": "error", 113 | "no-trailing-spaces": "error", 114 | "no-undef-init": "error", 115 | "no-underscore-dangle": "error", 116 | "no-unsafe-finally": "error", 117 | "no-unused-expressions": "error", 118 | "no-unused-labels": "error", 119 | "object-shorthand": "error", 120 | "one-var": [ 121 | "off", 122 | "never" 123 | ], 124 | "radix": "error", 125 | "spaced-comment": "error", 126 | "use-isnan": "error", 127 | "valid-typeof": "off", 128 | "@typescript-eslint/tslint/config": [ 129 | "error", 130 | { 131 | "rules": { 132 | "jsdoc-format": true, 133 | "no-reference-import": true 134 | } 135 | } 136 | ] 137 | } 138 | }; 139 | -------------------------------------------------------------------------------- /04-api-gateway-cors/source-code/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | samconfig.toml 3 | node_modules 4 | info 5 | -------------------------------------------------------------------------------- /04-api-gateway-cors/source-code/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2020 Gojko Adzic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /04-api-gateway-cors/source-code/README.md: -------------------------------------------------------------------------------- 1 | # AWS Serverless Application Model TypeScript template 2 | 3 | A simple AWS SAM template with TypeScript. 4 | 5 | ## Folder structure 6 | 7 | This project has the following folder structure: 8 | 9 | ```bash 10 | . 11 | ├── README.md # This file 12 | ├── build # Build folder 13 | │   └── hello-world # Each function has its own folder 14 | │   ├── lambda.js 15 | │   └── lambda.js.map # And each function has sourcemaps 16 | ├── jest.config.js # Jest configuration 17 | ├── package.json 18 | ├── src # Source code for all functions 19 | │   └── hello-world # Sample function 20 | │   ├── events # Sample event for local invocationn 21 | │   │ └── event.json 22 | │   ├── lambda.ts # Main file 23 | │   ├── lib # Rest of code, including function business logic 24 | │   │ └── main.ts 25 | │   └── tests # Tests for business logic and all important files 26 | │   └── main.test.ts 27 | ├── template.yaml # Main CloudFormation file 28 | ├── webpack.config.js # Webpack config 29 | ├── yarn.lock 30 | └── .eslintrc.js # ESLint config 31 | ``` 32 | 33 | ## Usage 34 | 35 | To use this template, make sure you have the following prerequisites: 36 | 37 | - AWS profile 38 | - AWS SAM installed and configured 39 | - Node.js version 8 or more (version 12 is recommended) 40 | 41 | ### Initialize a new project 42 | 43 | To create a new project using this template, create a new folder, navigate to your new folder in your terminal, and run the following command: 44 | 45 | ```bash 46 | sam init --location gh:serverlesspub/sam-ts 47 | ``` 48 | 49 | This will create a new AWS SAM project with the folder structure explained above. 50 | 51 | ### Build TypeScript 52 | 53 | To build TypeScript, run the following command: 54 | 55 | ```bash 56 | npm run build 57 | ``` 58 | 59 | If you want to build a project and run the webpack bundle analyzer, run the following command: 60 | 61 | ```bash 62 | npm run build-analyze 63 | ``` 64 | 65 | ### Deploy 66 | 67 | To deploy the project, run the following command: 68 | 69 | ```bash 70 | sam deploy --guided 71 | ``` 72 | 73 | This will run an interactive deployment process, save your configuration to the `samconfig.toml` file, and deploy the project to your AWS account. 74 | 75 | _NOTE: The `samconfig.toml` file is on git ignore list._ 76 | 77 | ### Run automated tests 78 | 79 | To run Jest tests, use the following command: 80 | 81 | ```bash 82 | npm run test 83 | ``` 84 | 85 | This command will run ESLint, and if there are no linting issues, it'll run Jest tests. 86 | 87 | If you want to run ESLint without tests, use the following command: 88 | 89 | ```bash 90 | npm run lint 91 | ``` 92 | 93 | ### Test and debug using SAM Local 94 | 95 | TBA 96 | 97 | ## License 98 | 99 | MIT, see [LICENSE](LICENSE). -------------------------------------------------------------------------------- /04-api-gateway-cors/source-code/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [ 3 | "./src" 4 | ], 5 | transform: { 6 | "^.+\\.tsx?$": "ts-jest" 7 | } 8 | } -------------------------------------------------------------------------------- /04-api-gateway-cors/source-code/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-sam-ts-template", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "watch": "NODE_ENV=dev webpack --watch --mode=development", 8 | "build": "NODE_ENV=prod BUNDLE_ANALYZER=false webpack --mode=production --progress", 9 | "build-analyze": "NODE_ENV=prod BUNDLE_ANALYZER=true webpack --mode=production --progress", 10 | "lint": "eslint -c .eslintrc.js --ext ts src", 11 | "pretest": "npm run lint", 12 | "test": "jest", 13 | "coverage": "jest --coverage" 14 | }, 15 | "keywords": [], 16 | "author": "Slobodan Stojanović (http://slobodan.me)", 17 | "license": "MIT", 18 | "dependencies": { 19 | "@types/aws-lambda": "^8.10.46", 20 | "@types/node": "^13.9.1", 21 | "aws-lambda": "^1.0.5", 22 | "aws-sdk": "^2.639.0", 23 | "aws-xray-sdk": "^2.5.0", 24 | "source-map-support": "^0.5.16", 25 | "tslint": "^6.1.0" 26 | }, 27 | "devDependencies": { 28 | "@types/jest": "^25.1.4", 29 | "@typescript-eslint/eslint-plugin": "^2.23.0", 30 | "@typescript-eslint/eslint-plugin-tslint": "^2.23.0", 31 | "@typescript-eslint/parser": "^2.23.0", 32 | "eslint": "^6.8.0", 33 | "eslint-plugin-prefer-arrow": "^1.1.7", 34 | "eslint-plugin-prefer-arrow-functions": "^3.0.1", 35 | "jest": "^25.1.0", 36 | "source-map-loader": "^0.2.4", 37 | "speed-measure-webpack-plugin": "^1.3.1", 38 | "ts-jest": "^25.2.1", 39 | "ts-loader": "^6.2.1", 40 | "ts-node": "^8.6.2", 41 | "typescript": "^3.8.3", 42 | "webpack": "^4.42.0", 43 | "webpack-bundle-analyzer": "^3.6.1", 44 | "webpack-cli": "^3.3.11" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /04-api-gateway-cors/source-code/src/hello-world/lambda.ts: -------------------------------------------------------------------------------- 1 | // Allow CloudWatch to read source maps 2 | import 'source-map-support/register' 3 | 4 | // You can import event types from @types/aws-lambda 5 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda' 6 | 7 | export async function handler(event: APIGatewayProxyEvent): Promise { 8 | const body = event.body ? JSON.parse(event.body) : {} 9 | const responseStatusCode = body.statusCode || 200 10 | const responseBody = body.response || { 11 | hello: 'world' 12 | } 13 | 14 | if (responseStatusCode >= 400) { 15 | throw new Error(responseBody) 16 | } 17 | 18 | return { 19 | statusCode: responseStatusCode, 20 | headers: { 21 | 'Access-Control-Allow-Origin': '*', 22 | 'Content-Type': 'application/json' 23 | }, 24 | body: JSON.stringify(responseBody) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /04-api-gateway-cors/source-code/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Vacation Tracker API 4 | 5 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 6 | Globals: 7 | Function: 8 | Runtime: nodejs12.x 9 | Tracing: Active 10 | 11 | Resources: 12 | Api: 13 | Type: AWS::Serverless::Api 14 | Properties: 15 | StageName: prod 16 | Cors: 17 | AllowHeaders: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" 18 | AllowOrigin: "'*'" 19 | MaxAge: "'3600'" 20 | AllowMethods: "'OPTIONS,POST,GET,PUT,DELETE'" 21 | GatewayResponses: 22 | DEFAULT_4xx: 23 | ResponseParameters: 24 | Headers: 25 | Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" 26 | Access-Control-Allow-Origin: "'*'" 27 | Access-Control-Allow-Methods: "'OPTIONS,POST,GET,PUT,DELETE'" 28 | DEFAULT_5xx: 29 | ResponseParameters: 30 | Headers: 31 | Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" 32 | Access-Control-Allow-Origin: "'*'" 33 | Access-Control-Allow-Methods: "'OPTIONS,POST,GET,PUT,DELETE'" 34 | 35 | HelloWorld: 36 | Type: AWS::Serverless::Function 37 | Properties: 38 | CodeUri: build/hello-world 39 | Handler: lambda.handler 40 | Events: 41 | PostResource: 42 | Type: Api 43 | Properties: 44 | Path: / 45 | Method: POST 46 | RestApiId: !Ref Api 47 | 48 | Outputs: 49 | FunctionLogicalId: 50 | Description: Function logical ID 51 | Value: !Ref HelloWorld 52 | 53 | FunctionArn: 54 | Description: Function ARN 55 | Value: !GetAtt HelloWorld.Arn 56 | 57 | ApiUrl: 58 | Description: An API URL 59 | Value: ! 'https://${Api}.execute-api.${AWS::Region}.amazonaws.com/prod/' 60 | -------------------------------------------------------------------------------- /04-api-gateway-cors/source-code/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*.ts" 4 | ], 5 | "compilerOptions": { 6 | "incremental": true, 7 | "allowJs": true, 8 | "target": "es2017", 9 | "module": "commonjs", 10 | "strict": true, 11 | "baseUrl": "./", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "types": [ 16 | "node", 17 | "jest" 18 | ], 19 | "esModuleInterop": true, 20 | "inlineSourceMap": true, 21 | "resolveJsonModule": true 22 | } 23 | } -------------------------------------------------------------------------------- /04-api-gateway-cors/source-code/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const glob = require('glob') 3 | const webpack = require('webpack') 4 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 5 | const SpeedMeasurePlugin = require('speed-measure-webpack-plugin') 6 | const smp = new SpeedMeasurePlugin() 7 | 8 | // Credits: https://hackernoon.com/webpack-creating-dynamically-named-outputs-for-wildcarded-entry-files-9241f596b065 9 | const entryArray = glob.sync('./src/**/lambda.ts') 10 | const entryObject = entryArray.reduce((acc, item) => { 11 | let name = path.dirname(item.replace('./src/', '')) 12 | // conforms with Webpack entry API 13 | // Example: { test: './src/test/lambda.ts' } 14 | acc[name] = item 15 | return acc 16 | }, {}) 17 | 18 | const config = smp.wrap({ 19 | entry: entryObject, 20 | devtool: 'source-map', 21 | target: 'node', 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.tsx?$/, 26 | use: [ 27 | { 28 | loader: 'ts-loader', 29 | options: { 30 | transpileOnly: true, 31 | experimentalWatchApi: true 32 | } 33 | } 34 | ], 35 | exclude: /node_modules/ 36 | } 37 | ] 38 | }, 39 | optimization: { 40 | minimize: false 41 | }, 42 | plugins: [new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)], 43 | resolve: { 44 | extensions: ['.ts', '.js'], 45 | symlinks: false, 46 | cacheWithContext: false 47 | }, 48 | // Output directive will generate build//lambda.js 49 | output: { 50 | filename: '[name]/lambda.js', 51 | path: path.resolve(__dirname, 'build'), 52 | devtoolModuleFilenameTemplate: '[absolute-resource-path]', 53 | // credits to Rich Buggy!!! 54 | libraryTarget: 'commonjs2' 55 | } 56 | }) 57 | 58 | if (process.env.BUNDLE_ANALYZER && process.env.BUNDLE_ANALYZER === 'true') { 59 | config.plugins.push(new BundleAnalyzerPlugin({ 60 | analyzerMode: 'static', 61 | reportFilename: '../info/bundle-report.html', 62 | openAnalyzer: false 63 | })) 64 | } 65 | 66 | module.exports = config 67 | -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/01-whats-cors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/01-whats-cors.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/02-cors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/02-cors.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/03-cors-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/03-cors-request.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/04-cors-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/04-cors-error.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/05-cors-simple-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/05-cors-simple-request.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/06-cors-preflight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/06-cors-preflight.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/07-cors-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/07-cors-request.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/08-aws-cors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/08-aws-cors.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/09-folder-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/09-folder-structure.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/10-sam-deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/10-sam-deploy.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/11-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/11-browser.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/12-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/12-browser.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/13-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/13-browser.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/14-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/14-browser.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/15-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/15-browser.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/16-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/16-browser.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/episode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/episode.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/intro.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/outro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspub/five-minutes-serverless/eec1d2ddc1c87f4c97bc01bbe2b17bbe7cc5bdda/04-api-gateway-cors/video-source/outro.png -------------------------------------------------------------------------------- /04-api-gateway-cors/video-source/source.md: -------------------------------------------------------------------------------- 1 | --- 2 | size: 720p 3 | subtitles: auto 4 | background: 5 | vendor: uplifting-2 6 | volume: 0.3 7 | --- 8 | 9 | ![](intro.png) 10 | 11 | Welcome to Five Minutes Serverless! 12 | 13 | --- 14 | 15 | ![](episode.png) 16 | 17 | In this episode, we talk about the API Gateway, AWS SAM, and cross-origin resource sharing. 18 | 19 | --- 20 | 21 | (voice: mario) 22 | 23 | ![](01-whats-cors.png) 24 | 25 | What's cross-origin resource sharing, and how does it work? 26 | 27 | --- 28 | 29 | ![](02-cors.png) 30 | 31 | Cross-Origin Resource Sharing, or CORS, allows a web application on one domain to access resources from a different origin. 32 | 33 | --- 34 | 35 | ![](03-cors-request.png) 36 | 37 | For example, if a web application sends a POST request from example.com to external-api.com, that's a cross-origin HTTP request. 38 | 39 | --- 40 | 41 | ![](04-cors-error.png) 42 | 43 | Normally, browsers would block these requests for security reasons. 44 | 45 | --- 46 | 47 | ![](05-cors-simple-request.png) 48 | 49 | To allow these types of calls, the server needs to send CORS headers. 50 | 51 | For simple GET requests, the server response needs to include the Access-Control-Allow-Origin header. 52 | 53 | That could be a specific domain, or an * to allow open access. 54 | 55 | --- 56 | 57 | ![](06-cors-preflight.png) 58 | 59 | More complex calls require a "preflight" validation. 60 | 61 | The browser first sends an OPTIONS request to check if the actual call is safe to send. 62 | 63 | --- 64 | 65 | ![](07-cors-request.png) 66 | 67 | If the preflight validation succeeds, the browser sends the real request. 68 | 69 | --- 70 | 71 | ![](08-aws-cors.png) 72 | 73 | Amazon API Gateway and AWS SAM have full support for CORS. 74 | 75 | Let's create a sample project and see how CORS works with SAM. 76 | 77 | --- 78 | 79 | (font-size: 42) 80 | 81 | ```md 82 | **Prerequisites:** 83 | 84 | - An active AWS account 85 | - AWS SAM installed 86 | - Node.js version 8+ (version 12 is recommended) 87 | ``` 88 | 89 | Before we begin, make sure you have the following prerequisites. 90 | 91 | You also need a basic knowledge of Node and TypeScript. 92 | 93 | --- 94 | 95 | ```bash 96 | mkdir cors-example 97 | cd cors-example 98 | ``` 99 | 100 | Start by creating a new folder and navigating to it using your terminal. We'll call it "cors-example." 101 | 102 | --- 103 | 104 | (font-size: 40) 105 | 106 | ```bash 107 | sam init --location gh:serverlesspub/sam-ts 108 | ``` 109 | 110 | You can initialize an AWS SAM project the same way we did in previous episodes. 111 | 112 | Run the following command from your new folder using your terminal. 113 | 114 | --- 115 | 116 | ![](09-folder-structure.png) 117 | 118 | This command will create a new AWS SAM project with a Hello World function written in TypeScript. 119 | 120 | --- 121 | 122 | (font-size: 32) 123 | 124 | ```yaml 125 | HelloWorld: 126 | Type: AWS::Serverless::Function 127 | Properties: 128 | CodeUri: build/hello-world 129 | Handler: lambda.handler 130 | Events: 131 | PostResource: 132 | Type: Api 133 | Properties: 134 | Path: / 135 | Method: POST 136 | ``` 137 | 138 | Then open your template file and modify the HelloWorld function to add an API Gateway trigger. 139 | 140 | --- 141 | 142 | (font-size: 22) 143 | 144 | ```yaml 145 | ApiUrl: 146 | Description: An API URL 147 | Value: ! 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/' 148 | ``` 149 | 150 | Add an API URL to the Outputs section of your template file. 151 | 152 | --- 153 | 154 | (font-size: 22) 155 | 156 | ```typescript 157 | // Allow CloudWatch to read source maps 158 | import 'source-map-support/register' 159 | 160 | // You can import event types from @types/aws-lambda 161 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda' 162 | 163 | export async function handler(event: APIGatewayProxyEvent): Promise { 164 | try { 165 | const body = event.body ? JSON.parse(event.body) : {} 166 | const responseStatusCode = body.statusCode || 200 167 | const responseBody = body.response || { 168 | hello: 'world' 169 | } 170 | 171 | return { 172 | statusCode: responseStatusCode, 173 | body: JSON.stringify(responseBody) 174 | } 175 | } catch(err) { 176 | return { 177 | statusCode: 400, 178 | body: JSON.stringify({ 179 | error: err.toString() 180 | }) 181 | } 182 | } 183 | } 184 | ``` 185 | 186 | Save the template, and modify the lambda.ts file of the Hello World function. This file is located in the source folder of our project. 187 | 188 | In our handler function, we'll return a status code and body passed in the POST request body. 189 | 190 | --- 191 | 192 | ```bash 193 | npm install 194 | npm run build 195 | sam deploy --guided 196 | ``` 197 | 198 | Build and deploy this project to confirm that everything works as expected. To do so, run the commands you see on your screen. 199 | 200 | --- 201 | 202 | ![](10-sam-deploy.png) 203 | 204 | When the deployment finishes, SAM will output our API URL. 205 | 206 | --- 207 | 208 | ![](11-browser.png) 209 | 210 | To test CORS, open your browser, visit any website, and open the developer tools. 211 | 212 | Send a post request from the console using the fetch API, and you'll see the CORS error. 213 | 214 | --- 215 | 216 | (font-size: 19) 217 | 218 | ```yaml 219 | Api: 220 | Type: AWS::Serverless::Api 221 | Properties: 222 | StageName: prod 223 | Cors: 224 | AllowHeaders: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" 225 | AllowOrigin: "'*'" 226 | MaxAge: "'3600'" 227 | AllowMethods: "'OPTIONS,POST,GET,PUT,DELETE'" 228 | ``` 229 | 230 | To enable CORS support, open your template file and replace the implicit Serverless Rest API in your template file, with the explicit AWS::Serverless::API resource. 231 | 232 | Providing the stage name is required for the AWS Serverless API, you can name your stage "prod" or anything else you like. 233 | 234 | --- 235 | 236 | (font-size: 32) 237 | 238 | ```yaml 239 | HelloWorld: 240 | Type: AWS::Serverless::Function 241 | Properties: 242 | CodeUri: build/hello-world 243 | Handler: lambda.handler 244 | Events: 245 | PostResource: 246 | Type: Api 247 | Properties: 248 | Path: / 249 | Method: POST 250 | RestApiId: !Ref Api 251 | ``` 252 | 253 | Then update the Hello World function by referencing the new API with its trigger. 254 | 255 | --- 256 | 257 | (font-size: 24) 258 | 259 | ```yaml 260 | ApiUrl: 261 | Description: An API URL 262 | Value: ! 'https://${Api}.execute-api.${AWS::Region}.amazonaws.com/prod/' 263 | ``` 264 | 265 | And update the outputs to print the new API URL. 266 | 267 | --- 268 | 269 | ![](12-browser.png) 270 | 271 | If you redeploy and test the app, you'll see the same CORS error in your browser. 272 | 273 | To fix this problem, we need to add additional headers to the Hello World function response. 274 | 275 | --- 276 | 277 | (font-size: 40) 278 | 279 | ```typescript 280 | return { 281 | statusCode: responseStatusCode, 282 | headers: { 283 | 'Access-Control-Allow-Origin': '*', 284 | 'Content-Type': 'application/json' 285 | }, 286 | body: JSON.stringify(responseBody) 287 | } 288 | ``` 289 | 290 | Open the lambda.ts file of Hello World function, and add headers to the object handler function returns. 291 | 292 | The handler function needs to return access control allow origin header to enable the CORS support. 293 | 294 | --- 295 | 296 | ```bash 297 | npm run build 298 | sam deploy --guided 299 | ``` 300 | 301 | Build and deploy the project. 302 | 303 | --- 304 | 305 | ![](13-browser.png) 306 | 307 | Then send the another HTTP request, and you'll see that it works now! 308 | 309 | --- 310 | 311 | ![](14-browser.png) 312 | 313 | The CORS configuration will work even if you return the error status code from your Lambda function. 314 | 315 | But will it work in case of unhandled errors? 316 | 317 | --- 318 | 319 | (font-size: 21) 320 | 321 | ```typescript 322 | // Allow CloudWatch to read source maps 323 | import 'source-map-support/register' 324 | 325 | // You can import event types from @types/aws-lambda 326 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda' 327 | 328 | export async function handler(event: APIGatewayProxyEvent): Promise { 329 | const body = event.body ? JSON.parse(event.body) : {} 330 | const responseStatusCode = body.statusCode || 200 331 | const responseBody = body.response || { 332 | hello: 'world' 333 | } 334 | 335 | if (responseStatusCode >= 400) { 336 | throw new Error(responseBody) 337 | } 338 | 339 | return { 340 | statusCode: responseStatusCode, 341 | headers: { 342 | 'Access-Control-Allow-Origin': '*', 343 | 'Content-Type': 'application/json' 344 | }, 345 | body: JSON.stringify(responseBody) 346 | } 347 | } 348 | ``` 349 | 350 | Let's test this by updating the Hello World function to throw an error if the status code is 400 or larger. 351 | 352 | --- 353 | 354 | ```bash 355 | npm run build 356 | sam deploy --guided 357 | ``` 358 | 359 | Build and deploy the project again. 360 | 361 | --- 362 | 363 | ![](15-browser.png) 364 | 365 | Send a request, and you'll see the good old CORS error. 366 | 367 | By default, API Gateway adds CORS support for success calls only. 368 | 369 | If functions or authorizers throw an error, your API will not add CORS headers to these responses. 370 | 371 | --- 372 | 373 | (font-size: 20) 374 | 375 | ```yaml 376 | Api: 377 | Type: AWS::Serverless::Api 378 | Properties: 379 | StageName: prod 380 | Cors: 381 | AllowHeaders: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" 382 | AllowOrigin: "'*'" 383 | MaxAge: "'3600'" 384 | AllowMethods: "'OPTIONS,POST,GET,PUT,DELETE'" 385 | GatewayResponses: 386 | DEFAULT_4xx: 387 | ResponseParameters: 388 | Headers: 389 | Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" 390 | Access-Control-Allow-Origin: "'*'" 391 | Access-Control-Allow-Methods: "'OPTIONS,POST,GET,PUT,DELETE'" 392 | DEFAULT_5xx: 393 | ResponseParameters: 394 | Headers: 395 | Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" 396 | Access-Control-Allow-Origin: "'*'" 397 | Access-Control-Allow-Methods: "'OPTIONS,POST,GET,PUT,DELETE'" 398 | ``` 399 | 400 | To enable CORS for function and API errors, you need to add Gateway Responses to your API Gateway resource. 401 | 402 | Open the template file and update your API with the following code. 403 | 404 | Then redeploy your application and send another test request. 405 | 406 | --- 407 | 408 | ![](16-browser.png) 409 | 410 | No more CORS error! 411 | 412 | (pause: 2) 413 | 414 | The Gateway Responses allow you to customize the response for different errors, such as ACCESS DENIED, AUTHORIZER FAILURE, and many others. 415 | 416 | --- 417 | 418 | (font-size: 24) 419 | 420 | ```md 421 | https://docs.aws.amazon.com/apigateway/latest/developerguide/supported-gateway-response-types.html 422 | ``` 423 | 424 | You can see the full list of Gateway Response types on the URL you see on the screen. 425 | 426 | --- 427 | 428 | (font-size: 32) 429 | 430 | ```md 431 | https://github.com/serverlesspub/five-minutes-serverless 432 | ``` 433 | 434 | The full example is available on the following Github repository. 435 | 436 | (duration: 5) 437 | 438 | --- 439 | 440 | (font-size: 32) 441 | 442 | ```md 443 | **Credits:** 444 | 445 | - I built this video using Video Puppet 446 | - I made diagrams using SimpleDiagrams 4 447 | - Thanks to my friends Aleksandar (@simalexan) and Gojko (@gojkoadzic) for helping me with this video 448 | 449 | For source code, visit the following repository: 450 | 451 | [https://github.com/serverlesspub/five-minutes-serverless](https://github.com/serverlesspub/five-minutes-serverless) 452 | 453 | Video by @slobodan_ 454 | ``` 455 | 456 | Thanks for watching the first episode of Five Minutes Serverless! 457 | 458 | (pause: 1) 459 | 460 | Feel free to send me feedback or questions on twitter. 461 | 462 | --- 463 | 464 | ![](outro.png) 465 | 466 | (duration: 4) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Five Minutes Serverless 2 | 3 | Short serverless video tutorials. 4 | 5 | ## Episodes 6 | 7 | 1. [Handle Stripe Events using AWS SAM, EventBridge and TypeScript](./01-stripe-events) 8 | 2. [Testing Serverless Applications - part 1](./02-testing-serverless-apps) 9 | 3. [Testing Serverless Applications using Hexagonal Architecture - part 2](./03-testing-serverless-apps-part-2) 10 | 11 | --------------------------------------------------------------------------------