├── .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 |
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 | Test duration |
406 | 0 sec |
407 |
408 |
409 | Scenarios created |
410 | 0 |
411 |
412 |
413 |
414 | Scenarios completed |
415 | 0 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
Scenario counts:
424 |
425 |
426 |
427 |
429 |
430 |
431 |
432 |
433 |
434 |
Codes
435 |
436 |
437 |
438 |
440 |
441 |
442 |
443 |
444 |
445 |
Errors
446 |
447 |
448 |
449 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
Charts
458 |
459 |
460 |
461 |
464 |
465 |
468 |
469 |
470 |
471 |
472 |
473 |
474 |
475 |
478 |
479 |
482 |
485 |
486 |
489 |
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 | Test duration |
404 | 0 sec |
405 |
406 |
407 | Scenarios created |
408 | 0 |
409 |
410 |
411 |
412 | Scenarios completed |
413 | 0 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
Scenario counts:
422 |
423 |
424 |
425 |
427 |
428 |
429 |
430 |
431 |
432 |
Codes
433 |
434 |
435 |
436 |
438 |
439 |
440 |
441 |
442 |
443 |
Errors
444 |
445 |
446 |
447 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
Charts
456 |
457 |
458 |
459 |
462 |
463 |
466 |
467 |
468 |
469 |
470 |
471 |
472 |
473 |
476 |
477 |
480 |
483 |
484 |
487 |
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 |
--------------------------------------------------------------------------------