├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── src ├── hello.spec.ts └── hello.ts ├── template.yaml ├── tsconfig.json ├── tslint.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # NodeJS 2 | node_modules 3 | dist 4 | 5 | # Python 6 | .venv 7 | __pycache__/ 8 | 9 | # Editor 10 | .vscode 11 | 12 | # Litter 13 | *.log 14 | template.packaged.yaml 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS SAM NodeJS + TypeScript Boilerplate 2 | 3 | This is a quick of example my setup for build AWS SAM applications in TypeScript. 4 | 5 | _Note that I'm not a build-config wizard, this is a configuration that worked for me and that is continually evolving. If you see any mistakes or room for improvement, please create an Issue or Pull Request. **Feedback very welcome!**_ 6 | 7 | ## Features 8 | 9 | * Multiple entries: Each function handler is built into its own entry file. This aims to keep each function as small and discrete as possible, only packaging the logic needed for that particular function. These entry files are determined by searching the `template.yaml` for Resources with a Type of `AWS::Serverless::Function`. 10 | * Uglify output during production builds. 11 | * Tests. 12 | 13 | ## Commands 14 | 15 | * `start`: Watch source code, build on changes 16 | * `build`: Build source code 17 | * `test`: Run tests 18 | * `test -- --watch`: Watch source code, run tests on changes 19 | * `package`: Run `sam package` with relevant `template-file` and `s3-bucket` values 20 | * `deploy`: Run `sam deploy` with relevant `template-file` and `stack-name` values 21 | * `clean`: Wipe-out build directory 22 | 23 | ## Examples 24 | 25 | ### Executing locally 26 | 27 | ```sh 28 | ▶ echo '{"name": "Doug"}' | sam local invoke Hello 29 | 2018-05-28 10:35:18 Reading invoke payload from stdin (you can also pass it from file with --event) 30 | 2018-05-28 10:35:18 Invoking hello.default (nodejs8.10) 31 | 2018-05-28 10:35:18 Found credentials in shared credentials file: ~/.aws/credentials 32 | 33 | Fetching lambci/lambda:nodejs8.10 Docker container image...... 34 | 2018-05-28 10:35:20 Mounting /Users/alukach/Projects/aws-sam-example/dist as /var/task:ro inside runtime container 35 | START RequestId: 03c81c3a-791a-15de-7225-bcdccc01d273 Version: $LATEST 36 | END RequestId: 03c81c3a-791a-15de-7225-bcdccc01d273 37 | REPORT RequestId: 03c81c3a-791a-15de-7225-bcdccc01d273 Duration: 8.71 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 32 MB 38 | 39 | "Hello Doug" 40 | ``` 41 | 42 | ## Resources 43 | 44 | It is strongly recommended that you are familiar with the following: 45 | 46 | * [AWS Serverless Application Model](https://github.com/awslabs/serverless-application-model) 47 | * [AWS SAM CLI](https://github.com/awslabs/aws-sam-cli) 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-sample-project", 3 | "version": "1.0.0", 4 | "description": "", 5 | "typings": "dist/types/index.d.ts", 6 | "scripts": { 7 | "start": "webpack -w", 8 | "test": "jest", 9 | "build": "NODE_ENV=${NODE_ENV:-production} webpack", 10 | "package": "sam package --template-file template.yaml --s3-bucket $npm_package_name-${STAGE:-dev} --output-template-file template.packaged.yaml", 11 | "deploy": "sam deploy --template-file template.packaged.yaml --stack-name $npm_package_name-${STAGE:-dev} --capabilities CAPABILITY_IAM --parameter-override Stage=${STAGE:-dev} ProjectName=$npm_package_name --no-fail-on-empty-changeset", 12 | "clean": "rm -rf -v dist/*" 13 | }, 14 | "repository": {}, 15 | "author": "", 16 | "license": "MIT", 17 | "private": true, 18 | "dependencies": {}, 19 | "devDependencies": { 20 | "@types/aws-lambda": "^8.10.3", 21 | "@types/chai": "^4.1.3", 22 | "@types/jest": "^23.1.4", 23 | "@types/node": "^10.0.4", 24 | "aws-sdk": "^2.234.1", 25 | "jest": "^23.3.0", 26 | "ts-jest": "^23.0.0", 27 | "ts-loader": "^4.2.0", 28 | "ts-node": "^6.0.3", 29 | "tslint": "^5.8.0", 30 | "tslint-config-prettier": "^1.12.0", 31 | "tslint-config-standard": "^7.0.0", 32 | "typescript": "^2.8.3", 33 | "uglifyjs-webpack-plugin": "^1.2.5", 34 | "webpack": "^4.7.0", 35 | "webpack-cli": "^3.1.1", 36 | "yaml-cfn": "^0.2.0" 37 | }, 38 | "jest": { 39 | "transform": { 40 | "^.+\\.tsx?$": "ts-jest" 41 | }, 42 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 43 | "moduleFileExtensions": [ 44 | "ts", 45 | "tsx", 46 | "js", 47 | "jsx", 48 | "json", 49 | "node" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/hello.spec.ts: -------------------------------------------------------------------------------- 1 | import handler, { Event } from './hello'; 2 | 3 | describe('Hello handler', () => { 4 | 5 | const eventBase: Event = { 6 | name: 'World', 7 | }; 8 | 9 | it('should greet user', () => { 10 | return handler(eventBase).then((result) => { 11 | expect(result).toEqual('Hello World'); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/hello.ts: -------------------------------------------------------------------------------- 1 | // If this is to be invoked directly, we can create our own event 2 | export interface Event { 3 | name: string; 4 | } 5 | 6 | // Or if it were to be called via APIGateway or S3, we could specify those events specifically: 7 | // type Event = AWSLambda.APIGatewayEvent | AWSLambda.S3Event; 8 | 9 | // This example demonstrates a NodeJS 8.10 async handler[1], however of course you could use 10 | // the more traditional callback-style handler. 11 | // [1]: https://aws.amazon.com/blogs/compute/node-js-8-10-runtime-now-available-in-aws-lambda/ 12 | export default async (event: Event): Promise => ( 13 | `Hello ${event.name}` 14 | ); 15 | -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | # https://github.com/awslabs/serverless-application-model/blob/develop/versions/2016-10-31.md 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Transform: AWS::Serverless-2016-10-31 4 | 5 | Parameters: 6 | Stage: 7 | Type: String 8 | ProjectName: 9 | Type: String 10 | 11 | Globals: 12 | Function: 13 | Runtime: nodejs8.10 14 | Timeout: 180 15 | Tracing: Active 16 | 17 | Resources: 18 | Hello: 19 | Type: AWS::Serverless::Function 20 | Properties: 21 | Handler: hello.default 22 | CodeUri: ./dist/hello 23 | 24 | ExampleTable: 25 | TableName: !Sub ${ProjectName}-exampletable-${Stage} 26 | Type: AWS::Serverless::SimpleTable 27 | Properties: 28 | PrimaryKey: 29 | Name: id 30 | Type: String 31 | 32 | Outputs: 33 | ExampleTableArn: 34 | Description: Example Table ARN 35 | Value: !GetAtt ExampleTable.Arn 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "lib": [ "es2017" ], 7 | "noImplicitAny": true, 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "outDir": "dist", 11 | }, 12 | "include": [ 13 | "src/**/*", 14 | ], 15 | "exclude": [ 16 | "**/*.spec.*", 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-standard", 4 | "tslint-config-prettier" 5 | ], 6 | "rules": { 7 | "align": [ 8 | true, 9 | "parameters", 10 | "arguments", 11 | "statements" 12 | ], 13 | "ban": false, 14 | "class-name": true, 15 | "comment-format": [ 16 | true, 17 | "check-space" 18 | ], 19 | "curly": true, 20 | "eofline": false, 21 | "forin": true, 22 | "indent": [ true, "spaces" ], 23 | "interface-name": [true, "never-prefix"], 24 | "jsdoc-format": true, 25 | "jsx-no-lambda": false, 26 | "jsx-no-multiline-js": false, 27 | "label-position": true, 28 | "max-line-length": [ true, 120 ], 29 | "member-ordering": [ 30 | true, 31 | { 32 | "order": [ 33 | "public-before-private", 34 | "static-before-instance", 35 | "variables-before-functions" 36 | ] 37 | } 38 | ], 39 | "no-any": true, 40 | "no-arg": true, 41 | "no-bitwise": true, 42 | "no-console": [ 43 | true, 44 | "log", 45 | "error", 46 | "debug", 47 | "info", 48 | "time", 49 | "timeEnd", 50 | "trace" 51 | ], 52 | "no-consecutive-blank-lines": true, 53 | "no-construct": true, 54 | "no-debugger": true, 55 | "no-duplicate-variable": true, 56 | "no-empty": true, 57 | "no-eval": true, 58 | "no-shadowed-variable": true, 59 | "no-string-literal": true, 60 | "no-switch-case-fall-through": true, 61 | "no-trailing-whitespace": false, 62 | "no-unused-expression": true, 63 | "no-use-before-declare": true, 64 | "one-line": [ 65 | true, 66 | "check-catch", 67 | "check-else", 68 | "check-open-brace", 69 | "check-whitespace" 70 | ], 71 | "quotemark": [true, "single", "jsx-double"], 72 | "radix": true, 73 | "semicolon": [true, "always"], 74 | "switch-default": true, 75 | 76 | "trailing-comma": false, 77 | 78 | "triple-equals": [ true, "allow-null-check" ], 79 | "typedef": [ 80 | true, 81 | "parameter", 82 | "property-declaration" 83 | ], 84 | "typedef-whitespace": [ 85 | true, 86 | { 87 | "call-signature": "nospace", 88 | "index-signature": "nospace", 89 | "parameter": "nospace", 90 | "property-declaration": "nospace", 91 | "variable-declaration": "nospace" 92 | } 93 | ], 94 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"], 95 | "whitespace": [ 96 | true, 97 | "check-branch", 98 | "check-decl", 99 | "check-module", 100 | "check-operator", 101 | "check-separator", 102 | "check-type", 103 | "check-typecast" 104 | ] 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { readFileSync } = require('fs'); 3 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 4 | const { yamlParse } = require('yaml-cfn'); 5 | 6 | const conf = { 7 | prodMode: process.env.NODE_ENV === 'production', 8 | templatePath: './template.yaml', 9 | }; 10 | const cfn = yamlParse(readFileSync(conf.templatePath)); 11 | const entries = Object.values(cfn.Resources) 12 | // Find nodejs functions 13 | .filter(v => v.Type === 'AWS::Serverless::Function') 14 | .filter(v => 15 | (v.Properties.Runtime && v.Properties.Runtime.startsWith('nodejs')) || 16 | (!v.Properties.Runtime && cfn.Globals.Function.Runtime) 17 | ) 18 | .map(v => ({ 19 | // Isolate handler src filename 20 | handlerFile: v.Properties.Handler.split('.')[0], 21 | // Build handler dst path 22 | CodeUriDir: v.Properties.CodeUri.split("/") 23 | .filter((x: string) => x !== ".") 24 | .slice(1, -1) 25 | .join("/") 26 | })) 27 | .reduce( 28 | (entries, v) => 29 | Object.assign( 30 | entries, 31 | // Generate {outputPath: inputPath} object 32 | {[`${v.CodeUriDir}/${v.handlerFile}`]: `./src/${v.handlerFile}.ts`} 33 | ), 34 | {} 35 | ); 36 | 37 | console.log(`Building for ${conf.prodMode ? 'production' : 'development'}...`) 38 | 39 | module.exports = { 40 | // http://codys.club/blog/2015/07/04/webpack-create-multiple-bundles-with-entry-points/#sec-3 41 | entry: entries, 42 | target: 'node', 43 | mode: conf.prodMode ? 'production' : 'development', 44 | module: { 45 | rules: [ 46 | { 47 | test: /\.tsx?$/, 48 | use: 'ts-loader', 49 | }, 50 | ] 51 | }, 52 | resolve: { 53 | extensions: [ '.tsx', '.ts', '.js' ] 54 | }, 55 | output: { 56 | path: path.resolve(__dirname, 'dist'), 57 | filename: "[name].js", 58 | libraryTarget: 'commonjs2', 59 | }, 60 | devtool: 'source-map', 61 | plugins: conf.prodMode ? [ 62 | new UglifyJsPlugin({ 63 | parallel: true, 64 | extractComments: true, 65 | sourceMap: true, 66 | }), 67 | ] : [], 68 | }; 69 | --------------------------------------------------------------------------------