├── .circleci └── config.yml ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── artillery-test.yml ├── docs └── performance │ ├── aws-serverless-express-results.html │ └── aws-serverless-fastify-results.html ├── package-lock.json ├── package.json ├── src ├── index.spec.ts ├── index.ts ├── request-forwarder.ts ├── request-mapper.ts ├── response-builder.spec.ts ├── response-builder.ts └── socket-manager.ts ├── tsconfig.build.json ├── tsconfig.json └── tslint.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: cimg/node:20.11.0 6 | working_directory: ~/repo 7 | steps: 8 | - checkout 9 | - restore_cache: 10 | keys: 11 | - v1-dependencies-{{ checksum "package.json" }} 12 | # fallback to using the latest cache if no exact match is found 13 | - v1-dependencies- 14 | - run: 15 | name: Install Dependencies 16 | command: | 17 | npm install 18 | - save_cache: 19 | paths: 20 | - ./node_modules 21 | key: v1-dependencies-{{ checksum "package.json" }} 22 | - run: 23 | name: Build 24 | command: | 25 | npm run build 26 | - run: 27 | name: Test 28 | command: | 29 | npm run test:cov 30 | - run: 31 | name: Lint 32 | command: | 33 | npm run lint 34 | 35 | release: 36 | docker: 37 | - image: cimg/node:20.11.0 38 | working_directory: ~/repo 39 | steps: 40 | - checkout 41 | - restore_cache: 42 | keys: 43 | - v1-dependencies-{{ checksum "package.json" }} 44 | # fallback to using the latest cache if no exact match is found 45 | - v1-dependencies- 46 | - run: 47 | name: Install Dependencies 48 | command: | 49 | npm install 50 | - run: 51 | name: Build 52 | command: | 53 | npm run build 54 | - run: 55 | name: Release 56 | command: | 57 | npx semantic-release 58 | 59 | workflows: 60 | version: 2 61 | test_and_release: 62 | jobs: 63 | - build 64 | - release: 65 | requires: 66 | - build 67 | filters: 68 | branches: 69 | only: master -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir : __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.9.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [3.1.0](https://github.com/benMain/aws-serverless-fastify/compare/v3.0.7...v3.1.0) (2024-05-16) 2 | 3 | 4 | ### Features 5 | 6 | * **NestJs10Compatibility:** Add compatibility for NestJs10. ([4a7f466](https://github.com/benMain/aws-serverless-fastify/commit/4a7f466dc380f2bc8ae6a5d1da79154fbd909254)) 7 | * **NestJs10Compatibility:** Add compatibility for NestJs10. ([fbb4eb2](https://github.com/benMain/aws-serverless-fastify/commit/fbb4eb2394b1eebf143e92c3171c44239bf5617f)) 8 | 9 | ## [3.0.7](https://github.com/benMain/aws-serverless-fastify/compare/v3.0.6...v3.0.7) (2024-05-16) 10 | 11 | ## [3.0.6](https://github.com/benMain/aws-serverless-fastify/compare/v3.0.5...v3.0.6) (2023-12-14) 12 | 13 | ## [3.0.5](https://github.com/benMain/aws-serverless-fastify/compare/v3.0.4...v3.0.5) (2023-12-08) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * use options object on instance.listen ([cc2820a](https://github.com/benMain/aws-serverless-fastify/commit/cc2820a)) 19 | 20 | ## [3.0.4](https://github.com/benMain/aws-serverless-fastify/compare/v3.0.3...v3.0.4) (2023-06-20) 21 | 22 | ## [3.0.3](https://github.com/benMain/aws-serverless-fastify/compare/v3.0.2...v3.0.3) (2023-06-08) 23 | 24 | ## [3.0.2](https://github.com/benMain/aws-serverless-fastify/compare/v3.0.1...v3.0.2) (2023-01-11) 25 | 26 | ## [3.0.1](https://github.com/benMain/aws-serverless-fastify/compare/v3.0.0...v3.0.1) (2023-01-10) 27 | 28 | # [3.0.0](https://github.com/benMain/aws-serverless-fastify/compare/v2.0.1...v3.0.0) (2022-11-15) 29 | 30 | ## [2.0.1](https://github.com/benMain/aws-serverless-fastify/compare/v2.0.0...v2.0.1) (2022-11-10) 31 | 32 | # [2.0.0](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.29...v2.0.0) (2022-11-10) 33 | 34 | ## [1.0.29](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.28...v1.0.29) (2022-06-07) 35 | 36 | ## [1.0.28](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.27...v1.0.28) (2021-10-05) 37 | 38 | ## [1.0.27](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.26...v1.0.27) (2021-07-22) 39 | 40 | ## [1.0.26](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.25...v1.0.26) (2021-05-04) 41 | 42 | ## [1.0.25](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.24...v1.0.25) (2021-01-14) 43 | 44 | ## [1.0.24](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.23...v1.0.24) (2020-11-25) 45 | 46 | ## [1.0.23](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.22...v1.0.23) (2020-10-15) 47 | 48 | ## [1.0.22](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.21...v1.0.22) (2020-10-15) 49 | 50 | ## [1.0.21](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.20...v1.0.21) (2020-08-10) 51 | 52 | ## [1.0.20](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.19...v1.0.20) (2020-07-28) 53 | 54 | ## [1.0.19](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.18...v1.0.19) (2020-07-28) 55 | 56 | ## [1.0.18](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.17...v1.0.18) (2020-07-10) 57 | 58 | ## [1.0.17](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.16...v1.0.17) (2020-05-14) 59 | 60 | ## [1.0.16](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.15...v1.0.16) (2020-05-14) 61 | 62 | ## [1.0.15](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.14...v1.0.15) (2020-05-14) 63 | 64 | ## [1.0.14](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.13...v1.0.14) (2020-05-14) 65 | 66 | ## [1.0.13](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.12...v1.0.13) (2019-12-30) 67 | 68 | ## [1.0.12](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.11...v1.0.12) (2019-12-30) 69 | 70 | ## [1.0.11](https://github.com/benMain/aws-serverless-fastify/compare/v1.0.10...v1.0.11) (2019-11-20) 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Benjamin Main 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS-SERVERLESS-FASTIFY 2 | 3 |

4 | Nest Logo 5 |

6 | 7 | ## Description 8 | 9 | A port of the AWSLABS [aws-serverless-express](https://github.com/awslabs/aws-serverless-express) library tailor made for the 10 | [Fastify](https://www.fastify.io/) web framework. Inspired by wanting to use the Fastify gracefully in [Nest](https://docs.nestjs.com/) projects with Lambda. Plus it's called Fastify, how cool is that! 11 | 12 | ## Nest Compatibility 13 | 14 | Version 1.x compatible with nestjs 7.x and Fastify 2.x 15 | Version 2.x compatible with nestjs 8.x and Fastify 3.x 16 | Version 3.x compatible with nestjs 9.x and Fastify 4.x 17 | 18 | ## Installation 19 | 20 | ```bash 21 | $ npm install aws-serverless-fastify 22 | ``` 23 | 24 | ## Examples 25 | 26 | ### Nest 27 | 28 | Nest example is provided [here](https://github.com/benMain/aws-serverless-fastify-nest-example). Below is the summary. 29 | 30 | lambda-entrypoint.ts (I think the lambda-entrypoint.ts is much cleaner than what is proposed by [Fastify](https://github.com/fastify/fastify/blob/master/docs/Serverless.md)) 31 | 32 | ```typescript 33 | import { 34 | Context, 35 | APIGatewayProxyEvent, 36 | APIGatewayProxyResult, 37 | } from 'aws-lambda'; 38 | import { bootstrap } from './app'; 39 | import * as fastify from 'fastify'; 40 | import { proxy } from 'aws-serverless-fastify'; 41 | 42 | let fastifyServer: fastify.FastifyInstance; 43 | 44 | export const handler = async ( 45 | event: APIGatewayProxyEvent, 46 | context: Context, 47 | ): Promise => { 48 | if (!fastifyServer) { 49 | fastifyServer = await bootstrap(); 50 | } 51 | return await proxy(fastifyServer, event, context); 52 | }; 53 | ``` 54 | 55 | app.ts (Setup the application once in app.ts) 56 | 57 | ```typescript 58 | import { NestFactory } from '@nestjs/core'; 59 | import { AppModule } from './app.module'; 60 | import { 61 | FastifyAdapter, 62 | NestFastifyApplication, 63 | } from '@nestjs/platform-fastify'; 64 | import * as fastify from 'fastify'; 65 | 66 | export async function bootstrap(): Promise { 67 | const serverOptions: fastify.ServerOptionsAsHttp = { 68 | logger: true, 69 | }; 70 | const instance: fastify.FastifyInstance = fastify(serverOptions); 71 | const nestApp = await NestFactory.create( 72 | AppModule, 73 | new FastifyAdapter(instance), 74 | ); 75 | nestApp.setGlobalPrefix('api'); 76 | nestApp.enableCors(); 77 | await nestApp.init(); 78 | return instance; 79 | } 80 | ``` 81 | 82 | main.ts (Will still serve to run the Nest App locally) 83 | 84 | ```typescript 85 | import { bootstrap } from './app'; 86 | 87 | async function startLocal() { 88 | const fastifyInstance = await bootstrap(); 89 | fastifyInstance.listen(3000); 90 | } 91 | 92 | startLocal(); 93 | ``` 94 | 95 | ## Performance 96 | 97 | Not meant to be exhaustive by any means but here are some basic load tests comparisons through API Gateway via [Artillery](https://artillery.io/). [aws-serverless-fastify](https://benMain.github.io/aws-serverless-fastify/performance/aws-serverless-fastify-results.html) versus [aws-serverless-express](https://benMain.github.io/aws-serverless-fastify/performance/aws-serverless-express-results.html) 98 | 99 | ## Support 100 | 101 | Pull Requests are welcome! We just thought this was a cool idea to simplify! 102 | Important: Commits should follow Angluar conventional-changelog format :) 103 | 104 | ## Stay in touch 105 | 106 | - Author - [Benjamin Main](mailto:bmain@lumeris.com) 107 | 108 | ## License 109 | 110 | aws-serverless-fastify is [MIT licensed](LICENSE). 111 | -------------------------------------------------------------------------------- /artillery-test.yml: -------------------------------------------------------------------------------- 1 | config: 2 | target: 'https://dev-customerservice.api.lumeris.io/api/members' 3 | phases: 4 | - duration: 60 5 | arrivalRate: 10 6 | defaults: 7 | headers: 8 | Accept: 'application/json' 9 | x-api-key: "{{ $processEnvironment.X_API_KEY }}" 10 | payload: 11 | path: "ids.csv" 12 | fields: 13 | - "id" 14 | scenarios: 15 | - flow: 16 | - get: 17 | url: "/{{ id }}" 18 | -------------------------------------------------------------------------------- /docs/performance/aws-serverless-express-results.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artillery report 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 48 | 49 | 357 | 358 | 372 | 373 | 374 | 375 | 376 | 377 |
378 | 379 | 392 | 393 |
394 |
395 |

Test run on date

396 |
397 |
398 | 399 |
400 |
401 |

Summary

402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 |
Test duration0 sec
Scenarios created0
Scenarios completed0
419 |
420 | 421 |
422 |
423 | Scenario counts: 424 | 425 |

426 | 427 | 428 |
429 |
430 |
431 | 432 |
433 |
434 | Codes 435 | 436 |

437 | 438 | 439 |
440 |
441 |
442 | 443 |
444 |
445 | Errors 446 | 447 |

448 | 449 | 450 |
451 |
452 |
453 |
454 | 455 |
456 |
457 |

Charts

458 |
459 |
460 | 461 |
462 |
463 |
464 | 465 |
466 |
467 |
468 | 469 |
470 | 471 |
472 | 473 |
474 | 475 |
476 |
477 |
478 | 479 |
480 |
481 |
482 |
483 |
484 |
485 | 486 |
487 |
488 |
489 |
490 |
491 |
492 | 493 | 494 | 495 | 496 | 497 |
498 | 499 |
500 |
501 |

Raw report data

502 |

This is the raw JSON stats output that the report was generated from.

503 |
504 |
505 |
506 |
507 | 508 | 776 | 779 | 780 | 781 | -------------------------------------------------------------------------------- /docs/performance/aws-serverless-fastify-results.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artillery report 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 48 | 49 | 355 | 356 | 370 | 371 | 372 | 373 | 374 | 375 |
376 | 377 | 390 | 391 |
392 |
393 |

Test run on date

394 |
395 |
396 | 397 |
398 |
399 |

Summary

400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 |
Test duration0 sec
Scenarios created0
Scenarios completed0
417 |
418 | 419 |
420 |
421 | Scenario counts: 422 | 423 |

424 | 425 | 426 |
427 |
428 |
429 | 430 |
431 |
432 | Codes 433 | 434 |

435 | 436 | 437 |
438 |
439 |
440 | 441 |
442 |
443 | Errors 444 | 445 |

446 | 447 | 448 |
449 |
450 |
451 |
452 | 453 |
454 |
455 |

Charts

456 |
457 |
458 | 459 |
460 |
461 |
462 | 463 |
464 |
465 |
466 | 467 |
468 | 469 |
470 | 471 |
472 | 473 |
474 |
475 |
476 | 477 |
478 |
479 |
480 |
481 |
482 |
483 | 484 |
485 |
486 |
487 |
488 |
489 |
490 | 491 | 492 | 493 | 494 | 495 |
496 | 497 |
498 |
499 |

Raw report data

500 |

This is the raw JSON stats output that the report was generated from.

501 |
502 |
503 |
504 |
505 | 506 | 774 | 777 | 778 | 779 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-serverless-fastify", 3 | "description": "Proxy Library for the Fastify Web Server", 4 | "author": "Benjamin Main", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "keywords": [ 9 | "fastify", 10 | "serverless", 11 | "lambda", 12 | "aws", 13 | "proxy" 14 | ], 15 | "homepage": "https://github.com/benMain/aws-serverless-fastify", 16 | "bugs": { 17 | "url": "https://github.com/benMain/aws-serverless-fastify/issues" 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/benMain/aws-serverless-fastify.git" 25 | }, 26 | "publishConfig": { 27 | "registry": "https://registry.npmjs.org/", 28 | "tag": "latest" 29 | }, 30 | "scripts": { 31 | "build": "tsc -p tsconfig.build.json", 32 | "format": "prettier --write \"src/**/*.ts\"", 33 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 34 | "test": "jest", 35 | "test:watch": "jest --watch", 36 | "test:cov": "jest --coverage", 37 | "release": "semantic-release", 38 | "release-local": "node -r dotenv/config node_modules/semantic-release/bin/semantic-release --no-ci --dry-run" 39 | }, 40 | "dependencies": { 41 | "binary-case": "^1.1.4", 42 | "type-is": "^1.6.18" 43 | }, 44 | "devDependencies": { 45 | "@semantic-release/changelog": "^6.0.3", 46 | "@semantic-release/git": "^10.0.1", 47 | "@types/aws-lambda": "^8.10.137", 48 | "@types/jest": "29.5.12", 49 | "@types/node": "20.12.12", 50 | "@types/supertest": "6.0.2", 51 | "@types/type-is": "1.6.6", 52 | "@typescript-eslint/eslint-plugin": "^6.0.0", 53 | "@typescript-eslint/parser": "^6.0.0", 54 | "eslint": "^8.42.0", 55 | "eslint-config-prettier": "^9.1.0", 56 | "eslint-plugin-prettier": "^5.1.3", 57 | "fastify": "4.27.0", 58 | "husky": "^9.0.11", 59 | "jest": "29.7.0", 60 | "lint-staged": "^15.2.2", 61 | "prettier": "^3.2.5", 62 | "semantic-release": "^23.1.1", 63 | "supertest": "^6.3.3", 64 | "ts-jest": "29.1.2", 65 | "ts-loader": "9.5.1", 66 | "ts-node": "^10.9.2", 67 | "tsconfig-paths": "4.2.0", 68 | "typescript": "5.4.5" 69 | }, 70 | "peerDependencies": { 71 | "fastify": "^4.0" 72 | }, 73 | "release": { 74 | "branch": "master", 75 | "plugins": [ 76 | [ 77 | "@semantic-release/commit-analyzer", 78 | { 79 | "preset": "angular", 80 | "releaseRules": [ 81 | { 82 | "type": "docs", 83 | "scope": "README", 84 | "release": "patch" 85 | }, 86 | { 87 | "type": "refactor", 88 | "release": "patch" 89 | }, 90 | { 91 | "type": "chore", 92 | "release": "patch" 93 | }, 94 | { 95 | "type": "style", 96 | "release": "patch" 97 | }, 98 | { 99 | "type": "nestcompat", 100 | "release": "major" 101 | } 102 | ], 103 | "parserOpts": { 104 | "noteKeywords": [ 105 | "BREAKING CHANGE", 106 | "BREAKING CHANGES" 107 | ] 108 | } 109 | } 110 | ], 111 | "@semantic-release/release-notes-generator", 112 | [ 113 | "@semantic-release/changelog", 114 | { 115 | "changelogFile": "CHANGELOG.md" 116 | } 117 | ], 118 | [ 119 | "@semantic-release/npm", 120 | { 121 | "npmPublish": true, 122 | "tarballDir": "dist" 123 | } 124 | ], 125 | [ 126 | "@semantic-release/git", 127 | { 128 | "assets": [ 129 | "package.json", 130 | "package-lock.json", 131 | "CHANGELOG.md" 132 | ], 133 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 134 | } 135 | ], 136 | [ 137 | "@semantic-release/github", 138 | { 139 | "assets": "dist/*.tgz" 140 | } 141 | ] 142 | ] 143 | }, 144 | "husky": { 145 | "hooks": { 146 | "pre-commit": "lint-staged" 147 | } 148 | }, 149 | "lint-staged": { 150 | "*.js": [ 151 | "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 152 | "jest --findRelatedTests" 153 | ], 154 | "./**/*.{js,json,css,md}": [ 155 | "prettier --write \"src/**/*.ts\"", 156 | "git add" 157 | ] 158 | }, 159 | "jest": { 160 | "moduleFileExtensions": [ 161 | "js", 162 | "json", 163 | "ts" 164 | ], 165 | "rootDir": "src", 166 | "testRegex": ".*\\.spec\\.ts$", 167 | "transform": { 168 | "^.+\\.(t|j)s$": "ts-jest" 169 | }, 170 | "collectCoverageFrom": [ 171 | "**/*.(t|j)s" 172 | ], 173 | "coverageDirectory": "../coverage", 174 | "testEnvironment": "node" 175 | }, 176 | "version": "3.1.0" 177 | } 178 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { fastify, FastifyInstance } from 'fastify'; 2 | import { proxy } from './index'; 3 | import { APIGatewayProxyEvent, Context } from 'aws-lambda'; 4 | import { tmpdir } from 'os'; 5 | import { join } from 'path'; 6 | import { SocketManager } from './socket-manager'; 7 | 8 | describe('proxy()', () => { 9 | let instance: FastifyInstance; 10 | let event: APIGatewayProxyEvent; 11 | let context: Context; 12 | beforeEach(async () => { 13 | instance = fastify({ logger: true }); 14 | event = buildGetEvent(); 15 | context = buildContext(); 16 | }); 17 | it('forwardRequestToNodeServer(): should forward get request with listening server', async () => { 18 | const exampleResponse = { hello: 'world' }; 19 | const random = SocketManager.getSocketSuffix(); 20 | const sockFile = join(tmpdir(), `${random}server.sock`); 21 | instance.get('/hello', async () => { 22 | return exampleResponse; 23 | }); 24 | await instance.listen({ path: sockFile }); 25 | const response = await proxy(instance, event, context); 26 | expect(response.statusCode).toEqual(200); 27 | expect(JSON.parse(response.body)).toEqual(exampleResponse); 28 | expect(response.isBase64Encoded).toEqual(false); 29 | await instance.close(); 30 | }); 31 | it('forwardRequestToNodeServer(): should forward get request and start not listening server', async () => { 32 | const exampleResponse = { hello: 'world' }; 33 | instance.get('/hello', async () => { 34 | return exampleResponse; 35 | }); 36 | const response = await proxy(instance, event, context); 37 | expect(response.statusCode).toEqual(200); 38 | expect(JSON.parse(response.body)).toEqual(exampleResponse); 39 | expect(response.isBase64Encoded).toEqual(false); 40 | await instance.close(); 41 | }); 42 | it('forwardRequestToNodeServer(): should forward post request', async () => { 43 | const postEvent: APIGatewayProxyEvent = buildPostEvent(); 44 | instance.post('/post-test', async (request) => { 45 | return { hello: (request.body as any).name }; 46 | }); 47 | const response = await proxy(instance, postEvent, context); 48 | expect(response.statusCode).toEqual(200); 49 | expect(JSON.parse(response.body).hello).toEqual( 50 | JSON.parse(postEvent.body).name, 51 | ); 52 | expect(response.isBase64Encoded).toEqual(false); 53 | await instance.close(); 54 | }); 55 | 56 | it('forwardRequestToNodeServer(): should respond with 404 when endpoint not found', async () => { 57 | const exampleResponse = { hello: 'world' }; 58 | instance.get('/incorrect', async () => { 59 | return exampleResponse; 60 | }); 61 | const response = await proxy(instance, event, context); 62 | expect(response.statusCode).toEqual(404); 63 | await instance.close(); 64 | }); 65 | }); 66 | 67 | function buildGetEvent(): APIGatewayProxyEvent { 68 | return { 69 | body: null, 70 | headers: { 71 | 'Accept-Encoding': 'gzip, deflate, sdch', 72 | 'Accept-Language': 'en-US,en;q=0.8', 73 | 'Cache-Control': 'max-age=0', 74 | 'CloudFront-Forwarded-Proto': 'https', 75 | 'CloudFront-Is-Desktop-Viewer': 'true', 76 | 'CloudFront-Is-Mobile-Viewer': 'false', 77 | 'CloudFront-Is-SmartTV-Viewer': 'false', 78 | 'CloudFront-Is-Tablet-Viewer': 'false', 79 | 'CloudFront-Viewer-Country': 'US', 80 | 'Upgrade-Insecure-Requests': '1', 81 | 'User-Agent': 'Custom User Agent String', 82 | 'X-Amz-Cf-Id': 'cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==', 83 | 'X-Forwarded-For': '127.0.0.1, 127.0.0.2', 84 | 'X-Forwarded-Port': '443', 85 | 'X-Forwarded-Proto': 'http', 86 | }, 87 | multiValueHeaders: {}, 88 | httpMethod: 'GET', 89 | isBase64Encoded: false, 90 | path: '/hello', 91 | pathParameters: null, 92 | queryStringParameters: null, 93 | multiValueQueryStringParameters: null, 94 | stageVariables: null, 95 | requestContext: null, 96 | resource: null, 97 | }; 98 | } 99 | 100 | function buildContext(): Context { 101 | return { 102 | functionName: null, 103 | callbackWaitsForEmptyEventLoop: null, 104 | functionVersion: null, 105 | invokedFunctionArn: null, 106 | awsRequestId: null, 107 | logGroupName: null, 108 | logStreamName: null, 109 | memoryLimitInMB: '1024', 110 | getRemainingTimeInMillis: () => 1000, 111 | done: () => null, 112 | fail: () => null, 113 | /* tslint:disable:no-empty */ 114 | 115 | succeed: () => { 116 | 'blah'; 117 | }, 118 | }; 119 | } 120 | 121 | function buildPostEvent(): APIGatewayProxyEvent { 122 | return { 123 | body: '{ "name": "Ben" }', 124 | headers: { 125 | 'Accept-Encoding': 'gzip, deflate, sdch', 126 | 'Accept-Language': 'en-US,en;q=0.8', 127 | 'Cache-Control': 'max-age=0', 128 | 'CloudFront-Forwarded-Proto': 'https', 129 | 'CloudFront-Is-Desktop-Viewer': 'true', 130 | 'CloudFront-Is-Mobile-Viewer': 'false', 131 | 'CloudFront-Is-SmartTV-Viewer': 'false', 132 | 'CloudFront-Is-Tablet-Viewer': 'false', 133 | 'CloudFront-Viewer-Country': 'US', 134 | 'Upgrade-Insecure-Requests': '1', 135 | 'User-Agent': 'Custom User Agent String', 136 | 'X-Amz-Cf-Id': 'cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==', 137 | 'X-Forwarded-For': '127.0.0.1, 127.0.0.2', 138 | 'X-Forwarded-Port': '443', 139 | 'X-Forwarded-Proto': 'http', 140 | 'Content-Type': 'application/json', 141 | }, 142 | multiValueHeaders: {}, 143 | httpMethod: 'POST', 144 | isBase64Encoded: false, 145 | path: '/post-test', 146 | pathParameters: null, 147 | queryStringParameters: null, 148 | multiValueQueryStringParameters: null, 149 | stageVariables: null, 150 | requestContext: null, 151 | resource: null, 152 | }; 153 | } 154 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'http'; 2 | import { 3 | APIGatewayProxyEvent, 4 | Context, 5 | APIGatewayProxyResult, 6 | } from 'aws-lambda'; 7 | import { RequestForwarder } from './request-forwarder'; 8 | import { SocketManager } from './socket-manager'; 9 | import * as fastify from 'fastify'; 10 | 11 | export function proxy( 12 | fastifyInstance: fastify.FastifyInstance, 13 | event: APIGatewayProxyEvent, 14 | context: Context, 15 | binaryTypes?: string[], 16 | ): Promise { 17 | return new Promise((resolve, reject) => { 18 | const promise = { 19 | resolve, 20 | reject, 21 | }; 22 | const resolver = { 23 | succeed: (data: APIGatewayProxyResult) => { 24 | return promise.resolve(data); 25 | }, 26 | }; 27 | binaryTypes = binaryTypes ? binaryTypes.slice() : []; 28 | if (fastifyInstance.server.listening) { 29 | RequestForwarder.forwardRequestToNodeServer( 30 | fastifyInstance.server, 31 | event, 32 | context, 33 | resolver, 34 | binaryTypes, 35 | ); 36 | } else { 37 | const socketSuffix = SocketManager.getSocketSuffix(); 38 | startFastify(fastifyInstance, socketSuffix).on('listening', () => 39 | RequestForwarder.forwardRequestToNodeServer( 40 | fastifyInstance.server, 41 | event, 42 | context, 43 | resolver, 44 | binaryTypes, 45 | ), 46 | ); 47 | } 48 | }); 49 | } 50 | 51 | function startFastify( 52 | instance: fastify.FastifyInstance, 53 | socketSuffix: string, 54 | ): Server { 55 | instance.listen({ path: SocketManager.getSocketPath(socketSuffix) }); 56 | return instance.server; 57 | } 58 | -------------------------------------------------------------------------------- /src/request-forwarder.ts: -------------------------------------------------------------------------------- 1 | import { request, Server, RequestOptions } from 'http'; 2 | import { 3 | APIGatewayProxyEvent, 4 | Context, 5 | APIGatewayProxyResult, 6 | } from 'aws-lambda'; 7 | import { RequestMapper } from './request-mapper'; 8 | import { ResponseBuilder } from './response-builder'; 9 | 10 | export class RequestForwarder { 11 | public static forwardRequestToNodeServer( 12 | server: Server, 13 | event: APIGatewayProxyEvent, 14 | context: Context, 15 | resolver: { succeed: (data: APIGatewayProxyResult) => void }, 16 | binaryTypes: string[], 17 | ): void { 18 | try { 19 | const requestOptions: RequestOptions = 20 | RequestMapper.mapApiGatewayEventToHttpRequest( 21 | event, 22 | context, 23 | server.address().toString(), 24 | ); 25 | const req = request(requestOptions, (response) => 26 | ResponseBuilder.buildResponseToApiGateway( 27 | response, 28 | resolver, 29 | binaryTypes, 30 | ), 31 | ); 32 | 33 | if (event.body) { 34 | const body = RequestMapper.getEventBody(event); 35 | req.write(body); 36 | } 37 | 38 | req 39 | .on('error', (error) => 40 | ResponseBuilder.buildConnectionErrorResponseToApiGateway( 41 | error, 42 | resolver, 43 | ), 44 | ) 45 | .end(); 46 | } catch (error) { 47 | ResponseBuilder.buildLibraryErrorResponseToApiGateway(error, resolver); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/request-mapper.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, Context } from 'aws-lambda'; 2 | import { format } from 'url'; 3 | import { RequestOptions } from 'http'; 4 | 5 | export class RequestMapper { 6 | public static mapApiGatewayEventToHttpRequest( 7 | event: APIGatewayProxyEvent, 8 | context: Context, 9 | socketPath: string, 10 | ): RequestOptions { 11 | const headers: { [name: string]: any } = Object.assign({}, event.headers); 12 | 13 | if (event.body && !headers['Content-Length']) { 14 | const body = RequestMapper.getEventBody(event); 15 | headers['Content-Length'] = Buffer.byteLength(body); 16 | } 17 | 18 | return { 19 | method: event.httpMethod, 20 | path: RequestMapper.getPathWithQueryStringParams(event), 21 | headers, 22 | socketPath, 23 | }; 24 | } 25 | 26 | private static getPathWithQueryStringParams( 27 | event: APIGatewayProxyEvent, 28 | ): string { 29 | return format({ pathname: event.path, query: event.queryStringParameters }); 30 | } 31 | 32 | public static getEventBody(event: APIGatewayProxyEvent): Buffer { 33 | return Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/response-builder.spec.ts: -------------------------------------------------------------------------------- 1 | import { ResponseBuilder } from './response-builder'; 2 | import { APIGatewayProxyResult } from 'aws-lambda'; 3 | 4 | describe('ResponseBuilder', () => { 5 | it('buildConnectionErrorResponseToApiGateway(): should build valid gateway response', () => { 6 | const resolver: any = { 7 | cache: null, 8 | succeed(data: APIGatewayProxyResult) { 9 | this.cache = data; 10 | }, 11 | }; 12 | const error = new Error('Connection error!'); 13 | ResponseBuilder.buildConnectionErrorResponseToApiGateway(error, resolver); 14 | expect(resolver.cache.statusCode).toEqual(502); 15 | }); 16 | 17 | it('buildLibraryErrorResponseToApiGateway(): should build valid gateway response', () => { 18 | const resolver: any = { 19 | cache: null, 20 | succeed(data: APIGatewayProxyResult) { 21 | this.cache = data; 22 | }, 23 | }; 24 | const error = new Error('Connection error!'); 25 | ResponseBuilder.buildLibraryErrorResponseToApiGateway(error, resolver); 26 | expect(resolver.cache.statusCode).toEqual(500); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/response-builder.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage } from 'http'; 2 | import { APIGatewayProxyResult } from 'aws-lambda'; 3 | import { is } from 'type-is'; 4 | import * as binarycase from 'binary-case'; 5 | 6 | export class ResponseBuilder { 7 | public static buildResponseToApiGateway( 8 | response: IncomingMessage, 9 | resolver: { succeed: (data: APIGatewayProxyResult) => void }, 10 | binaryTypes: string[], 11 | ) { 12 | const buf = []; 13 | 14 | response 15 | .on('data', (chunk: any) => buf.push(chunk)) 16 | .on('end', () => { 17 | const bodyBuffer = Buffer.concat(buf); 18 | const statusCode = response.statusCode; 19 | const headers = response.headers as { 20 | [header: string]: string | number | boolean; 21 | }; 22 | 23 | if (headers['transfer-encoding'] === 'chunked') { 24 | delete headers['transfer-encoding']; 25 | } 26 | 27 | // HACK: modifies header casing to get around API Gateway's limitation of not allowing multiple 28 | // headers with the same name, as discussed on the AWS Forum https://forums.aws.amazon.com/message.jspa?messageID=725953#725953 29 | Object.keys(headers).forEach((h) => { 30 | if (Array.isArray(h)) { 31 | if (h.toLowerCase() === 'set-cookie') { 32 | h.forEach((value: any, i: number) => { 33 | headers[binarycase(h, i + 1)] = value; 34 | }); 35 | delete headers[h]; 36 | } else { 37 | headers[h] = h.join(','); 38 | } 39 | } 40 | }); 41 | 42 | const contentType = ResponseBuilder.getContentType({ 43 | contentTypeHeader: headers['content-type'], 44 | }); 45 | let isBase64Encoded = ResponseBuilder.isContentTypeBinaryMimeType({ 46 | contentType, 47 | binaryMimeTypes: binaryTypes, 48 | }); 49 | if (!isBase64Encoded) { 50 | isBase64Encoded = headers['content-encoding'] === 'gzip'; 51 | } 52 | const body = bodyBuffer.toString(isBase64Encoded ? 'base64' : 'utf8'); 53 | const successResponse: APIGatewayProxyResult = { 54 | statusCode, 55 | body, 56 | headers, 57 | isBase64Encoded, 58 | }; 59 | 60 | resolver.succeed(successResponse); 61 | }); 62 | } 63 | 64 | public static buildConnectionErrorResponseToApiGateway( 65 | error: Error, 66 | resolver: { succeed: (data: APIGatewayProxyResult) => void }, 67 | ) { 68 | // tslint:disable-next-line: no-console 69 | console.log('ERROR: aws-serverless-fastify connection error'); 70 | // tslint:disable-next-line: no-console 71 | console.error(error); 72 | const errorResponse: APIGatewayProxyResult = { 73 | statusCode: 502, 74 | body: '', 75 | headers: {}, 76 | }; 77 | resolver.succeed(errorResponse); 78 | } 79 | 80 | public static buildLibraryErrorResponseToApiGateway( 81 | error: Error, 82 | resolver: { succeed: (data: APIGatewayProxyResult) => void }, 83 | ) { 84 | // tslint:disable-next-line: no-console 85 | console.log('ERROR: aws-serverless-fastify error'); 86 | // tslint:disable-next-line: no-console 87 | console.error(error); 88 | const errorResponse: APIGatewayProxyResult = { 89 | statusCode: 500, 90 | body: '', 91 | headers: {}, 92 | }; 93 | resolver.succeed(errorResponse); 94 | } 95 | 96 | private static getContentType(params: { contentTypeHeader: any }) { 97 | return params.contentTypeHeader 98 | ? params.contentTypeHeader.split(';')[0] 99 | : ''; 100 | } 101 | 102 | private static isContentTypeBinaryMimeType(params: { 103 | contentType: any; 104 | binaryMimeTypes: any; 105 | }) { 106 | return ( 107 | params.binaryMimeTypes.length > 0 && 108 | !!is(params.contentType, params.binaryMimeTypes) 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/socket-manager.ts: -------------------------------------------------------------------------------- 1 | export class SocketManager { 2 | public static getSocketPath(socketPathSuffix: string): string { 3 | if (/^win/.test(process.platform)) { 4 | // eslint-disable-next-line @typescript-eslint/no-var-requires 5 | const path = require('path'); 6 | return path.join( 7 | '\\\\?\\pipe', 8 | process.cwd(), 9 | `server-${socketPathSuffix}`, 10 | ); 11 | } else { 12 | return `/tmp/server-${socketPathSuffix}.sock`; 13 | } 14 | } 15 | 16 | public static getSocketSuffix(): string { 17 | return Math.random().toString(36).substring(2, 15); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true 15 | }, 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 150], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false 16 | }, 17 | "rulesDirectory": [] 18 | } 19 | --------------------------------------------------------------------------------