├── .gitignore ├── package.json ├── LICENSE ├── index.js ├── README.md └── test └── handler.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | #jetbrains folder 40 | .idea 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-lambda", 3 | "version": "1.0.0", 4 | "description": "Support Hapi 17+ applications on Amazon Lambda", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha --reporter spec" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/carbonrobot/hapi-lambda.git" 12 | }, 13 | "keywords": [ 14 | "web", 15 | "api", 16 | "http", 17 | "framework", 18 | "hapi", 19 | "hapijs", 20 | "lambda", 21 | "amazon", 22 | "aws" 23 | ], 24 | "author": "Charles Brown", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/carbonrobot/hapi-lambda/issues" 28 | }, 29 | "homepage": "https://github.com/carbonrobot/hapi-lambda#readme", 30 | "devDependencies": { 31 | "chai": "^4.1.2", 32 | "hapi": "^17.0.0", 33 | "mocha": "^5.2.0" 34 | }, 35 | "peerDependencies": { 36 | "hapi": ">=17.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Charlie Brown 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const transformUrlPath = (event, options) => { 2 | let url = event.path; 3 | 4 | // extract the stage from the url 5 | if (options.stripStage) { 6 | const currentStage = event.requestContext ? event.requestContext.stage : null; 7 | if (currentStage) { 8 | url = url.replace(`${currentStage}/`, ''); 9 | } 10 | } 11 | 12 | // append qs params 13 | const params = event.queryStringParameters; 14 | if (params) { 15 | const qs = Object.keys(params).map(key => `${key}=${params[key]}`); 16 | if (qs.length > 0) { 17 | url += `?${qs.join('&')}`; 18 | } 19 | } 20 | 21 | return url; 22 | }; 23 | 24 | const transformRequest = (event, options) => { 25 | const opt = { 26 | path: { 27 | stripStage: false, 28 | }, 29 | ...options, 30 | }; 31 | 32 | return { 33 | method: event.httpMethod, 34 | url: transformUrlPath(event, opt.path), 35 | payload: event.body, 36 | headers: event.headers, 37 | validate: false 38 | }; 39 | }; 40 | 41 | const transformResponse = response => { 42 | const { statusCode } = response; 43 | 44 | const headers = { 45 | ...response.headers, 46 | }; 47 | 48 | // some headers are rejected by lambda 49 | // ref: http://stackoverflow.com/questions/37942119/rust-aws-api-gateway-service-proxy-to-s3-file-upload-using-raw-https-request/37950875#37950875 50 | // ref: https://github.com/awslabs/aws-serverless-express/issues/10 51 | delete headers['content-encoding']; 52 | delete headers['transfer-encoding']; 53 | 54 | let body = response.result; 55 | if (typeof response.result !== 'string') { 56 | body = JSON.stringify(body); 57 | } 58 | 59 | return { 60 | statusCode, 61 | headers, 62 | body 63 | }; 64 | }; 65 | 66 | module.exports = { 67 | transformRequest, 68 | transformResponse 69 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hapi-lambda 2 | ------------- 3 | 4 | CAUTION: This package is ancient in Node.js terms, and is no longer maintained. 5 | 6 | [![npm version](https://badge.fury.io/js/hapi-lambda.svg)](https://badge.fury.io/js/hapi-lambda) 7 | 8 | This module will allow you to host your Hapijs (17+) application on Amazon Lambda with node 8.10+. 9 | If you are using API Gateway, you should set the Gateway to "proxy" mode. 10 | 11 | ## Usage 12 | 13 | ``` 14 | // api.js 15 | 16 | const Hapi = require('@hapi/hapi'); 17 | 18 | module.exports = { 19 | init: async () => { 20 | const server = new Hapi.server({ 21 | port: process.env.port || 3000, 22 | routes: { cors: true } 23 | }); 24 | 25 | const plugins = []; // your plugins here 26 | await server.register(plugins); 27 | 28 | // return the server for Lambda support 29 | return server; 30 | }, 31 | }; 32 | ``` 33 | 34 | Your index.js file that you expose to Lambda should look like the following: 35 | 36 | ``` 37 | // index.js 38 | 39 | const api = require('./api'); 40 | const { transformRequest, transformResponse } = require('hapi-lambda'); 41 | 42 | // cache the server for better peformance 43 | let server; 44 | 45 | exports.handler = async event => { 46 | if (!server) { 47 | server = await api.init(); 48 | } 49 | 50 | const request = transformRequest(event); 51 | 52 | // handle cors here if needed 53 | request.headers['Access-Control-Allow-Origin'] = '*'; 54 | request.headers['Access-Control-Allow-Credentials'] = true; 55 | 56 | const response = await server.inject(request); 57 | 58 | return transformResponse(response); 59 | }; 60 | ``` 61 | 62 | ## Deployment 63 | 64 | Deployment is a much larger topic and not covered by this module, however I highly recommend deploying your Lambda application with [Serverless](https://serverless.com/) 65 | 66 | ### Usage with Serverless 67 | 68 | Here is an example serverless configuration. 69 | 70 | ``` 71 | service: hapi-lambda-demo 72 | provider: 73 | name: aws 74 | runtime: nodejs8.10 75 | 76 | stage: dev 77 | region: us-east-1 78 | 79 | functions: 80 | api: 81 | handler: index.handler 82 | events: 83 | - http: 84 | path: "{proxy+}" 85 | method: any 86 | cors: true 87 | 88 | plugins: 89 | - serverless-offline 90 | ``` 91 | 92 | ## Demo 93 | 94 | A working repository and example is provided at https://www.carbonatethis.com/hosting-a-serverless-hapi-17-api-with-aws-lambda/ 95 | -------------------------------------------------------------------------------- /test/handler.test.js: -------------------------------------------------------------------------------- 1 | const { assert } = require('chai'); 2 | 3 | const { transformRequest, transformResponse } = require('../index'); 4 | 5 | describe('transformRequest', () => { 6 | 7 | it('should return the url path', () => { 8 | const event = { 9 | path: '/api/something', 10 | }; 11 | 12 | const { url } = transformRequest(event); 13 | assert.equal(url, '/api/something'); 14 | }); 15 | 16 | it('should return the correct http method', () => { 17 | const event = { 18 | path: '/api/something', 19 | httpMethod: 'GET' 20 | }; 21 | 22 | const { method } = transformRequest(event); 23 | assert.equal(method, 'GET'); 24 | }); 25 | 26 | it('should return the body as payload', () => { 27 | const event = { 28 | path: '/api/something', 29 | body: '{}' 30 | }; 31 | 32 | const { payload } = transformRequest(event); 33 | assert.equal(payload, '{}'); 34 | }); 35 | 36 | it('should append a single query param to the path', () => { 37 | const event = { 38 | path: '/api/something', 39 | queryStringParameters: { 40 | user: '1' 41 | }, 42 | }; 43 | 44 | const { url } = transformRequest(event); 45 | assert.equal(url, '/api/something?user=1'); 46 | }); 47 | 48 | it('should append query params to the path', () => { 49 | const event = { 50 | path: '/api/something', 51 | queryStringParameters: { 52 | company: '2', 53 | user: '1' 54 | }, 55 | }; 56 | 57 | const { url } = transformRequest(event); 58 | assert.equal(url, '/api/something?company=2&user=1'); 59 | }); 60 | 61 | it('should strip the stage from the path', () => { 62 | const event = { 63 | path: '/prod/api/something', 64 | queryStringParameters: { 65 | company: '2', 66 | user: '1' 67 | }, 68 | requestContext: { 69 | stage: 'prod', 70 | } 71 | }; 72 | 73 | const { url } = transformRequest(event, { path: { stripStage: true } }); 74 | assert.equal(url, '/api/something?company=2&user=1'); 75 | }); 76 | 77 | }); 78 | 79 | describe('transformResponse', () => { 80 | 81 | it('should stringify the body of a json response', () => { 82 | const response = { 83 | statusCode: 200, 84 | headers: {}, 85 | result: { status: 'OK' } 86 | }; 87 | 88 | const { body } = transformResponse(response); 89 | assert.equal(body, JSON.stringify({ status: 'OK' })); 90 | }); 91 | 92 | it('should return a string response without encoding', () => { 93 | const response = { 94 | statusCode: 200, 95 | headers: {}, 96 | result: '', 97 | }; 98 | 99 | const { body } = transformResponse(response); 100 | assert.equal(body, ''); 101 | }); 102 | 103 | it('should preserve the headers', () => { 104 | const response = { 105 | statusCode: 200, 106 | headers: { 'Content-Type': 'application/json' }, 107 | result: { status: 'OK' } 108 | }; 109 | 110 | const { headers } = transformResponse(response); 111 | assert.deepEqual(headers, { 'Content-Type': 'application/json' }); 112 | }); 113 | 114 | it('should strip restricted headers', () => { 115 | const response = { 116 | statusCode: 200, 117 | headers: { 118 | 'Content-Type': 'application/json', 119 | 'content-encoding': 'something', 120 | 'transfer-encoding': 'something else', 121 | }, 122 | result: { status: 'OK' } 123 | }; 124 | 125 | const { headers } = transformResponse(response); 126 | assert.deepEqual(headers, { 'Content-Type': 'application/json' }); 127 | }); 128 | 129 | }); 130 | --------------------------------------------------------------------------------