├── deploy.sh ├── .gitignore ├── docker-compose.yml ├── functions ├── player.proto ├── json.js ├── lib.js ├── proto.js └── contentNegotiated.js ├── tests ├── steps │ └── when.js └── test_cases │ └── get_proto.js ├── package.json ├── serverless.yml ├── .vscode └── launch.json ├── LICENSE ├── README.md └── event.json /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | npm install --force 3 | node_modules/.bin/sls deploy -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | deploy: 2 | image: node:latest 3 | environment: 4 | - HOME=/home 5 | volumes: 6 | - .:/src 7 | - $HOME/.aws:/home/.aws 8 | working_dir: /src 9 | command: "./deploy.sh" -------------------------------------------------------------------------------- /functions/player.proto: -------------------------------------------------------------------------------- 1 | package protodemo; 2 | syntax = "proto3"; 3 | 4 | message Player { 5 | string id = 1; 6 | string name = 2; 7 | repeated int32 scores = 3; 8 | } 9 | 10 | message Players { 11 | repeated Player players = 1; 12 | } -------------------------------------------------------------------------------- /functions/json.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const lib = require('./lib'); 4 | 5 | module.exports.handler = (event, context, callback) => { 6 | console.log(JSON.stringify(event)); 7 | 8 | let players = lib.genPlayers(); 9 | 10 | const response = { 11 | statusCode: 200, 12 | body: JSON.stringify(players) 13 | }; 14 | 15 | callback(null, response); 16 | }; -------------------------------------------------------------------------------- /functions/lib.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Chance = require('chance'); 4 | const chance = new Chance(); 5 | 6 | function newPlayer() { 7 | let scores = []; 8 | for (var i = 0; i < 10; i++) { 9 | let newScore = chance.integer({ min: 0, max: 100 }); 10 | scores.push(newScore); 11 | } 12 | 13 | return { 14 | id: chance.hash(), 15 | name: chance.name(), 16 | scores 17 | }; 18 | } 19 | 20 | function genPlayers() { 21 | let players = []; 22 | for (var i = 0; i < 10; i++) { 23 | players.push(newPlayer()); 24 | } 25 | 26 | return { players }; 27 | }; 28 | 29 | module.exports = { 30 | genPlayers 31 | }; -------------------------------------------------------------------------------- /tests/steps/when.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const co = require('co'); 4 | const Promise = require("bluebird"); 5 | const http = require('superagent-promise')(require('superagent'), Promise); 6 | const root = process.env.TEST_ROOT; 7 | 8 | let we_invoke_get_proto = co.wrap(function* () { 9 | let url = `${root}/proto`; 10 | 11 | let httpRes = yield http 12 | .get(url) 13 | .set('Accept', 'application/x-protobuf') 14 | .responseType('blob'); 15 | 16 | return { 17 | statusCode: httpRes.status, 18 | body: httpRes.body, 19 | headers: httpRes.headers 20 | }; 21 | }); 22 | 23 | module.exports = { 24 | we_invoke_get_proto 25 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda-protobuf-demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "acceptance-dev": "./node_modules/.bin/mocha tests/test_cases --reporter spec" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "chai": "^4.1.2", 13 | "mocha": "^3.5.3", 14 | "serverless": "^1.22.0", 15 | "serverless-apigw-binary": "^0.4.0", 16 | "superagent": "^3.6.0", 17 | "superagent-promise": "^1.1.0" 18 | }, 19 | "dependencies": { 20 | "bluebird": "^3.5.0", 21 | "chance": "^1.0.11", 22 | "co": "^4.6.0", 23 | "protobufjs": "^6.8.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: lambda-protobuf-demo 2 | 3 | plugins: 4 | - serverless-apigw-binary 5 | 6 | custom: 7 | apigwBinary: 8 | types: 9 | - 'application/x-protobuf' 10 | 11 | provider: 12 | name: aws 13 | runtime: nodejs6.10 14 | stage: dev 15 | region: us-east-1 16 | versionFunctions: false 17 | 18 | functions: 19 | proto: 20 | handler: functions/proto.handler 21 | events: 22 | - http: 23 | path: /proto 24 | method: get 25 | 26 | json: 27 | handler: functions/json.handler 28 | events: 29 | - http: 30 | path: /json 31 | method: get 32 | 33 | contentNegotiated: 34 | handler: functions/contentNegotiated.handler 35 | events: 36 | - http: 37 | path: /both 38 | method: get -------------------------------------------------------------------------------- /functions/proto.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const co = require('co'); 4 | const Promise = require('bluebird'); 5 | const protobuf = Promise.promisifyAll(require("protobufjs")); 6 | const lib = require('./lib'); 7 | const fs = require('fs'); 8 | 9 | module.exports.handler = co.wrap(function* (event, context, callback) { 10 | console.log(JSON.stringify(event)); 11 | 12 | let players = lib.genPlayers(); 13 | let root = yield protobuf.loadAsync("functions/player.proto"); 14 | let Players = root.lookupType("protodemo.Players"); 15 | let message = Players.create(players); 16 | let buffer = Players.encode(message).finish(); 17 | 18 | const response = { 19 | statusCode: 200, 20 | headers: { 'Content-Type': 'application/x-protobuf' }, 21 | body: buffer.toString('base64'), 22 | isBase64Encoded: true 23 | }; 24 | 25 | callback(null, response); 26 | }); -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "acceptance test", 8 | "program": "${workspaceRoot}/node_modules/.bin/mocha", 9 | "args": [ 10 | "tests/test_cases", 11 | "--reporter", 12 | "spec" 13 | ], 14 | "cwd": "${workspaceRoot}", 15 | "runtimeArgs": [ 16 | "--nolazy" 17 | ], 18 | "env": { 19 | "TEST_ROOT": "https://your-deployed-api/dev" 20 | } 21 | }, 22 | { 23 | "type": "node", 24 | "request": "launch", 25 | "name": "proto", 26 | "program": "${workspaceRoot}/node_modules/.bin/sls", 27 | "args": [ 28 | "invoke", 29 | "local", 30 | "-f", 31 | "proto", 32 | "-d", 33 | "{}" 34 | ] 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Yan Cui 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 | -------------------------------------------------------------------------------- /tests/test_cases/get_proto.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const co = require('co'); 4 | const Promise = require('bluebird'); 5 | const expect = require('chai').expect; 6 | const when = require('../steps/when'); 7 | const protobuf = Promise.promisifyAll(require("protobufjs")); 8 | 9 | let decode = co.wrap(function* (payload) { 10 | let root = yield protobuf.loadAsync("functions/player.proto"); 11 | let Players = root.lookupType("protodemo.Players"); 12 | return Players.decode(payload); 13 | }); 14 | 15 | describe('When we hit the GET /proto endpoint', () => { 16 | it('Should return 10 players in protobuf', co.wrap(function* () { 17 | let httpResp = yield when.we_invoke_get_proto(); 18 | 19 | expect(httpResp.statusCode).to.equal(200); 20 | 21 | let body = yield decode(httpResp.body); 22 | console.log(body); 23 | 24 | expect(body.players).to.have.lengthOf(10); 25 | body.players.forEach(p => { 26 | expect(p).to.have.property("name"); 27 | expect(p).to.have.property("id"); 28 | expect(p).to.have.property("scores"); 29 | 30 | expect(p.scores).to.have.lengthOf(10); 31 | }); 32 | })); 33 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lambda-protobuf-demo 2 | 3 | Demo using API Gateway and Lambda with Protocol Buffer. 4 | 5 | In this demo you'll find 3 handlers: 6 | 7 | * json - always return response in JSON 8 | * proto - always return response in protocol buffer format, you need to set the 9 | `Accept` header to `application/x-protobuf` for this to work otherwise API Gateway 10 | just returns the base64 string instead 11 | * contentNegotiated - uses the `Accept` header to decide whether to return JSON 12 | or protocol buffer, and return `406 Not Acceptable` if the `Accept` header is 13 | neither `application/json` nor `application/x-protobuf` 14 | 15 | For more details, please read this [blog post](https://medium.com/@theburningmonk/using-protocol-buffers-with-api-gateway-and-aws-lambda-22c3804f3e76). 16 | 17 | ### Deployment 18 | 19 | If you're on Linux, run `./deploy.sh`. 20 | 21 | If you're not on Linux, run `docker-compose up`. 22 | 23 | Why use `docker` for deployment? Because the `protobufjs` library used to encode 24 | Protocol Buffer messages has a dependency that is distributed as a native 25 | binary. So in order to get the right version you need to run grab the dependency 26 | on a Linux system, `docker` provides a nice abstraction to do that. -------------------------------------------------------------------------------- /functions/contentNegotiated.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const co = require('co'); 4 | const Promise = require('bluebird'); 5 | const protobuf = Promise.promisifyAll(require("protobufjs")); 6 | const lib = require('./lib'); 7 | const fs = require('fs'); 8 | 9 | const NotAcceptableResponse = { 10 | statusCode: 406 11 | } 12 | 13 | function jsonResponse(result) { 14 | return { 15 | statusCode: 200, 16 | body: JSON.stringify(result) 17 | }; 18 | } 19 | 20 | let protoResponse = co.wrap(function* (result, protoFilePath, messageType) { 21 | let root = yield protobuf.loadAsync(protoFilePath); 22 | let MsgType = root.lookupType(messageType); 23 | let message = MsgType.create(result); 24 | let buffer = MsgType.encode(message).finish(); 25 | 26 | return { 27 | statusCode: 200, 28 | headers: { 'Content-Type': 'application/x-protobuf' }, 29 | body: buffer.toString('base64'), 30 | isBase64Encoded: true 31 | }; 32 | }); 33 | 34 | module.exports.handler = co.wrap(function* (event, context, callback) { 35 | console.log(JSON.stringify(event)); 36 | 37 | let players = lib.genPlayers(); 38 | 39 | let accept = event.headers.Accept || "application/json"; 40 | switch (accept) { 41 | case "application/x-protobuf": 42 | let response = yield protoResponse(players, "functions/player.proto", "protodemo.Players"); 43 | callback(null, response); 44 | break; 45 | case "application/json": 46 | callback(null, jsonResponse(players)); 47 | break; 48 | default: 49 | callback(null, NotAcceptableResponse); 50 | } 51 | }); -------------------------------------------------------------------------------- /event.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/proto", 3 | "path": "/proto", 4 | "httpMethod": "GET", 5 | "headers": { 6 | "Accept": "application/x-protobuf", 7 | "Accept-Encoding": "gzip, deflate, br", 8 | "Accept-Language": "en-US,en;q=0.8,zh-TW;q=0.6,zh;q=0.4,zh-CN;q=0.2,it;q=0.2", 9 | "cache-control": "no-cache", 10 | "CloudFront-Forwarded-Proto": "https", 11 | "CloudFront-Is-Desktop-Viewer": "true", 12 | "CloudFront-Is-Mobile-Viewer": "false", 13 | "CloudFront-Is-SmartTV-Viewer": "false", 14 | "CloudFront-Is-Tablet-Viewer": "false", 15 | "CloudFront-Viewer-Country": "GB", 16 | "Host": "ya9sv0svn8.execute-api.us-east-1.amazonaws.com", 17 | "postman-token": "5f4b53c3-8e28-adf3-b58e-fa7f16999739", 18 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", 19 | "Via": "2.0 f76a5f650b7fe2be49bcae6cfe9c8215.cloudfront.net (CloudFront)", 20 | "X-Amz-Cf-Id": "LrgMvzuxuf4uXHjipvjV7BwYCaD3wMU2TLqQOCEQWlvUTj7rw9xCvA==", 21 | "x-amzn-ssl-client-hello": "AQAB/AMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgRipZaqEIMRqhf1gcF7zQILP5xa6dpSMPd+9XvrUoWIQAHCoqwCvAL8AswDDMqcyowBPAFACcAJ0ALwA1AAoBAAGXKioAAP8BAAEAAAAAMwAxAAAueWE5c3Ywc3ZuOC5leGVjdXRlLWFwaS51cy1lYXN0LTEuYW1hem9uYXdzLmNvbQAXAAAAIwDQfFGZCx/sPkQUQIybKRmA1TTHJrOPx0Q+jg20uxuBOwdTDw9Xymv2iKHr0nEQS0Xdqx4ewPqFFoOAAIvunzzKoX40Hyg1jqtn22Pk53ICilO378lJ9escjjRMgkt67SKTm2krqv2ebfrRy6UhzJazvp5818OA4Xta13AE2ph0ac6+EvOks8eI45AApjCGmORJZOJX4DKZzS3sEpAFAvG2jKts6GZ/SwDVKYOEdhLJrZ/eMk1A/CTK0nn1UD3pnJtyykkTP0C3yCfDYYAFBCiUDQANABQAEgQDCAQEAQUDCAUFAQgGBgECAQAFAAUBAAAAAAASAAAAEAAOAAwCaDIIaHR0cC8xLjF1UAAAAAsAAgEAAAoACgAIGhoAHQAXABg6OgABAAAVACcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", 22 | "X-Amzn-Trace-Id": "Root=1-59bb0585-5fa84d5078271a7441010427", 23 | "X-Forwarded-For": "88.98.200.238, 216.137.62.52", 24 | "X-Forwarded-Port": "443", 25 | "X-Forwarded-Proto": "https" 26 | }, 27 | "queryStringParameters": null, 28 | "pathParameters": null, 29 | "stageVariables": null, 30 | "requestContext": { 31 | "path": "/dev/proto", 32 | "accountId": "374852340823", 33 | "resourceId": "m0cide", 34 | "stage": "dev", 35 | "requestId": "cd737306-999d-11e7-8cbc-d7d4a57a3a28", 36 | "identity": { 37 | "cognitoIdentityPoolId": null, 38 | "accountId": null, 39 | "cognitoIdentityId": null, 40 | "caller": null, 41 | "apiKey": "", 42 | "sourceIp": "88.98.200.238", 43 | "accessKey": null, 44 | "cognitoAuthenticationType": null, 45 | "cognitoAuthenticationProvider": null, 46 | "userArn": null, 47 | "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", 48 | "user": null 49 | }, 50 | "resourcePath": "/proto", 51 | "httpMethod": "GET", 52 | "apiId": "ya9sv0svn8" 53 | }, 54 | "body": null, 55 | "isBase64Encoded": false 56 | } --------------------------------------------------------------------------------