├── .gitignore ├── README.md ├── handler.js ├── package-lock.json ├── package.json ├── serverless.yml └── test ├── delete_sample.json ├── form_sample.json ├── get_sample.json ├── post_sample.json └── put_sample.json /.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless API Sample Project 2 | 3 | This is a sample [Serverless](https://serverless.com) project that creates a serverless API using the [Serverless](https://serverless.com) framework and [Lambda API](https://github.com/jeremydaly/lambda-api), a lightweight web framework for your serverless applications. 4 | 5 | This project is a companion to the post [How To: Build a Serverless API with Serverless, AWS Lambda and lambda-api](https://www.jeremydaly.com/build-serverless-api-serverless-aws-lambda-lambda-api) at [JeremyDaly.com](https://www.jeremydaly.com). 6 | 7 | This project is free to use as a starting point for building your Serverless APIs. 8 | 9 | ## Requirements 10 | This project requires the installation of the [Serverless](https://serverless.com) framework: 11 | 12 | ```bash 13 | npm install -g serverless 14 | ``` 15 | More details at: https://serverless.com/learn/quick-start/ 16 | 17 | This project is also dependent on [Lambda API](https://github.com/jeremydaly/lambda-api) and [serverless-stage-manager](https://github.com/jeremydaly/serverless-stage-manager). Both can be installed by running the following in the cloned project folder: 18 | 19 | ```bash 20 | npm install 21 | ``` 22 | -------------------------------------------------------------------------------- /handler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Sample serverless API using Serverless framework and lambda-api 5 | * @author Jeremy Daly 6 | * @version 1.0.0 7 | * @license MIT 8 | */ 9 | 10 | 11 | // Require and init API router module 12 | const app = require('lambda-api')({ version: 'v1.0', base: 'v1' }) 13 | 14 | 15 | //----------------------------------------------------------------------------// 16 | // Define Middleware 17 | //----------------------------------------------------------------------------// 18 | 19 | // Add CORS Middleware 20 | app.use((req,res,next) => { 21 | 22 | // Add default CORS headers for every request 23 | res.cors() 24 | 25 | // Call next to continue processing 26 | next() 27 | }) 28 | 29 | 30 | // Add Authorization Middleware 31 | app.use((req,res,next) => { 32 | 33 | // Check for Authorization Bearer token 34 | if (req.auth.type === 'Bearer') { 35 | // Get the Bearer token value 36 | let token = req.auth.value 37 | // Set the token in the request scope 38 | req.token = token 39 | // Do some checking here to make sure it is valid (set an auth flag) 40 | req.auth = true 41 | } 42 | 43 | // Call next to continue processing 44 | next() 45 | }) 46 | 47 | //----------------------------------------------------------------------------// 48 | // Build API routes 49 | //----------------------------------------------------------------------------// 50 | 51 | // Get 52 | app.get('/posts', (req,res) => { 53 | // Send the response 54 | res.status(200).json({ 55 | status: 'ok', 56 | version: req.version, 57 | auth: req.auth, 58 | body: req.body, 59 | query: req.query 60 | }) 61 | }) 62 | 63 | // Post 64 | app.post('/posts', (req,res) => { 65 | // Send the response 66 | res.status(200).json({ 67 | status: 'ok', 68 | version: req.version, 69 | auth: req.auth, 70 | body: req.body, 71 | query: req.query 72 | }) 73 | }) 74 | 75 | // Put 76 | app.put('/posts/:post_id', (req,res) => { 77 | // Send the response 78 | res.status(200).json({ 79 | status: 'ok', 80 | version: req.version, 81 | auth: req.auth, 82 | body: req.body, 83 | query: req.query, 84 | params: req.params 85 | }) 86 | }) 87 | 88 | 89 | // Delete 90 | app.delete('/posts/:post_id', (req,res) => { 91 | // Send the response 92 | res.status(200).json({ 93 | status: 'ok', 94 | version: req.version, 95 | auth: req.auth, 96 | params: req.params 97 | }) 98 | }) 99 | 100 | 101 | // Default Options for CORS preflight 102 | app.options('/*', (req,res) => { 103 | res.status(200).json({}) 104 | }) 105 | 106 | 107 | 108 | //----------------------------------------------------------------------------// 109 | // Main router handler 110 | //----------------------------------------------------------------------------// 111 | module.exports.router = (event, context, callback) => { 112 | 113 | // !!!IMPORTANT: Set this flag to false, otherwise the lambda function 114 | // won't quit until all DB connections are closed, which is not good 115 | // if you want to freeze and reuse these connections 116 | context.callbackWaitsForEmptyEventLoop = false 117 | 118 | // Run the request 119 | app.run(event,context,callback) 120 | 121 | } // end router handler 122 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-api-sample", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "lambda-api": { 8 | "version": "0.6.0", 9 | "resolved": "https://registry.npmjs.org/lambda-api/-/lambda-api-0.6.0.tgz", 10 | "integrity": "sha512-XjFH9BihRoxb5RBsvrkNUdpgL2i1X9WNW/ICzIlUok3Vla1CW1ieMx3qB6l071ggMWwLRVxKRyeXIFNznvWHZw==" 11 | }, 12 | "serverless-stage-manager": { 13 | "version": "1.0.5", 14 | "resolved": "https://registry.npmjs.org/serverless-stage-manager/-/serverless-stage-manager-1.0.5.tgz", 15 | "integrity": "sha1-KPnYwXQtlQRFZOge8GoZET8eBk0=" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-api-sample", 3 | "version": "1.0.0", 4 | "description": "Sample Serverless API using Lambda API", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/jeremydaly/serverless-api-sample" 12 | }, 13 | "keywords": [ 14 | "serverless", 15 | "api", 16 | "lambda", 17 | "aws", 18 | "restful" 19 | ], 20 | "author": "Jeremy Daly ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/jeremydaly/serverless-api-sample/issues" 24 | }, 25 | "homepage": "https://github.com/jeremydaly/serverless-api-sample#readme", 26 | "dependencies": { 27 | "lambda-api": "^0.6.0", 28 | "serverless-stage-manager": "^1.0.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-api-sample 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs8.10 6 | stage: ${opt:stage,'dev'} 7 | region: us-east-1 8 | profile: 9 | iamRoleStatements: # IAM Role Permissions 10 | - Effect: "Allow" 11 | Action: 12 | - "logs:CreateLogGroup" 13 | - "logs:CreateLogStream" 14 | - "logs:PutLogEvents" 15 | Resource: "*" 16 | - Effect: "Allow" 17 | Action: 18 | - "s3:ListBucket" 19 | Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] } 20 | - Effect: "Allow" 21 | Action: 22 | - "s3:PutObject" 23 | Resource: 24 | Fn::Join: 25 | - "" 26 | - - "arn:aws:s3:::" 27 | - "Ref" : "ServerlessDeploymentBucket" 28 | - "/*" 29 | 30 | 31 | # Custom variables 32 | custom: 33 | stages: # for stage manager 34 | - dev 35 | - staging 36 | - prod 37 | 38 | # Plugins 39 | plugins: 40 | - serverless-stage-manager 41 | 42 | # Functions 43 | functions: 44 | serverless-api-sample: 45 | name: ${self:service}-${self:provider.stage}-serverless-api-sample 46 | handler: handler.router 47 | timeout: 30 48 | events: 49 | - http: 50 | path: 'v1/{proxy+}' 51 | method: any 52 | -------------------------------------------------------------------------------- /test/delete_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/v1/posts/123", 3 | "path": "/v1/posts/123", 4 | "httpMethod": "DELETE", 5 | "headers": { 6 | "Authorization": "Bearer ...", 7 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 8 | "Accept-Encoding": "gzip, deflate", 9 | "Accept-Language": "en-us", 10 | "cache-control": "max-age=0", 11 | "CloudFront-Forwarded-Proto": "https", 12 | "CloudFront-Is-Desktop-Viewer": "true", 13 | "CloudFront-Is-Mobile-Viewer": "false", 14 | "CloudFront-Is-SmartTV-Viewer": "false", 15 | "CloudFront-Is-Tablet-Viewer": "false", 16 | "CloudFront-Viewer-Country": "US", 17 | "Cookie": "...", 18 | "Host": "...", 19 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) ...", 20 | "Via": "2.0 ... (CloudFront)", 21 | "X-Amz-Cf-Id": "...", 22 | "X-Amzn-Trace-Id": "...", 23 | "X-Forwarded-For": "xxx.xxx.xxx.xxx", 24 | "X-Forwarded-Port": "443", 25 | "X-Forwarded-Proto": "https" 26 | }, 27 | "queryStringParameters": {}, 28 | "stageVariables": null, 29 | "requestContext": { 30 | "accountId": "...", 31 | "resourceId": "...", 32 | "stage": "prod", 33 | "requestId": "...", 34 | "identity": { 35 | "cognitoIdentityPoolId": null, 36 | "accountId": null, 37 | "cognitoIdentityId": null, 38 | "caller": null, 39 | "apiKey": null, 40 | "sourceIp": "xxx.xxx.xxx.xxx", 41 | "accessKey": null, 42 | "cognitoAuthenticationType": null, 43 | "cognitoAuthenticationProvider": null, 44 | "userArn": null, 45 | "userAgent": "...", 46 | "user": null 47 | }, 48 | "resourcePath": "/v1/posts/123", 49 | "httpMethod": "DELETE", 50 | "apiId": "..." 51 | }, 52 | "body": null, 53 | "isBase64Encoded": false 54 | } 55 | -------------------------------------------------------------------------------- /test/form_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/v1/posts", 3 | "path": "/v1/posts", 4 | "httpMethod": "POST", 5 | "headers": { 6 | "Authorization": "Bearer ...", 7 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 8 | "Accept-Encoding": "gzip, deflate", 9 | "Accept-Language": "en-us", 10 | "cache-control": "max-age=0", 11 | "CloudFront-Forwarded-Proto": "https", 12 | "CloudFront-Is-Desktop-Viewer": "true", 13 | "CloudFront-Is-Mobile-Viewer": "false", 14 | "CloudFront-Is-SmartTV-Viewer": "false", 15 | "CloudFront-Is-Tablet-Viewer": "false", 16 | "CloudFront-Viewer-Country": "US", 17 | "Content-Type": "application/x-www-form-urlencoded", 18 | "Cookie": "...", 19 | "Host": "...", 20 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) ...", 21 | "Via": "2.0 ... (CloudFront)", 22 | "X-Amz-Cf-Id": "...", 23 | "X-Amzn-Trace-Id": "...", 24 | "X-Forwarded-For": "xxx.xxx.xxx.xxx", 25 | "X-Forwarded-Port": "443", 26 | "X-Forwarded-Proto": "https" 27 | }, 28 | "queryStringParameters": { 29 | "qs1": "q1" 30 | }, 31 | "stageVariables": null, 32 | "requestContext": { 33 | "accountId": "...", 34 | "resourceId": "...", 35 | "stage": "prod", 36 | "requestId": "...", 37 | "identity": { 38 | "cognitoIdentityPoolId": null, 39 | "accountId": null, 40 | "cognitoIdentityId": null, 41 | "caller": null, 42 | "apiKey": null, 43 | "sourceIp": "xxx.xxx.xxx.xxx", 44 | "accessKey": null, 45 | "cognitoAuthenticationType": null, 46 | "cognitoAuthenticationProvider": null, 47 | "userArn": null, 48 | "userAgent": "...", 49 | "user": null 50 | }, 51 | "resourcePath": "/v1/posts", 52 | "httpMethod": "POST", 53 | "apiId": "..." 54 | }, 55 | "body": "test=true&form=test123", 56 | "isBase64Encoded": false 57 | } 58 | -------------------------------------------------------------------------------- /test/get_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/v1/posts", 3 | "path": "/v1/posts", 4 | "httpMethod": "GET", 5 | "headers": { 6 | "Authorization": "Bearer ...", 7 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 8 | "Accept-Encoding": "gzip, deflate", 9 | "Accept-Language": "en-us", 10 | "cache-control": "max-age=0", 11 | "CloudFront-Forwarded-Proto": "https", 12 | "CloudFront-Is-Desktop-Viewer": "true", 13 | "CloudFront-Is-Mobile-Viewer": "false", 14 | "CloudFront-Is-SmartTV-Viewer": "false", 15 | "CloudFront-Is-Tablet-Viewer": "false", 16 | "CloudFront-Viewer-Country": "US", 17 | "Cookie": "...", 18 | "Host": "...", 19 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) ...", 20 | "Via": "2.0 ... (CloudFront)", 21 | "X-Amz-Cf-Id": "...", 22 | "X-Amzn-Trace-Id": "...", 23 | "X-Forwarded-For": "xxx.xxx.xxx.xxx", 24 | "X-Forwarded-Port": "443", 25 | "X-Forwarded-Proto": "https" 26 | }, 27 | "queryStringParameters": { 28 | "qs1": "q1" 29 | }, 30 | "stageVariables": null, 31 | "requestContext": { 32 | "accountId": "...", 33 | "resourceId": "...", 34 | "stage": "prod", 35 | "requestId": "...", 36 | "identity": { 37 | "cognitoIdentityPoolId": null, 38 | "accountId": null, 39 | "cognitoIdentityId": null, 40 | "caller": null, 41 | "apiKey": null, 42 | "sourceIp": "xxx.xxx.xxx.xxx", 43 | "accessKey": null, 44 | "cognitoAuthenticationType": null, 45 | "cognitoAuthenticationProvider": null, 46 | "userArn": null, 47 | "userAgent": "...", 48 | "user": null 49 | }, 50 | "resourcePath": "/v1/posts", 51 | "httpMethod": "GET", 52 | "apiId": "..." 53 | }, 54 | "body": null, 55 | "isBase64Encoded": false 56 | } 57 | -------------------------------------------------------------------------------- /test/post_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/v1/posts", 3 | "path": "/v1/posts", 4 | "httpMethod": "POST", 5 | "headers": { 6 | "Authorization": "Bearer ...", 7 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 8 | "Accept-Encoding": "gzip, deflate", 9 | "Accept-Language": "en-us", 10 | "cache-control": "max-age=0", 11 | "CloudFront-Forwarded-Proto": "https", 12 | "CloudFront-Is-Desktop-Viewer": "true", 13 | "CloudFront-Is-Mobile-Viewer": "false", 14 | "CloudFront-Is-SmartTV-Viewer": "false", 15 | "CloudFront-Is-Tablet-Viewer": "false", 16 | "CloudFront-Viewer-Country": "US", 17 | "Cookie": "...", 18 | "Host": "...", 19 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) ...", 20 | "Via": "2.0 ... (CloudFront)", 21 | "X-Amz-Cf-Id": "...", 22 | "X-Amzn-Trace-Id": "...", 23 | "X-Forwarded-For": "xxx.xxx.xxx.xxx", 24 | "X-Forwarded-Port": "443", 25 | "X-Forwarded-Proto": "https" 26 | }, 27 | "queryStringParameters": { 28 | "qs1": "q1" 29 | }, 30 | "stageVariables": null, 31 | "requestContext": { 32 | "accountId": "...", 33 | "resourceId": "...", 34 | "stage": "prod", 35 | "requestId": "...", 36 | "identity": { 37 | "cognitoIdentityPoolId": null, 38 | "accountId": null, 39 | "cognitoIdentityId": null, 40 | "caller": null, 41 | "apiKey": null, 42 | "sourceIp": "xxx.xxx.xxx.xxx", 43 | "accessKey": null, 44 | "cognitoAuthenticationType": null, 45 | "cognitoAuthenticationProvider": null, 46 | "userArn": null, 47 | "userAgent": "...", 48 | "user": null 49 | }, 50 | "resourcePath": "/v1/posts", 51 | "httpMethod": "POST", 52 | "apiId": "..." 53 | }, 54 | "body": "{ \"test\": true }", 55 | "isBase64Encoded": false 56 | } 57 | -------------------------------------------------------------------------------- /test/put_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/v1/posts/123", 3 | "path": "/v1/posts/123", 4 | "httpMethod": "PUT", 5 | "headers": { 6 | "Authorization": "Bearer ...", 7 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 8 | "Accept-Encoding": "gzip, deflate", 9 | "Accept-Language": "en-us", 10 | "cache-control": "max-age=0", 11 | "CloudFront-Forwarded-Proto": "https", 12 | "CloudFront-Is-Desktop-Viewer": "true", 13 | "CloudFront-Is-Mobile-Viewer": "false", 14 | "CloudFront-Is-SmartTV-Viewer": "false", 15 | "CloudFront-Is-Tablet-Viewer": "false", 16 | "CloudFront-Viewer-Country": "US", 17 | "Cookie": "...", 18 | "Host": "...", 19 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) ...", 20 | "Via": "2.0 ... (CloudFront)", 21 | "X-Amz-Cf-Id": "...", 22 | "X-Amzn-Trace-Id": "...", 23 | "X-Forwarded-For": "xxx.xxx.xxx.xxx", 24 | "X-Forwarded-Port": "443", 25 | "X-Forwarded-Proto": "https" 26 | }, 27 | "queryStringParameters": {}, 28 | "stageVariables": null, 29 | "requestContext": { 30 | "accountId": "...", 31 | "resourceId": "...", 32 | "stage": "prod", 33 | "requestId": "...", 34 | "identity": { 35 | "cognitoIdentityPoolId": null, 36 | "accountId": null, 37 | "cognitoIdentityId": null, 38 | "caller": null, 39 | "apiKey": null, 40 | "sourceIp": "xxx.xxx.xxx.xxx", 41 | "accessKey": null, 42 | "cognitoAuthenticationType": null, 43 | "cognitoAuthenticationProvider": null, 44 | "userArn": null, 45 | "userAgent": "...", 46 | "user": null 47 | }, 48 | "resourcePath": "/v1/posts/123", 49 | "httpMethod": "PUT", 50 | "apiId": "..." 51 | }, 52 | "body": "{ \"test\": true }", 53 | "isBase64Encoded": false 54 | } 55 | --------------------------------------------------------------------------------