├── source-map-install.js ├── src ├── dtos │ ├── updatePostDto.ts │ └── createPostDto.ts ├── models │ └── Post.ts ├── core │ ├── formatJsonResponse.ts │ └── middify.ts ├── database │ ├── services │ │ ├── index.ts │ │ └── postService.ts │ └── db.ts └── functions │ ├── getAllPost.ts │ ├── getPost.ts │ ├── deletePost.ts │ ├── updatePost.ts │ └── createPost.ts ├── .gitignore ├── tsconfig.json ├── README.md ├── webpack.config.js ├── package.json └── serverless.yml /source-map-install.js: -------------------------------------------------------------------------------- 1 | require('source-map-support').install(); 2 | -------------------------------------------------------------------------------- /src/dtos/updatePostDto.ts: -------------------------------------------------------------------------------- 1 | import Post from "../models/Post"; 2 | 3 | interface UpdatePost { 4 | body: Post; 5 | } 6 | 7 | export default UpdatePost; 8 | -------------------------------------------------------------------------------- /src/dtos/createPostDto.ts: -------------------------------------------------------------------------------- 1 | interface CreatePost { 2 | body: { 3 | title: string; 4 | description: string; 5 | }; 6 | } 7 | 8 | export default CreatePost; 9 | -------------------------------------------------------------------------------- /src/models/Post.ts: -------------------------------------------------------------------------------- 1 | interface Post { 2 | postId: string; 3 | title: string; 4 | description: string; 5 | active: boolean; 6 | createdAt: string; 7 | } 8 | 9 | export default Post; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules/ 3 | package-lock.json 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | # Webpack directories 9 | .webpack 10 | 11 | # Dynamodb 12 | .dynamodb -------------------------------------------------------------------------------- /src/core/formatJsonResponse.ts: -------------------------------------------------------------------------------- 1 | const formatJSONResponse = (statusCode: number, response: any): any => { 2 | return { 3 | statusCode, 4 | body: JSON.stringify(response), 5 | }; 6 | }; 7 | 8 | export default formatJSONResponse; 9 | -------------------------------------------------------------------------------- /src/database/services/index.ts: -------------------------------------------------------------------------------- 1 | import createDynamoDBClient from "../db"; 2 | import PostService from "./postService"; 3 | 4 | const { POSTS_TABLE } = process.env; 5 | 6 | const postService = new PostService(createDynamoDBClient(), POSTS_TABLE); 7 | 8 | export default postService; 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es2017" 5 | ], 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "target": "es6", 9 | "outDir": "lib" 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /src/core/middify.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from "aws-lambda"; 2 | import middy from "@middy/core"; 3 | import middyJsonBodyParser from "@middy/http-json-body-parser"; 4 | import cors from "@middy/http-cors"; 5 | 6 | const middify = (handler: Handler) => { 7 | return middy(handler).use(middyJsonBodyParser()).use(cors()); 8 | }; 9 | 10 | export default middify; 11 | -------------------------------------------------------------------------------- /src/database/db.ts: -------------------------------------------------------------------------------- 1 | import * as AWS from "aws-sdk"; 2 | import { DocumentClient } from "aws-sdk/clients/dynamodb"; 3 | 4 | const createDynamoDBClient = (): DocumentClient => { 5 | if (process.env.IS_OFFLINE) { 6 | return new AWS.DynamoDB.DocumentClient({ 7 | region: "localhost", 8 | endpoint: "http://localhost:5000", 9 | }); 10 | } 11 | 12 | return new AWS.DynamoDB.DocumentClient(); 13 | }; 14 | 15 | export default createDynamoDBClient; 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AWS Serverless API 2 | 3 | ### Project Structure 4 | 5 | ``` 6 | aws-serverless-api 7 | ├─ .git 8 | ├─ .gitignore 9 | ├─ package.json 10 | ├─ serverless.yml 11 | ├─ source-map-install.js 12 | ├─ src 13 | │ ├─ core 14 | │ │ ├─ formatJsonResponse.ts 15 | │ │ └─ middify.ts 16 | │ ├─ database 17 | │ │ ├─ db.ts 18 | │ │ └─ services 19 | │ │ ├─ index.ts 20 | │ │ └─ postService.ts 21 | │ ├─ dtos 22 | │ │ ├─ createPostDto.ts 23 | │ │ └─ updatePostDto.ts 24 | │ ├─ functions 25 | │ │ ├─ createPost.ts 26 | │ │ ├─ deletePost.ts 27 | │ │ ├─ getAllPost.ts 28 | │ │ ├─ getPost.ts 29 | │ │ └─ updatePost.ts 30 | │ └─ models 31 | │ └─ Post.ts 32 | ├─ tsconfig.json 33 | └─ webpack.config.js 34 | 35 | ``` 36 | -------------------------------------------------------------------------------- /src/functions/getAllPost.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APIGatewayEvent, 3 | Handler, 4 | Context, 5 | APIGatewayProxyResult, 6 | } from "aws-lambda"; 7 | import middify from "../core/middify"; 8 | import formatJSONResponse from "../core/formatJsonResponse"; 9 | import postService from "../database/services"; 10 | 11 | export const handler: Handler = middify( 12 | async ( 13 | event: APIGatewayEvent, 14 | context: Context 15 | ): Promise => { 16 | try { 17 | const posts = await postService.getAllPosts(); 18 | 19 | return formatJSONResponse(200, posts); 20 | } catch (err) { 21 | return formatJSONResponse(400, err); 22 | } 23 | } 24 | ); 25 | -------------------------------------------------------------------------------- /src/functions/getPost.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APIGatewayEvent, 3 | Handler, 4 | Context, 5 | APIGatewayProxyResult, 6 | } from "aws-lambda"; 7 | import middify from "../core/middify"; 8 | import formatJSONResponse from "../core/formatJsonResponse"; 9 | import postService from "../database/services"; 10 | 11 | export const handler: Handler = middify( 12 | async ( 13 | event: APIGatewayEvent, 14 | context: Context 15 | ): Promise => { 16 | const postId: string = event.pathParameters.postId; 17 | try { 18 | const posts = await postService.getPost(postId); 19 | 20 | return formatJSONResponse(200, posts); 21 | } catch (err) { 22 | return formatJSONResponse(400, err); 23 | } 24 | } 25 | ); 26 | -------------------------------------------------------------------------------- /src/functions/deletePost.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APIGatewayEvent, 3 | Handler, 4 | Context, 5 | APIGatewayProxyResult, 6 | } from "aws-lambda"; 7 | import middify from "../core/middify"; 8 | import formatJSONResponse from "../core/formatJsonResponse"; 9 | import postService from "../database/services"; 10 | 11 | export const handler: Handler = middify( 12 | async ( 13 | event: APIGatewayEvent, 14 | context: Context 15 | ): Promise => { 16 | const postId: string = event.pathParameters.postId; 17 | try { 18 | const posts = await postService.deletePost(postId); 19 | 20 | return formatJSONResponse(200, posts); 21 | } catch (err) { 22 | return formatJSONResponse(400, err); 23 | } 24 | } 25 | ); 26 | -------------------------------------------------------------------------------- /src/functions/updatePost.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APIGatewayEvent, 3 | Handler, 4 | Context, 5 | APIGatewayProxyResult, 6 | } from "aws-lambda"; 7 | import middify from "../core/middify"; 8 | import formatJSONResponse from "../core/formatJsonResponse"; 9 | import postService from "../database/services"; 10 | import UpdatePost from "../dtos/updatePostDto"; 11 | 12 | export const handler: Handler = middify( 13 | async ( 14 | event: APIGatewayEvent & UpdatePost, 15 | context: Context 16 | ): Promise => { 17 | const postId: string = event.pathParameters.postId; 18 | const { body } = event; 19 | try { 20 | const posts = await postService.updatePost(postId, body); 21 | 22 | return formatJSONResponse(200, posts); 23 | } catch (err) { 24 | return formatJSONResponse(400, err); 25 | } 26 | } 27 | ); 28 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const slsw = require('serverless-webpack'); 3 | 4 | const entries = {}; 5 | 6 | Object.keys(slsw.lib.entries).forEach( 7 | key => (entries[key] = ['./source-map-install.js', slsw.lib.entries[key]]) 8 | ); 9 | 10 | module.exports = { 11 | mode: slsw.lib.webpack.isLocal ? 'development' : 'production', 12 | entry: entries, 13 | devtool: 'source-map', 14 | resolve: { 15 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 16 | }, 17 | output: { 18 | libraryTarget: 'commonjs', 19 | path: path.join(__dirname, '.webpack'), 20 | filename: '[name].js', 21 | }, 22 | target: 'node', 23 | module: { 24 | rules: [ 25 | // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader` 26 | { test: /\.tsx?$/, loader: 'ts-loader' }, 27 | ], 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-serverless-api", 3 | "version": "1.0.0", 4 | "description": "Serverless API with AWS Lambda", 5 | "main": "handler.ts", 6 | "scripts": { 7 | "start": "sls offline start", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "dependencies": { 11 | "@middy/core": "^2.2.0", 12 | "@middy/http-cors": "^2.2.0", 13 | "@middy/http-json-body-parser": "^2.2.0", 14 | "aws-lambda": "^0.1.2", 15 | "node": "^12.3.1", 16 | "uuid": "^3.3.2" 17 | }, 18 | "devDependencies": { 19 | "@types/aws-lambda": "8.10.1", 20 | "@types/node": "^8.0.57", 21 | "@types/uuid": "^3.4.4", 22 | "json-schema-to-ts": "^1.6.4", 23 | "serverless-dynamodb-local": "^0.2.39", 24 | "serverless-offline": "^7.0.0", 25 | "serverless-webpack": "^5.1.1", 26 | "source-map-support": "^0.5.6", 27 | "ts-loader": "^4.2.0", 28 | "typescript": "^2.9.2", 29 | "webpack": "^4.5.0" 30 | }, 31 | "author": "Verdotte Aututu", 32 | "license": "MIT" 33 | } 34 | -------------------------------------------------------------------------------- /src/functions/createPost.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APIGatewayEvent, 3 | Handler, 4 | Context, 5 | APIGatewayProxyResult, 6 | } from "aws-lambda"; 7 | import * as uuid from "uuid"; 8 | import middify from "../core/middify"; 9 | import formatJSONResponse from "../core/formatJsonResponse"; 10 | import postService from "../database/services"; 11 | import CreatePost from "../dtos/createPostDto"; 12 | 13 | export const handler: Handler = middify( 14 | async ( 15 | event: APIGatewayEvent & CreatePost, 16 | context: Context 17 | ): Promise => { 18 | const { title, description } = event.body; 19 | 20 | try { 21 | const postId: string = uuid.v4(); 22 | const post = await postService.createPost({ 23 | postId, 24 | title, 25 | description, 26 | active: true, 27 | createdAt: new Date().toISOString(), 28 | }); 29 | 30 | return formatJSONResponse(201, post); 31 | } catch (err) { 32 | return formatJSONResponse(400, err); 33 | } 34 | } 35 | ); 36 | -------------------------------------------------------------------------------- /src/database/services/postService.ts: -------------------------------------------------------------------------------- 1 | import { DocumentClient } from "aws-sdk/clients/dynamodb"; 2 | import Post from "../../models/Post"; 3 | 4 | class PostService { 5 | constructor( 6 | private readonly docClient: DocumentClient, 7 | private readonly tableName: string 8 | ) {} 9 | 10 | async getAllPosts(): Promise { 11 | const result = await this.docClient 12 | .scan({ 13 | TableName: this.tableName, 14 | }) 15 | .promise(); 16 | 17 | return result.Items as Post[]; 18 | } 19 | 20 | async getPost(postId: string): Promise { 21 | const result = await this.docClient 22 | .get({ 23 | TableName: this.tableName, 24 | Key: { postId }, 25 | }) 26 | .promise(); 27 | 28 | return result.Item as Post; 29 | } 30 | 31 | async createPost(post: Post): Promise { 32 | await this.docClient 33 | .put({ 34 | TableName: this.tableName, 35 | Item: post, 36 | }) 37 | .promise(); 38 | 39 | return post; 40 | } 41 | 42 | async updatePost(postId: string, partialPost: Partial): Promise { 43 | const updated = await this.docClient 44 | .update({ 45 | TableName: this.tableName, 46 | Key: { postId }, 47 | UpdateExpression: 48 | "set #title = :title, description = :description, active = :active", 49 | ExpressionAttributeNames: { 50 | "#title": "title", 51 | }, 52 | ExpressionAttributeValues: { 53 | ":title": partialPost.title, 54 | ":description": partialPost.description, 55 | ":active": partialPost.active, 56 | }, 57 | ReturnValues: "ALL_NEW", 58 | }) 59 | .promise(); 60 | 61 | return updated.Attributes as Post; 62 | } 63 | 64 | async deletePost(postId: string) { 65 | return this.docClient 66 | .delete({ 67 | TableName: this.tableName, 68 | Key: { postId }, 69 | }) 70 | .promise(); 71 | } 72 | } 73 | 74 | export default PostService; 75 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: aws-serverless-api 3 | 4 | # Add the serverless-webpack plugin 5 | plugins: 6 | - serverless-webpack 7 | - serverless-dynamodb-local 8 | - serverless-offline 9 | 10 | custom: 11 | webpack: 12 | webpackConfig: ./webpack.config.js 13 | includeModules: true 14 | serverless-offline: 15 | httpPort: 3003 16 | dynamodb: 17 | start: 18 | port: 5000 19 | inMemory: true 20 | migrate: true 21 | stages: 22 | - dev 23 | 24 | provider: 25 | name: aws 26 | runtime: nodejs12.x 27 | stage: ${opt:stage, 'dev'} 28 | region: ${opt:region, 'us-east-2'} 29 | environment: 30 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1 31 | POSTS_TABLE: Posts-${self:provider.stage} 32 | iamRoleStatements: 33 | - Effect: Allow 34 | Action: 35 | - dynamodb:DescribeTable 36 | - dynamodb:Query 37 | - dynamodb:Scan 38 | - dynamodb:GetItem 39 | - dynamodb:PutItem 40 | - dynamodb:DeleteItem 41 | Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.POSTS_TABLE}" 42 | 43 | functions: 44 | createPost: 45 | handler: src/functions/createPost.handler 46 | events: 47 | - http: 48 | method: post 49 | path: create-post 50 | cors: true 51 | getAllPost: 52 | handler: src/functions/getAllPost.handler 53 | events: 54 | - http: 55 | method: get 56 | path: get-post 57 | cors: true 58 | getPost: 59 | handler: src/functions/getPost.handler 60 | events: 61 | - http: 62 | method: get 63 | path: get-post/{postId} 64 | cors: true 65 | updatePost: 66 | handler: src/functions/updatePost.handler 67 | events: 68 | - http: 69 | method: put 70 | path: update-post/{postId} 71 | cors: true 72 | deletePost: 73 | handler: src/functions/deletePost.handler 74 | events: 75 | - http: 76 | method: delete 77 | path: delete-post/{postId} 78 | cors: true 79 | 80 | resources: 81 | Resources: 82 | PostsListTable: 83 | Type: AWS::DynamoDB::Table 84 | Properties: 85 | TableName: ${self:provider.environment.POSTS_TABLE} 86 | AttributeDefinitions: 87 | - AttributeName: postId 88 | AttributeType: S 89 | KeySchema: 90 | - AttributeName: postId 91 | KeyType: HASH 92 | ProvisionedThroughput: 93 | ReadCapacityUnits: 1 94 | WriteCapacityUnits: 1 95 | --------------------------------------------------------------------------------