├── .editorconfig
├── .eslintrc.js
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ └── codeql.yml
├── .gitignore
├── .npmrc
├── .prettierrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── DOCS.md
├── LICENSE
├── README.md
├── __tests__
└── resources
│ ├── example-pet-api.openapi.json
│ ├── example-pet-api.openapi.yml
│ └── refs.openapi.json
├── examples
├── aws-cdk
│ ├── .gitignore
│ ├── README.md
│ ├── cdk.json
│ ├── index.test.ts
│ ├── jest.config.js
│ ├── openapi.yml
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── lambdas
│ │ │ └── api-entrypoint.lambda.ts
│ │ └── main.ts
│ └── tsconfig.json
├── aws-sam
│ ├── .gitignore
│ ├── README.md
│ ├── buildspec.yml
│ ├── env.json
│ ├── events
│ │ └── event.json
│ ├── index.test.ts
│ ├── index.ts
│ ├── jest.config.js
│ ├── openapi.yml
│ ├── package-lock.json
│ ├── package.json
│ ├── template.yml
│ └── tsconfig.json
├── aws-sst
│ ├── .gitignore
│ ├── LICENSE
│ ├── README.md
│ ├── package.json
│ ├── packages
│ │ ├── core
│ │ │ ├── openapi.yml
│ │ │ ├── package.json
│ │ │ ├── src
│ │ │ │ ├── definition.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── openapi.json
│ │ │ └── tsconfig.json
│ │ └── functions
│ │ │ ├── package.json
│ │ │ ├── src
│ │ │ └── lambda.ts
│ │ │ └── tsconfig.json
│ ├── pnpm-lock.yaml
│ ├── pnpm-workspace.yaml
│ ├── sst.config.ts
│ ├── stacks
│ │ └── OpenAPIStack.ts
│ └── tsconfig.json
├── azure-function
│ ├── .gitignore
│ ├── README.md
│ ├── handler
│ │ ├── function.json
│ │ ├── index.js
│ │ └── sample.dat
│ ├── host.json
│ ├── index.test.js
│ ├── jest.config.js
│ ├── local.settings.json
│ ├── package-lock.json
│ ├── package.json
│ └── proxies.json
├── bun
│ ├── .gitignore
│ ├── README.md
│ ├── index.test.ts
│ ├── index.ts
│ ├── package.json
│ └── tsconfig.json
├── express-apikey-auth
│ ├── .gitignore
│ ├── README.md
│ ├── index.js
│ ├── index.test.js
│ ├── jest.config.js
│ ├── openapi.yml
│ ├── package-lock.json
│ └── package.json
├── express-jwt-auth
│ ├── .gitignore
│ ├── README.md
│ ├── index.js
│ ├── index.test.js
│ ├── jest.config.js
│ ├── openapi.yml
│ ├── package-lock.json
│ └── package.json
├── express-ts-mock
│ ├── .gitignore
│ ├── README.md
│ ├── index.test.ts
│ ├── index.ts
│ ├── jest.config.js
│ ├── openapi.yml
│ ├── package-lock.json
│ ├── package.json
│ ├── tsconfig.json
│ └── types
│ │ └── wait-on.d.ts
├── express-typescript
│ ├── .gitignore
│ ├── README.md
│ ├── index.test.ts
│ ├── index.ts
│ ├── jest.config.js
│ ├── package-lock.json
│ ├── package.json
│ ├── tsconfig.json
│ └── types
│ │ └── wait-on.d.ts
├── express
│ ├── .gitignore
│ ├── README.md
│ ├── index.js
│ ├── index.test.js
│ ├── jest.config.js
│ ├── package-lock.json
│ └── package.json
├── fastify
│ ├── .gitignore
│ ├── README.md
│ ├── index.js
│ ├── index.test.js
│ ├── jest.config.js
│ ├── package-lock.json
│ └── package.json
├── hapi-typescript
│ ├── .gitignore
│ ├── README.md
│ ├── index.test.ts
│ ├── index.ts
│ ├── jest.config.js
│ ├── package-lock.json
│ ├── package.json
│ ├── tsconfig.json
│ └── types
│ │ └── wait-on.d.ts
├── koa
│ ├── .gitignore
│ ├── README.md
│ ├── index.js
│ ├── index.test.js
│ ├── jest.config.js
│ ├── package-lock.json
│ └── package.json
└── serverless-framework
│ ├── .gitignore
│ ├── README.md
│ ├── index.test.ts
│ ├── index.ts
│ ├── jest.config.js
│ ├── openapi.yml
│ ├── package-lock.json
│ ├── package.json
│ ├── serverless.yml
│ ├── tsconfig.json
│ └── types
│ └── wait-on.d.ts
├── header.png
├── jest.config.js
├── lgtm.yml
├── package-lock.json
├── package.json
├── src
├── backend.test.ts
├── backend.ts
├── index.ts
├── refparser.ts
├── router.test.ts
├── router.ts
├── types
│ └── swagger-parser.d.ts
├── utils.test.ts
├── utils.ts
├── validation.test.ts
└── validation.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
4 | parser: '@typescript-eslint/parser',
5 | plugins: ['@typescript-eslint'],
6 | root: true,
7 | ignorePatterns: ["examples/*"],
8 | env: {
9 | node: true,
10 | jest: true
11 | },
12 | rules: {
13 | "ordered-imports": 0,
14 | "object-literal-sort-keys": 0,
15 | "no-string-literal": 0,
16 | "object-literal-key-quotes": 0,
17 | "no-console": 0,
18 | "@typescript-eslint/no-explicit-any": 1,
19 | "@typescript-eslint/no-unused-vars": [1, { "argsIgnorePattern": "^_" }],
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: anttiviljami
2 | open_collective: openapi-stack
3 | custom:
4 | - https://buymeacoff.ee/anttiviljami
5 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | - package-ecosystem: "npm"
8 | directory: "/"
9 | schedule:
10 | interval: "daily"
11 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches: ["main"]
5 | tags: ["*"]
6 | pull_request:
7 | branches: ["main"]
8 |
9 | jobs:
10 | test:
11 | name: Test
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | project_dir:
16 | - .
17 | - examples/express
18 | - examples/express-apikey-auth
19 | - examples/express-jwt-auth
20 | - examples/express-ts-mock
21 | - examples/express-typescript
22 | - examples/fastify
23 | - examples/hapi-typescript
24 | - examples/koa
25 | - examples/serverless-framework
26 | - examples/aws-sam
27 | - examples/aws-cdk
28 | steps:
29 | - uses: actions/checkout@v4
30 | - uses: actions/setup-node@v4
31 | with:
32 | node-version: "16"
33 | - uses: actions/setup-python@v5
34 | with:
35 | python-version: "3.8"
36 | - uses: aws-actions/setup-sam@v2
37 | if: matrix.project_dir == 'examples/aws-sam' || matrix.project_dir == 'examples/aws-cdk'
38 | - run: npm ci && npm run build
39 | - run: npm ci
40 | working-directory: ${{ matrix.project_dir }}
41 | - run: npm link ../../
42 | if: matrix.project_dir != '.' && matrix.project_dir != 'examples/aws-sam'
43 | working-directory: ${{ matrix.project_dir }}
44 | - run: npm run lint
45 | if: matrix.project_dir == '.'
46 | working-directory: ${{ matrix.project_dir }}
47 | - run: npm test
48 | working-directory: ${{ matrix.project_dir }}
49 |
50 | publish:
51 | name: Publish
52 | runs-on: ubuntu-latest
53 | if: startsWith(github.ref, 'refs/tags/')
54 | needs:
55 | - test
56 | steps:
57 | - uses: actions/checkout@v4
58 | - uses: actions/setup-node@v4
59 | with:
60 | node-version: "18"
61 | registry-url: https://registry.npmjs.org/
62 | - run: npm ci
63 | - run: npm publish || true
64 | env:
65 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
66 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 | tags: ["*"]
7 | pull_request:
8 | branches: ["main"]
9 | schedule:
10 | - cron: "56 9 * * 2"
11 |
12 | jobs:
13 | analyze:
14 | name: Analyze
15 | runs-on: ubuntu-latest
16 | permissions:
17 | actions: read
18 | contents: read
19 | security-events: write
20 |
21 | strategy:
22 | fail-fast: false
23 | matrix:
24 | language: [javascript]
25 |
26 | steps:
27 | - name: Checkout
28 | uses: actions/checkout@v4
29 |
30 | - name: Initialize CodeQL
31 | uses: github/codeql-action/init@v3
32 | with:
33 | languages: ${{ matrix.language }}
34 | queries: +security-and-quality
35 |
36 | - name: Autobuild
37 | uses: github/codeql-action/autobuild@v3
38 |
39 | - name: Perform CodeQL Analysis
40 | uses: github/codeql-action/analyze@v3
41 | with:
42 | category: "/language:${{ matrix.language }}"
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # build
2 | *.js
3 | *.js.map
4 | *.d.ts
5 |
6 | # include js scripts
7 | !scripts/*.js
8 |
9 | # include types
10 | !**/types/*.d.ts
11 |
12 | # include jest config
13 | !jest.config.js
14 | !.eslintrc.js
15 |
16 | # npm
17 | node_modules
18 | npm_debug.log*
19 |
20 | # vscode
21 | .vscode
22 |
23 | # idea
24 | .idea
25 |
26 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | tag-version-prefix=""
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "typescript",
3 | "arrowParens": "always",
4 | "trailingComma": "all",
5 | "singleQuote": true,
6 | "printWidth": 120
7 | }
8 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | viljami@viljami.io.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | OpenAPI Backend is Free and Open Source Software. Issues and pull requests are more than welcome!
4 |
--------------------------------------------------------------------------------
/DOCS.md:
--------------------------------------------------------------------------------
1 | # Documentation
2 |
3 | OpenAPI Backend documentation has moved to [openapistack.co](https://openapistack.co)
4 |
5 | See: https://openapistack.co/docs/openapi-backend/intro
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Viljami Kuosmanen
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 | [](https://github.com/openapistack/openapi-backend/actions?query=workflow%3ACI)
6 | [](https://github.com/openapistack/openapi-backend/blob/main/LICENSE)
7 | [](https://www.npmjs.com/package/openapi-backend)
8 | [](https://www.npmjs.com/package/openapi-backend)
9 | [](https://www.npmjs.com/package/openapi-backend?activeTab=dependencies)
10 | 
11 | [](https://buymeacoff.ee/anttiviljami)
12 |
13 | Build, Validate, Route, Authenticate, and Mock using OpenAPI definitions.
14 |
15 | OpenAPI Backend is a Framework-agnostic middleware tool for building beautiful APIs with OpenAPI Specification.
16 |
17 | ## Features
18 |
19 | - [x] Build APIs by describing them in [OpenAPI specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md)
20 | - [x] Register handlers for [operationIds](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#fixed-fields-8)
21 | to route requests in your favourite Node.js backend
22 | - [x] Use [JSON Schema](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#data-types) to validate
23 | API requests and/or responses. OpenAPI Backend uses the [AJV](https://ajv.js.org/) library under the hood for performant validation
24 | - [x] Register Auth / Security Handlers for [OpenAPI Security Schemes](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securitySchemeObject)
25 | to authorize API requests
26 | - [x] Auto-mock API responses using [OpenAPI examples objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#example-object)
27 | or [JSON Schema definitions](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schema-object)
28 | - [x] Built with TypeScript, types included
29 | - [x] Optimised runtime routing and validation. **No generated code!**
30 | - [x] OpenAPI 3.1 support
31 |
32 | ## Documentation
33 |
34 | **New!** OpenAPI Backend documentation is now found on [openapistack.co](https://openapistack.co)
35 |
36 | https://openapistack.co/docs/openapi-backend/intro
37 |
38 | ## Quick Start
39 |
40 | Full [example projects](https://github.com/openapistack/openapi-backend/tree/main/examples) included in the repo
41 |
42 | ```
43 | npm install --save openapi-backend
44 | ```
45 |
46 | ```javascript
47 | import OpenAPIBackend from 'openapi-backend';
48 |
49 | // create api with your definition file or object
50 | const api = new OpenAPIBackend({ definition: './petstore.yml' });
51 |
52 | // register your framework specific request handlers here
53 | api.register({
54 | getPets: (c, req, res) => res.status(200).json({ result: 'ok' }),
55 | getPetById: (c, req, res) => res.status(200).json({ result: 'ok' }),
56 | validationFail: (c, req, res) => res.status(400).json({ err: c.validation.errors }),
57 | notFound: (c, req, res) => res.status(404).json({ err: 'not found' }),
58 | });
59 |
60 | // initalize the backend
61 | api.init();
62 | ```
63 |
64 | ### Express
65 |
66 | ```javascript
67 | import express from 'express';
68 |
69 | const app = express();
70 | app.use(express.json());
71 | app.use((req, res) => api.handleRequest(req, req, res));
72 | app.listen(9000);
73 | ```
74 |
75 | [See full Express example](https://github.com/openapistack/openapi-backend/tree/main/examples/express)
76 |
77 | [See full Express TypeScript example](https://github.com/openapistack/openapi-backend/tree/main/examples/express-typescript)
78 |
79 | ### AWS Serverless (Lambda)
80 |
81 | ```javascript
82 | // API Gateway Proxy handler
83 | module.exports.handler = (event, context) =>
84 | api.handleRequest(
85 | {
86 | method: event.httpMethod,
87 | path: event.path,
88 | query: event.queryStringParameters,
89 | body: event.body,
90 | headers: event.headers,
91 | },
92 | event,
93 | context,
94 | );
95 | ```
96 |
97 | [See full AWS SAM example](https://github.com/openapistack/openapi-backend/tree/main/examples/aws-sam)
98 |
99 | [See full AWS CDK example](https://github.com/openapistack/openapi-backend/tree/main/examples/aws-cdk)
100 |
101 | [See full SST example](https://github.com/openapistack/openapi-backend/tree/main/examples/aws-sst)
102 |
103 | [See full Serverless Framework example](https://github.com/openapistack/openapi-backend/tree/main/examples/serverless-framework)
104 |
105 | ### Azure Function
106 |
107 | ```javascript
108 | module.exports = (context, req) =>
109 | api.handleRequest(
110 | {
111 | method: req.method,
112 | path: req.params.path,
113 | query: req.query,
114 | body: req.body,
115 | headers: req.headers,
116 | },
117 | context,
118 | req,
119 | );
120 | ```
121 |
122 | [See full Azure Function example](https://github.com/openapistack/openapi-backend/tree/main/examples/azure-function)
123 |
124 | ### Fastify
125 |
126 | ```ts
127 | import fastify from 'fastify';
128 |
129 | fastify.route({
130 | method: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
131 | url: '/*',
132 | handler: async (request, reply) =>
133 | api.handleRequest(
134 | {
135 | method: request.method,
136 | path: request.url,
137 | body: request.body,
138 | query: request.query,
139 | headers: request.headers,
140 | },
141 | request,
142 | reply,
143 | ),
144 | });
145 | fastify.listen();
146 | ```
147 |
148 | [See full Fastify example](https://github.com/openapistack/openapi-backend/tree/main/examples/fastify)
149 |
150 | ### Hapi
151 |
152 | ```javascript
153 | import Hapi from '@hapi/hapi';
154 |
155 | const server = new Hapi.Server({ host: '0.0.0.0', port: 9000 });
156 | server.route({
157 | method: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
158 | path: '/{path*}',
159 | handler: (req, h) =>
160 | api.handleRequest(
161 | {
162 | method: req.method,
163 | path: req.path,
164 | body: req.payload,
165 | query: req.query,
166 | headers: req.headers,
167 | },
168 | req,
169 | h,
170 | ),
171 | });
172 | server.start();
173 | ```
174 |
175 | [See full Hapi example](https://github.com/openapistack/openapi-backend/tree/main/examples/hapi-typescript)
176 |
177 |
178 | ### Koa
179 |
180 | ```javascript
181 | import Koa from 'koa';
182 | import bodyparser from 'koa-bodyparser';
183 |
184 | const app = new Koa();
185 |
186 | app.use(bodyparser());
187 | app.use((ctx) =>
188 | api.handleRequest(
189 | ctx.request,
190 | ctx,
191 | ),
192 | );
193 | app.listen(9000);
194 | ```
195 |
196 | [See full Koa example](https://github.com/openapistack/openapi-backend/tree/main/examples/koa)
197 |
198 | ## Registering Handlers for Operations
199 |
200 | Handlers are registered for [`operationIds`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#fixed-fields-8)
201 | found in the OpenAPI definitions. You can register handlers as shown above with [`new OpenAPIBackend()`](https://github.com/openapistack/openapi-backend/blob/main/DOCS.md#parameter-optshandlers)
202 | constructor opts, or using the [`register()`](https://github.com/openapistack/openapi-backend/blob/main/DOCS.md#registeroperationid-handler)
203 | method.
204 |
205 | ```javascript
206 | async function getPetByIdHandler(c, req, res) {
207 | const id = c.request.params.id;
208 | const pet = await pets.getPetById(id);
209 | return res.status(200).json({ result: pet });
210 | }
211 | api.register('getPetById', getPetByIdHandler);
212 | // or
213 | api.register({
214 | getPetById: getPetByIdHandler,
215 | });
216 | ```
217 |
218 | Operation handlers are passed a special [Context object](https://github.com/openapistack/openapi-backend/blob/main/DOCS.md#context-object)
219 | as the first argument, which contains the parsed request, the
220 | matched API operation and input validation results. The other arguments in the example above are Express-specific
221 | handler arguments.
222 |
223 | ## Request validation
224 |
225 | The easiest way to enable request validation in your API is to register a [`validationFail`](https://github.com/openapistack/openapi-backend/blob/main/DOCS.md#validationfail-handler)
226 | handler.
227 |
228 | ```javascript
229 | function validationFailHandler(c, req, res) {
230 | return res.status(400).json({ status: 400, err: c.validation.errors });
231 | }
232 | api.register('validationFail', validationFailHandler);
233 | ```
234 |
235 | Once registered, this handler gets called if any JSON Schemas in either operation parameters (in: path, query, header,
236 | cookie) or requestPayload don't match the request.
237 |
238 | The context object `c` gets a `validation` property with the [validation result](https://github.com/openapistack/openapi-backend/blob/main/DOCS.md#validationresult-object).
239 |
240 | ## Response validation
241 |
242 | OpenAPIBackend doesn't automatically perform response validation for your handlers, but you can register a
243 | [`postResponseHandler`](https://github.com/openapistack/openapi-backend/blob/main/DOCS.md#postresponsehandler-handler)
244 | to add a response validation step using [`validateResponse`](https://github.com/openapistack/openapi-backend/blob/main/DOCS.md#validateresponseres-operation).
245 |
246 | ```javascript
247 | api.register({
248 | getPets: (c) => {
249 | // when a postResponseHandler is registered, your operation handlers' return value gets passed to context.response
250 | return [{ id: 1, name: 'Garfield' }];
251 | },
252 | postResponseHandler: (c, req, res) => {
253 | const valid = c.api.validateResponse(c.response, c.operation);
254 | if (valid.errors) {
255 | // response validation failed
256 | return res.status(502).json({ status: 502, err: valid.errors });
257 | }
258 | return res.status(200).json(c.response);
259 | },
260 | });
261 | ```
262 |
263 | It's also possible to validate the response headers using [`validateResponseHeaders`](https://github.com/openapistack/openapi-backend/blob/main/DOCS.md#validateresponseheadersheaders-operation-opts).
264 |
265 | ```javascript
266 | api.register({
267 | getPets: (c) => {
268 | // when a postResponseHandler is registered, your operation handlers' return value gets passed to context.response
269 | return [{ id: 1, name: 'Garfield' }];
270 | },
271 | postResponseHandler: (c, req, res) => {
272 | const valid = c.api.validateResponseHeaders(res.headers, c.operation, {
273 | statusCode: res.statusCode,
274 | setMatchType: 'exact',
275 | });
276 | if (valid.errors) {
277 | // response validation failed
278 | return res.status(502).json({ status: 502, err: valid.errors });
279 | }
280 | return res.status(200).json(c.response);
281 | },
282 | });
283 | ```
284 |
285 | ## Auth / Security Handlers
286 |
287 | If your OpenAPI definition contains [Security Schemes](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securitySchemeObject)
288 | you can register security handlers to handle authorization for your API:
289 |
290 | ```yaml
291 | components:
292 | securitySchemes:
293 | - ApiKey:
294 | type: apiKey
295 | in: header
296 | name: x-api-key
297 | security:
298 | - ApiKey: []
299 | ```
300 |
301 | ```javascript
302 | api.registerSecurityHandler('ApiKey', (c) => {
303 | const authorized = c.request.headers['x-api-key'] === 'SuperSecretPassword123';
304 | // truthy return values are interpreted as auth success
305 | // you can also add any auth information to the return value
306 | return authorized;
307 | });
308 | ```
309 |
310 | The authorization status and return values of each security handler can be
311 | accessed via the [Context Object](https://github.com/openapistack/openapi-backend/blob/main/DOCS.md#context-object)
312 |
313 | You can also register an [`unauthorizedHandler`](https://github.com/openapistack/openapi-backend/blob/main/DOCS.md#unauthorizedhandler-handler)
314 | to handle unauthorized requests.
315 |
316 | ```javascript
317 | api.register('unauthorizedHandler', (c, req, res) => {
318 | return res.status(401).json({ err: 'unauthorized' })
319 | });
320 | ```
321 |
322 | See examples:
323 | - [API Key auth (express)](https://github.com/openapistack/openapi-backend/tree/main/examples/express-apikey-auth)
324 | - [JWT auth (express)](https://github.com/openapistack/openapi-backend/tree/main/examples/express-jwt-auth)
325 |
326 | ## Mocking API responses
327 |
328 | Mocking APIs just got really easy with OpenAPI Backend! Register a [`notImplemented`](https://github.com/openapistack/openapi-backend/blob/main/DOCS.md#notimplemented-handler)
329 | handler and use [`mockResponseForOperation()`](https://github.com/openapistack/openapi-backend/blob/main/DOCS.md##mockresponseforoperationoperationid-opts)
330 | to generate mock responses for operations with no custom handlers specified yet:
331 |
332 | ```javascript
333 | api.register('notImplemented', (c, req, res) => {
334 | const { status, mock } = c.api.mockResponseForOperation(c.operation.operationId);
335 | return res.status(status).json(mock);
336 | });
337 | ```
338 |
339 | OpenAPI Backend supports mocking responses using both OpenAPI example objects and JSON Schema:
340 | ```yaml
341 | paths:
342 | '/pets':
343 | get:
344 | operationId: getPets
345 | summary: List pets
346 | responses:
347 | 200:
348 | $ref: '#/components/responses/PetListWithExample'
349 | '/pets/{id}':
350 | get:
351 | operationId: getPetById
352 | summary: Get pet by its id
353 | responses:
354 | 200:
355 | $ref: '#/components/responses/PetResponseWithSchema'
356 | components:
357 | responses:
358 | PetListWithExample:
359 | description: List of pets
360 | content:
361 | 'application/json':
362 | example:
363 | - id: 1
364 | name: Garfield
365 | - id: 2
366 | name: Odie
367 | PetResponseWithSchema:
368 | description: A single pet
369 | content:
370 | 'application/json':
371 | schema:
372 | type: object
373 | properties:
374 | id:
375 | type: integer
376 | minimum: 1
377 | name:
378 | type: string
379 | example: Garfield
380 | ```
381 |
382 | The example above will yield:
383 | ```javascript
384 | api.mockResponseForOperation('getPets'); // => { status: 200, mock: [{ id: 1, name: 'Garfield' }, { id: 2, name: 'Odie' }]}
385 | api.mockResponseForOperation('getPetById'); // => { status: 200, mock: { id: 1, name: 'Garfield' }}
386 | ```
387 |
388 | [See full Mock API example on Express](https://github.com/openapistack/openapi-backend/tree/main/examples/express-ts-mock)
389 |
390 | ## Commercial support
391 |
392 | For assistance with integrating openapi-backend in your company, reach out at support@openapistack.co.
393 |
394 | ## Contributing
395 |
396 | OpenAPI Backend is Free and Open Source Software. Issues and pull requests are more than welcome!
397 |
398 |
--------------------------------------------------------------------------------
/__tests__/resources/example-pet-api.openapi.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.0",
3 | "info": {
4 | "title": "Example API",
5 | "description": "Example CRUD API for pets",
6 | "version": "1.0.0"
7 | },
8 | "tags": [
9 | {
10 | "name": "pets",
11 | "description": "Pet operations"
12 | }
13 | ],
14 | "paths": {
15 | "/pets": {
16 | "get": {
17 | "operationId": "getPets",
18 | "summary": "List pets",
19 | "description": "Returns all pets in database",
20 | "tags": [
21 | "pets"
22 | ],
23 | "responses": {
24 | "200": {
25 | "description": "List of pets in database"
26 | }
27 | },
28 | "parameters": [
29 | {
30 | "name": "limit",
31 | "in": "query",
32 | "description": "Number of items to return",
33 | "required": false,
34 | "schema": {
35 | "$ref": "#/components/schemas/QueryLimit"
36 | }
37 | },
38 | {
39 | "name": "offset",
40 | "in": "query",
41 | "description": "Starting offset for returning items",
42 | "required": false,
43 | "schema": {
44 | "$ref": "#/components/schemas/QueryOffset"
45 | }
46 | }
47 | ]
48 | },
49 | "post": {
50 | "operationId": "createPet",
51 | "summary": "Create a pet",
52 | "description": "Crete a new pet into the database",
53 | "tags": [
54 | "pets"
55 | ],
56 | "responses": {
57 | "201": {
58 | "description": "Pet created succesfully"
59 | }
60 | },
61 | "requestBody": {
62 | "$ref": "#/components/requestBodies/PetPayload"
63 | }
64 | }
65 | },
66 | "/pets/{id}": {
67 | "get": {
68 | "operationId": "getPetById",
69 | "summary": "Get a pet",
70 | "description": "Returns a pet by its id in database",
71 | "tags": [
72 | "pets"
73 | ],
74 | "responses": {
75 | "200": {
76 | "description": "Pet object corresponding to id"
77 | },
78 | "404": {
79 | "description": "Pet not found"
80 | }
81 | },
82 | "parameters": [
83 | {
84 | "name": "id",
85 | "in": "path",
86 | "description": "Unique identifier for pet in database",
87 | "required": true,
88 | "schema": {
89 | "$ref": "#/components/schemas/PetId"
90 | }
91 | }
92 | ]
93 | },
94 | "put": {
95 | "operationId": "replacePetById",
96 | "summary": "Replace pet",
97 | "description": "Replace an existing pet in the database",
98 | "tags": [
99 | "pets"
100 | ],
101 | "responses": {
102 | "200": {
103 | "description": "Pet replaced succesfully"
104 | },
105 | "404": {
106 | "description": "Pet not found"
107 | }
108 | },
109 | "parameters": [
110 | {
111 | "name": "id",
112 | "in": "path",
113 | "description": "Unique identifier for pet in database",
114 | "required": true,
115 | "schema": {
116 | "$ref": "#/components/schemas/PetId"
117 | }
118 | }
119 | ],
120 | "requestBody": {
121 | "$ref": "#/components/requestBodies/PetPayload"
122 | }
123 | },
124 | "patch": {
125 | "operationId": "updatePetById",
126 | "summary": "Update pet",
127 | "description": "Update an existing pet in the database",
128 | "tags": [
129 | "pets"
130 | ],
131 | "responses": {
132 | "200": {
133 | "description": "Pet updated succesfully"
134 | },
135 | "404": {
136 | "description": "Pet not found"
137 | }
138 | },
139 | "parameters": [
140 | {
141 | "name": "id",
142 | "in": "path",
143 | "description": "Unique identifier for pet in database",
144 | "required": true,
145 | "schema": {
146 | "$ref": "#/components/schemas/PetId"
147 | }
148 | }
149 | ],
150 | "requestBody": {
151 | "$ref": "#/components/requestBodies/PetPayload"
152 | }
153 | },
154 | "delete": {
155 | "operationId": "deletePetById",
156 | "summary": "Delete a pet",
157 | "description": "Deletes a pet by its id in database",
158 | "tags": [
159 | "pets"
160 | ],
161 | "responses": {
162 | "200": {
163 | "description": "Pet deleted succesfully"
164 | },
165 | "404": {
166 | "description": "Pet not found"
167 | }
168 | },
169 | "parameters": [
170 | {
171 | "name": "id",
172 | "in": "path",
173 | "description": "Unique identifier for pet in database",
174 | "required": true,
175 | "schema": {
176 | "$ref": "#/components/schemas/PetId"
177 | }
178 | }
179 | ]
180 | }
181 | },
182 | "/pets/{id}/human": {
183 | "get": {
184 | "operationId": "getHumanByPetId",
185 | "summary": "Get a pet's human",
186 | "description": "Get the human for a pet",
187 | "tags": [
188 | "pets"
189 | ],
190 | "responses": {
191 | "200": {
192 | "description": "Human corresponding pet id"
193 | },
194 | "404": {
195 | "description": "Human or pet not found"
196 | }
197 | },
198 | "parameters": [
199 | {
200 | "name": "id",
201 | "in": "path",
202 | "description": "Unique identifier for pet in database",
203 | "required": true,
204 | "schema": {
205 | "$ref": "#/components/schemas/PetId"
206 | }
207 | }
208 | ]
209 | }
210 | },
211 | "/pets/meta": {
212 | "get": {
213 | "operationId": "getPetsMeta",
214 | "summary": "Get pet metadata",
215 | "description": "Returns a list of metadata about pets and their relations in the database",
216 | "tags": [
217 | "pets"
218 | ],
219 | "responses": {
220 | "200": {
221 | "description": "Metadata for pets"
222 | }
223 | }
224 | }
225 | }
226 | },
227 | "components": {
228 | "schemas": {
229 | "PetId": {
230 | "description": "Unique identifier for pet in database",
231 | "example": 1,
232 | "title": "PetId",
233 | "type": "integer"
234 | },
235 | "PetPayload": {
236 | "type": "object",
237 | "properties": {
238 | "name": {
239 | "description": "Name of the pet",
240 | "example": "Garfield",
241 | "title": "PetName",
242 | "type": "string"
243 | }
244 | },
245 | "additionalProperties": false,
246 | "required": [
247 | "name"
248 | ]
249 | },
250 | "QueryLimit": {
251 | "description": "Number of items to return",
252 | "example": 25,
253 | "title": "QueryLimit",
254 | "type": "integer"
255 | },
256 | "QueryOffset": {
257 | "description": "Starting offset for returning items",
258 | "example": 0,
259 | "title": "QueryOffset",
260 | "type": "integer",
261 | "minimum": 0
262 | }
263 | },
264 | "requestBodies": {
265 | "PetPayload": {
266 | "description": "Request payload containing a pet object",
267 | "content": {
268 | "application/json": {
269 | "schema": {
270 | "$ref": "#/components/schemas/PetPayload"
271 | }
272 | }
273 | }
274 | }
275 | }
276 | }
277 | }
--------------------------------------------------------------------------------
/__tests__/resources/example-pet-api.openapi.yml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | title: Example API
4 | description: Example CRUD API for pets
5 | version: 1.0.0
6 | tags:
7 | - name: pets
8 | description: Pet operations
9 | paths:
10 | /pets:
11 | get:
12 | operationId: getPets
13 | summary: List pets
14 | description: Returns all pets in database
15 | tags:
16 | - pets
17 | responses:
18 | '200':
19 | description: List of pets in database
20 | parameters:
21 | - name: limit
22 | in: query
23 | description: Number of items to return
24 | required: false
25 | schema:
26 | $ref: '#/components/schemas/QueryLimit'
27 | - name: offset
28 | in: query
29 | description: Starting offset for returning items
30 | required: false
31 | schema:
32 | $ref: '#/components/schemas/QueryOffset'
33 | post:
34 | operationId: createPet
35 | summary: Create a pet
36 | description: Crete a new pet into the database
37 | tags:
38 | - pets
39 | responses:
40 | '201':
41 | description: Pet created succesfully
42 | parameters: []
43 | requestBody:
44 | $ref: '#/components/requestBodies/PetPayload'
45 | '/pets/{id}':
46 | get:
47 | operationId: getPetById
48 | summary: Get a pet
49 | description: Returns a pet by its id in database
50 | tags:
51 | - pets
52 | responses:
53 | '200':
54 | description: Pet object corresponding to id
55 | '404':
56 | description: Pet not found
57 | parameters:
58 | - name: id
59 | in: path
60 | description: Unique identifier for pet in database
61 | required: true
62 | schema:
63 | $ref: '#/components/schemas/PetId'
64 | put:
65 | operationId: replacePetById
66 | summary: Replace pet
67 | description: Replace an existing pet in the database
68 | tags:
69 | - pets
70 | responses:
71 | '200':
72 | description: Pet replaced succesfully
73 | '404':
74 | description: Pet not found
75 | parameters:
76 | - name: id
77 | in: path
78 | description: Unique identifier for pet in database
79 | required: true
80 | schema:
81 | $ref: '#/components/schemas/PetId'
82 | requestBody:
83 | $ref: '#/components/requestBodies/PetPayload'
84 | patch:
85 | operationId: updatePetById
86 | summary: Update pet
87 | description: Update an existing pet in the database
88 | tags:
89 | - pets
90 | responses:
91 | '200':
92 | description: Pet updated succesfully
93 | '404':
94 | description: Pet not found
95 | parameters:
96 | - name: id
97 | in: path
98 | description: Unique identifier for pet in database
99 | required: true
100 | schema:
101 | $ref: '#/components/schemas/PetId'
102 | requestBody:
103 | $ref: '#/components/requestBodies/PetPayload'
104 | delete:
105 | operationId: deletePetById
106 | summary: Delete a pet
107 | description: Deletes a pet by its id in database
108 | tags:
109 | - pets
110 | responses:
111 | '200':
112 | description: Pet deleted succesfully
113 | '404':
114 | description: Pet not found
115 | parameters:
116 | - name: id
117 | in: path
118 | description: Unique identifier for pet in database
119 | required: true
120 | schema:
121 | $ref: '#/components/schemas/PetId'
122 | '/pets/{id}/human':
123 | get:
124 | operationId: getHumanByPetId
125 | summary: Get a pet's human
126 | description: Get the human for a pet
127 | tags:
128 | - pets
129 | responses:
130 | '200':
131 | description: Human corresponding pet id
132 | '404':
133 | description: Human or pet not found
134 | parameters:
135 | - name: id
136 | in: path
137 | description: Unique identifier for pet in database
138 | required: true
139 | schema:
140 | $ref: '#/components/schemas/PetId'
141 | /pets/meta:
142 | get:
143 | operationId: getPetsMeta
144 | summary: Get pet metadata
145 | description: Returns a list of metadata about pets and their relations in the database
146 | tags:
147 | - pets
148 | responses:
149 | '200':
150 | description: Metadata for pets
151 | components:
152 | schemas:
153 | PetId:
154 | description: Unique identifier for pet in database
155 | example: 1
156 | title: PetId
157 | type: integer
158 | PetPayload:
159 | type: object
160 | properties:
161 | name:
162 | description: Name of the pet
163 | example: Garfield
164 | title: PetName
165 | type: string
166 | additionalProperties: false
167 | required:
168 | - name
169 | QueryLimit:
170 | description: Number of items to return
171 | example: 25
172 | title: QueryLimit
173 | type: integer
174 | QueryOffset:
175 | description: Starting offset for returning items
176 | example: 0
177 | title: QueryOffset
178 | type: integer
179 | minimum: 0
180 | requestBodies:
181 | PetPayload:
182 | description: 'Request payload containing a pet object'
183 | content:
184 | application/json:
185 | schema:
186 | $ref: '#/components/schemas/PetPayload'
187 |
--------------------------------------------------------------------------------
/__tests__/resources/refs.openapi.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.3",
3 | "info": {
4 | "title": "Circular References",
5 | "description": "API with circular references",
6 | "version": "1.0.0"
7 | },
8 | "paths": {
9 | "/trees": {
10 | "post": {
11 | "operationId": "createTree",
12 | "responses": {
13 | "200": {
14 | "description": "ok"
15 | }
16 | },
17 | "requestBody": {
18 | "$ref": "#/components/requestBodies/CreateTree"
19 | }
20 | },
21 | "get": {
22 | "operationId": "getTrees",
23 | "responses": {
24 | "200": {
25 | "$ref": "#/components/schemas/BinTree"
26 | }
27 | },
28 | "parameters": [
29 | {
30 | "name": "subtree",
31 | "in": "query",
32 | "description": "Filter trees by existance of a subtree",
33 | "required": false,
34 | "schema": {
35 | "$ref": "#/components/schemas/BinTree"
36 | }
37 | }
38 | ]
39 | }
40 | }
41 | },
42 | "components": {
43 | "schemas": {
44 | "BinTree": {
45 | "title": "BinTree",
46 | "type": "object",
47 | "nullable": true,
48 | "properties": {
49 | "left": {
50 | "allOf": [{ "$ref": "#/components/schemas/BinTree" }]
51 | },
52 | "right": {
53 | "allOf": [{ "$ref": "#/components/schemas/BinTree" }]
54 | },
55 | "value": {
56 | "type": "number"
57 | }
58 | }
59 | }
60 | },
61 | "requestBodies": {
62 | "CreateTree": {
63 | "content": {
64 | "application/json": {
65 | "schema": { "$ref": "#/components/schemas/BinTree" }
66 | }
67 | }
68 | }
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/examples/aws-cdk/.gitignore:
--------------------------------------------------------------------------------
1 | !/package.json
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 | lib-cov
14 | coverage
15 | *.lcov
16 | .nyc_output
17 | build/Release
18 | node_modules/
19 | jspm_packages/
20 | *.tsbuildinfo
21 | .eslintcache
22 | *.tgz
23 | .yarn-integrity
24 | .cache
25 | /test-reports/
26 | junit.xml
27 | /coverage/
28 | !/tsconfig.json
29 | !/src/
30 | /lib
31 | /dist/
32 | /assets/
33 | !/cdk.json
34 | /cdk.out/
35 | .cdk.staging/
36 | .parcel-cache/
37 |
--------------------------------------------------------------------------------
/examples/aws-cdk/README.md:
--------------------------------------------------------------------------------
1 | # OpenAPI Backend AWS CDK Example
2 |
3 | [](http://anttiviljami.mit-license.org)
4 |
5 | Example project using [openapi-backend](https://github.com/openapistack/openapi-backend) with [AWS CDK](https://aws.amazon.com/cdk/)
6 |
7 | ## QuickStart
8 |
9 | ### Requirements
10 |
11 | - NodeJS and NPM
12 | - AWS and AWS CDK
13 | - (optional) [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-reference.html#serverless-sam-cli) (>= v1.65) and [Docker](https://docs.docker.com/get-docker/) for local `npm start` and `npm test`
14 |
15 | #### AWS Authentication
16 |
17 | Make sure to set up your AWS authentication correctly. This example assume you are using AWS SSO profiles, make sure you are authenticated with the correct profile. E.g. `aws sso login --profile your-profile`
18 |
19 | #### CDK Bootstrap
20 |
21 | To use this example, you need to make sure CDK is bootstrapped for your AWS account and region.
22 |
23 | ```bash
24 | AWS_PROFILE=your-profile npx cdk bootstrap aws://YOUR_ACCOUNT_ID/YOUR_DEFAULT_REGION
25 | ```
26 |
27 | ### Run and Test
28 |
29 | ```bash
30 | AWS_PROFILE=your-profile npx cdk deploy
31 | ```
32 |
33 | To try the endpoints, first copy the API GW URL that the `npx cdk deploy` command has outputted. The output will look like;
34 |
35 | ```
36 | openapi-backend-example.OpenAPIBackendHttpApiEndpoint = https://ie88ixpq7g.execute-api.eu-west-1.amazonaws.com
37 | ```
38 |
39 | Set an environment variable for making the further calls easier:
40 | ```bash
41 | export CDK_OUTPUT_API_GW_URL=https://ie88ixpq7g.execute-api.eu-west-1.amazonaws.com
42 | ```
43 |
44 | Try the endpoints:
45 |
46 | ```bash
47 | curl -i "$CDK_OUTPUT_API_GW_URL/pets"
48 | curl -i "$CDK_OUTPUT_API_GW_URL/pets/1"
49 | ```
50 |
51 | ### Clean Up the Resources
52 |
53 | If you would like to remove the example from your AWS account, run:
54 |
55 | ```bash
56 | AWS_PROFILE=your-profile npx cdk destroy
57 | ```
58 |
59 | ## ! IMPORTANT ! Caveats with the Example
60 |
61 | - [HttpApi CORS Settings](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-cors.html) related problems maybe hard to troubleshoot. If you are calling endpoints with `OPTIONS` method manually, make sure you are providing all necessary request headers(E.g. `Origin`, `Access-Control-Request-Method`) properly. Otherwise, you will get `204 No Content` response without any proper CORS response headers.
62 | - You may use `$default` integration of `HttpApi` if you don't need to set up CORS for your API, or you would like to set up CORS on application side.
63 | - `source-map-support/register` takes a signification amount of time to convert error stacktraces to proper ones. You may want to optimize or disable `source-map-support` when deploying to production environment.
64 |
--------------------------------------------------------------------------------
/examples/aws-cdk/cdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": "npx ts-node -P tsconfig.json --prefer-ts-exts src/main.ts",
3 | "output": "cdk.out",
4 | "watch": {
5 | "include": [
6 | "src/**/*.ts"
7 | ],
8 | "exclude": [
9 | "README.md",
10 | "cdk*.json",
11 | "**/*.d.ts",
12 | "**/*.js",
13 | "tsconfig.json",
14 | "package*.json",
15 | "yarn.lock",
16 | "node_modules"
17 | ]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/aws-cdk/index.test.ts:
--------------------------------------------------------------------------------
1 | import { spawn, ChildProcess } from 'child_process';
2 | import axios, { AxiosInstance } from 'axios';
3 | import waitOn from 'wait-on';
4 |
5 | jest.setTimeout(90000);
6 |
7 | describe('aws cdk example', () => {
8 | let start: ChildProcess;
9 | let client: AxiosInstance;
10 |
11 | beforeAll(async () => {
12 | client = axios.create({ baseURL: 'http://localhost:3000', validateStatus: () => true });
13 | start = spawn('npm', ['start'], { cwd: __dirname, detached: true, stdio: 'inherit' });
14 | await waitOn({ resources: ['tcp:localhost:3000'] });
15 | });
16 |
17 | afterAll(() => process.kill(-start.pid));
18 |
19 | test('GET /pets returns 200 with matched operation', async () => {
20 | const res = await client.get('/pets');
21 | expect(res.status).toBe(200);
22 | expect(res.data).toEqual({ operationId: 'getPets' });
23 | });
24 |
25 | test('GET /pets/1 returns 200 with matched operation', async () => {
26 | const res = await client.get('/pets/1');
27 | expect(res.status).toBe(200);
28 | expect(res.data).toEqual({ operationId: 'getPetById', id: '1' });
29 | });
30 |
31 | test('GET /pets/1a returns 400 with validation error', async () => {
32 | const res = await client.get('/pets/1a');
33 | expect(res.status).toBe(400);
34 | expect(res.data).toHaveProperty('err');
35 | });
36 |
37 | test('GET /unknown returns 404', async () => {
38 | const res = await client.get('/unknown');
39 | expect(res.status).toBe(404);
40 | expect(res.data).toHaveProperty('err');
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/examples/aws-cdk/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | testMatch: [ '**/?(*.)+(spec|test).ts?(x)' ]
5 | };
6 |
--------------------------------------------------------------------------------
/examples/aws-cdk/openapi.yml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.1
2 | info:
3 | title: 'My API'
4 | version: 1.0.0
5 | paths:
6 | '/pets':
7 | get:
8 | operationId: getPets
9 | responses:
10 | '200':
11 | description: ok
12 | '/pets/{id}':
13 | get:
14 | operationId: getPetById
15 | responses:
16 | '200':
17 | description: ok
18 | parameters:
19 | - name: id
20 | in: path
21 | required: true
22 | schema:
23 | type: integer
24 |
--------------------------------------------------------------------------------
/examples/aws-cdk/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "license": "MIT",
3 | "name": "openapi-backend-example",
4 | "description": "OpenAPI Backend Example running on AWS",
5 | "version": "0.0.1",
6 | "scripts": {
7 | "deploy": "npx cdk deploy",
8 | "destroy": "npx cdk destroy",
9 | "start": "npx cdk synth && sam local start-api -t ./cdk.out/openapi-backend-example.template.json",
10 | "lint": "tslint --format prose --project .",
11 | "test": "jest -i --forceExit"
12 | },
13 | "dependencies": {
14 | "@aws-cdk/aws-apigatewayv2-alpha": "^2.55.1-alpha.0",
15 | "@aws-cdk/aws-apigatewayv2-integrations-alpha": "^2.55.1-alpha.0",
16 | "aws-cdk-lib": "^2.55.1",
17 | "constructs": "^10.0.5",
18 | "openapi-backend": "^5.6.0",
19 | "source-map-support": "^0.5.13"
20 | },
21 | "devDependencies": {
22 | "@types/aws-lambda": "^8.10.109",
23 | "@types/jest": "^29.2.5",
24 | "@types/node": "^14",
25 | "@types/wait-on": "^5.3.1",
26 | "aws-cdk": "^2.55.1",
27 | "axios": "^1.2.2",
28 | "esbuild": "^0.16.10",
29 | "jest": "^29.3.1",
30 | "ts-jest": "^29.0.3",
31 | "ts-node": "^10.9.1",
32 | "typescript": "^4.9.4",
33 | "wait-on": "^7.0.1"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/examples/aws-cdk/src/lambdas/api-entrypoint.lambda.ts:
--------------------------------------------------------------------------------
1 | import 'source-map-support/register';
2 | import type * as Lambda from 'aws-lambda';
3 | import OpenAPIBackend from 'openapi-backend';
4 |
5 | const DEFAULT_HEADERS = {
6 | 'content-type': 'application/json',
7 | };
8 |
9 | const openAPIBackend = new OpenAPIBackend({
10 | // spec file is put to the `process.cwd()/openapi.yml` by AWS CDK configuration
11 | definition: './openapi.yml',
12 | // recommended for optimizing cold start
13 | quick: true,
14 | });
15 |
16 | // register some handlers
17 | openAPIBackend.register({
18 | notFound: async (_c, _event: Lambda.APIGatewayProxyEvent, _context: Lambda.Context) => ({
19 | statusCode: 404,
20 | body: JSON.stringify({ err: 'not found' }),
21 | headers: DEFAULT_HEADERS,
22 | }),
23 | validationFail: async (c, _event: Lambda.APIGatewayProxyEvent, _context: Lambda.Context) => ({
24 | statusCode: 400,
25 | body: JSON.stringify({ err: c.validation.errors }),
26 | headers: DEFAULT_HEADERS,
27 | }),
28 | getPets: async (c, _event: Lambda.APIGatewayProxyEvent, _context: Lambda.Context) => ({
29 | statusCode: 200,
30 | body: JSON.stringify({ operationId: c.operation.operationId }),
31 | headers: DEFAULT_HEADERS,
32 | }),
33 | getPetById: async (c, _event: Lambda.APIGatewayProxyEvent, _context: Lambda.Context) => ({
34 | statusCode: 200,
35 | body: JSON.stringify({
36 | operationId: c.operation.operationId,
37 | id: c.request.params.id,
38 | }),
39 | headers: DEFAULT_HEADERS,
40 | }),
41 | });
42 |
43 | // call and cache result of `factoryFunc` and create a new func that always returns the cached version
44 | const oncePromise = (factoryFunc: () => Promise): (() => Promise) => {
45 | const cache = factoryFunc();
46 |
47 | return () => cache;
48 | };
49 |
50 | const getAPI = oncePromise(() => openAPIBackend.init());
51 |
52 | export async function handler(
53 | event: Lambda.APIGatewayProxyEventV2,
54 | context: Lambda.Context
55 | ): Promise {
56 | const api = await getAPI();
57 |
58 | return api.handleRequest(
59 | {
60 | method: event.requestContext.http.method,
61 | path: event.requestContext.http.path,
62 | query: event.queryStringParameters as Record,
63 | body: event.body,
64 | headers: event.headers as Record,
65 | },
66 | event,
67 | context
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/examples/aws-cdk/src/main.ts:
--------------------------------------------------------------------------------
1 | import { App, CfnOutput, Duration, Stack, StackProps } from 'aws-cdk-lib';
2 | import { Construct } from 'constructs';
3 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
4 | import * as apigwv2 from '@aws-cdk/aws-apigatewayv2-alpha';
5 | import { CorsHttpMethod, HttpMethod } from '@aws-cdk/aws-apigatewayv2-alpha';
6 | import { resolve } from 'path';
7 | import { Architecture, Runtime } from 'aws-cdk-lib/aws-lambda';
8 | import { HttpLambdaIntegration } from '@aws-cdk/aws-apigatewayv2-integrations-alpha';
9 |
10 | const MAXIMUM_HTTP_API_INTEGRATION_TIMEOUT = Duration.seconds(29);
11 |
12 | export class MyStack extends Stack {
13 | constructor(scope: Construct, id: string, props: StackProps = {}) {
14 | super(scope, id, props);
15 |
16 | const entrypointLambda = new NodejsFunction(this, 'EntrypointLambda', {
17 | entry: resolve(__dirname, './lambdas/api-entrypoint.lambda.ts'),
18 | description: 'OpenAPI Backend Entrypoint Lambda',
19 | // NodeJS LTS with AWS SDK v3
20 | runtime: Runtime.NODEJS_16_X,
21 | // Cost-effective Processor Architecture
22 | // architecture: Architecture.ARM_64,
23 | architecture: Architecture.X86_64,
24 | // Maximum time a given endpoint Lambda invoke can take
25 | timeout: MAXIMUM_HTTP_API_INTEGRATION_TIMEOUT,
26 | // Lambda code bundling options
27 | bundling: {
28 | // Enable Source Map for better error logs
29 | sourceMap: true,
30 | // Hook into Commands for adding the OpenAPI Specification to Output
31 | commandHooks: {
32 | beforeBundling: () => [],
33 | beforeInstall: () => [],
34 | // Add the OpenAPI specification to the Lambda bundle
35 | afterBundling: (inputDir: string, outputDir: string) => [
36 | `cp "${inputDir}/openapi.yml" "${outputDir}/openapi.yml"`,
37 | ],
38 | },
39 | // Add bundled AWS SDK V3 and CDK dependencies to the externals
40 | externalModules: ['@aws-sdk/*', '@aws-cdk/*', 'aws-cdk', 'aws-cdk-lib', 'node-fetch'],
41 | },
42 | });
43 |
44 | const httpApi = new apigwv2.HttpApi(this, 'HttpApi', {
45 | description: 'OpenAPI Backend Http Api',
46 | corsPreflight: {
47 | allowHeaders: ['content-type'],
48 | allowMethods: [CorsHttpMethod.ANY],
49 | allowOrigins: ['*'],
50 | },
51 | });
52 |
53 | // let's create a separate proxy integration instead of using `$default` integration of `HttpApi`
54 | // this way; `OPTIONS` requests will be handled by `HttpApi` instead of invoking your Lambda
55 | // You can opt in to use `defaultIntegration` and change `notFound` to return `204` for `OPTIONS` requests
56 | httpApi.addRoutes({
57 | path: '/{proxy+}',
58 | // ALL methods expect OPTIONS / ANY should be handled by our Lambda
59 | methods: Object.values(HttpMethod).filter(
60 | (method) => method !== HttpMethod.OPTIONS && method !== HttpMethod.ANY
61 | ),
62 | integration: new HttpLambdaIntegration(
63 | 'OpenAPIBackendIntegration',
64 | entrypointLambda
65 | ),
66 | });
67 |
68 | /* tslint:disable-next-line no-unused-expression */
69 | new CfnOutput(this, 'OpenAPIBackendHttpApiEndpoint', {
70 | value: httpApi.apiEndpoint,
71 | description: 'OpenAPI Backend Example HttpApi Endpoint',
72 | });
73 | }
74 | }
75 |
76 | const app = new App();
77 |
78 | /* tslint:disable-next-line no-unused-expression */
79 | new MyStack(app, 'openapi-backend-example');
80 |
81 | app.synth();
82 |
--------------------------------------------------------------------------------
/examples/aws-cdk/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "rootDir": "src",
4 | "outDir": "lib",
5 | "alwaysStrict": true,
6 | "declaration": true,
7 | "esModuleInterop": true,
8 | "experimentalDecorators": true,
9 | "inlineSourceMap": true,
10 | "inlineSources": true,
11 | "lib": ["esnext"],
12 | "module": "CommonJS",
13 | "noEmitOnError": false,
14 | "noFallthroughCasesInSwitch": true,
15 | "noImplicitAny": true,
16 | "noImplicitReturns": true,
17 | "noImplicitThis": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "resolveJsonModule": true,
21 | "strict": true,
22 | "strictNullChecks": true,
23 | "strictPropertyInitialization": true,
24 | "stripInternal": true,
25 | "target": "es2021"
26 | },
27 | "include": [
28 | "src/**/*.ts"
29 | ],
30 | "exclude": [
31 | "cdk.out"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/examples/aws-sam/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | dist/*
3 | samconfig.toml
4 |
--------------------------------------------------------------------------------
/examples/aws-sam/README.md:
--------------------------------------------------------------------------------
1 | # OpenAPI Backend AWS SAM Example
2 | [](http://anttiviljami.mit-license.org)
3 |
4 | Example project using [openapi-backend](https://github.com/openapistack/openapi-backend) on [AWS SAM](https://aws.amazon.com/serverless/sam/)
5 |
6 | ## QuickStart
7 |
8 | Requirements:
9 | - [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-reference.html#serverless-sam-cli)
10 | - [Docker](https://docs.docker.com/get-docker/)
11 |
12 | ```
13 | npm install
14 | npm start # API running at http://localhost:3000
15 | ```
16 |
17 | Try the endpoints:
18 |
19 | ```bash
20 | curl -i http://localhost:3000/pets
21 | curl -i http://localhost:3000/pets/1
22 | ```
23 |
24 |
--------------------------------------------------------------------------------
/examples/aws-sam/buildspec.yml:
--------------------------------------------------------------------------------
1 | version: 0.2
2 | phases:
3 | install:
4 | commands:
5 | - npm install
6 | pre_build:
7 | commands:
8 | - npm test
9 | - npm prune --production
10 | build:
11 | commands:
12 | - npm build
13 | - aws cloudformation package --template template.yml --s3-bucket $S3_BUCKET --output-template template-export.yml
14 | artifacts:
15 | type: zip
16 | files:
17 | - template-export.yml
18 |
--------------------------------------------------------------------------------
/examples/aws-sam/env.json:
--------------------------------------------------------------------------------
1 | {
2 | "api": {}
3 | }
4 |
--------------------------------------------------------------------------------
/examples/aws-sam/events/event.json:
--------------------------------------------------------------------------------
1 | {
2 | "httpMethod": "GET"
3 | }
--------------------------------------------------------------------------------
/examples/aws-sam/index.test.ts:
--------------------------------------------------------------------------------
1 | import { spawn, ChildProcess } from 'child_process';
2 | import axios, { AxiosInstance } from 'axios';
3 | import waitOn from 'wait-on';
4 |
5 | jest.setTimeout(90000);
6 |
7 | describe('aws sam example', () => {
8 | let start: ChildProcess;
9 | let client: AxiosInstance;
10 |
11 | beforeAll(async () => {
12 | client = axios.create({ baseURL: 'http://localhost:3000', validateStatus: () => true });
13 | start = spawn('npm', ['start'], { cwd: __dirname, detached: true, stdio: 'inherit' });
14 | await waitOn({ resources: ['tcp:localhost:3000'] });
15 | });
16 |
17 | afterAll(() => process.kill(-start.pid));
18 |
19 | test('GET /pets returns 200 with matched operation', async () => {
20 | const res = await client.get('/pets');
21 | expect(res.status).toBe(200);
22 | expect(res.data).toEqual({ operationId: 'getPets' });
23 | });
24 |
25 | test('GET /pets/1 returns 200 with matched operation', async () => {
26 | const res = await client.get('/pets/1');
27 | expect(res.status).toBe(200);
28 | expect(res.data).toEqual({ operationId: 'getPetById' });
29 | });
30 |
31 | test('GET /pets/1a returns 400 with validation error', async () => {
32 | const res = await client.get('/pets/1a');
33 | expect(res.status).toBe(400);
34 | expect(res.data).toHaveProperty('err');
35 | });
36 |
37 | test('GET /unknown returns 404', async () => {
38 | const res = await client.get('/unknown');
39 | expect(res.status).toBe(404);
40 | expect(res.data).toHaveProperty('err');
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/examples/aws-sam/index.ts:
--------------------------------------------------------------------------------
1 | import 'source-map-support/register';
2 | import * as Lambda from 'aws-lambda';
3 | import OpenAPIBackend from 'openapi-backend';
4 | const headers = {
5 | 'content-type': 'application/json',
6 | 'access-control-allow-origin': '*', // lazy cors config
7 | };
8 |
9 | // create api from definition
10 | const api = new OpenAPIBackend({ definition: './openapi.yml', quick: true });
11 |
12 | // register some handlers
13 | api.register({
14 | notFound: async (c, event: Lambda.APIGatewayProxyEvent, context: Lambda.Context) => ({
15 | statusCode: 404,
16 | body: JSON.stringify({ err: 'not found' }),
17 | headers,
18 | }),
19 | validationFail: async (c, event: Lambda.APIGatewayProxyEvent, context: Lambda.Context) => ({
20 | statusCode: 400,
21 | body: JSON.stringify({ err: c.validation.errors }),
22 | headers,
23 | }),
24 | getPets: async (c, event: Lambda.APIGatewayProxyEvent, context: Lambda.Context) => ({
25 | statusCode: 200,
26 | body: JSON.stringify({ operationId: c.operation.operationId }),
27 | headers,
28 | }),
29 | getPetById: async (c, event: Lambda.APIGatewayProxyEvent, context: Lambda.Context) => ({
30 | statusCode: 200,
31 | body: JSON.stringify({ operationId: c.operation.operationId }),
32 | headers,
33 | }),
34 | });
35 |
36 | // init api
37 | api.init();
38 |
39 | export async function handler(event: Lambda.APIGatewayProxyEvent, context: Lambda.Context) {
40 | return api.handleRequest(
41 | {
42 | method: event.httpMethod,
43 | path: event.path,
44 | query: event.queryStringParameters,
45 | body: event.body,
46 | headers: event.headers,
47 | },
48 | event,
49 | context,
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/examples/aws-sam/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | testMatch: [ '**/?(*.)+(spec|test).ts?(x)' ]
5 | };
6 |
--------------------------------------------------------------------------------
/examples/aws-sam/openapi.yml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.1
2 | info:
3 | title: 'My API'
4 | version: 1.0.0
5 | paths:
6 | '/pets':
7 | get:
8 | operationId: getPets
9 | responses:
10 | '200':
11 | description: ok
12 | '/pets/{id}':
13 | get:
14 | operationId: getPetById
15 | responses:
16 | '200':
17 | description: ok
18 | parameters:
19 | - name: id
20 | in: path
21 | required: true
22 | schema:
23 | type: integer
24 |
25 |
--------------------------------------------------------------------------------
/examples/aws-sam/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openapi-backend-aws-sam",
3 | "version": "1.0.0",
4 | "description": "",
5 | "author": "Viljami Kuosmanen ",
6 | "license": "MIT",
7 | "keywords": [],
8 | "scripts": {
9 | "postinstall": "npm run build",
10 | "build": "tsc",
11 | "watch-build": "tsc -w",
12 | "start": "sam local start-api",
13 | "watch-start": "nodemon --delay 2 -w template.yml -w dist/ -x 'npm run start'",
14 | "dev": "concurrently -k -p '[{name}]' -n 'typescript,api' -c 'yellow.bold,cyan.bold' npm:watch-build npm:watch-start",
15 | "lint": "tslint --format prose --project .",
16 | "test": "jest -i"
17 | },
18 | "dependencies": {
19 | "openapi-backend": "^5.2.0"
20 | },
21 | "devDependencies": {
22 | "@types/aws-lambda": "^8.10.56",
23 | "@types/jest": "^29.2.5",
24 | "@types/node": "^14.0.13",
25 | "@types/wait-on": "^4.0.0",
26 | "axios": "^0.21.1",
27 | "jest": "^29.3.1",
28 | "ts-jest": "^29.0.3",
29 | "typescript": "^4.9.4",
30 | "wait-on": "^5.0.1"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/examples/aws-sam/template.yml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: 2010-09-09
2 | Description: openapi-backend-aws-sam
3 | Transform:
4 | - AWS::Serverless-2016-10-31
5 | Resources:
6 | api:
7 | Type: AWS::Serverless::Function
8 | Properties:
9 | Handler: dist/index.handler
10 | Runtime: nodejs16.x
11 | # Architectures:
12 | # - arm64
13 | MemorySize: 128
14 | Timeout: 100
15 | Events:
16 | Api:
17 | Type: Api
18 | Properties:
19 | Path: /{proxy+}
20 | Method: ANY
21 | Outputs:
22 | WebEndpoint:
23 | Description: "API Gateway endpoint URL for Prod stage"
24 | Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
25 |
--------------------------------------------------------------------------------
/examples/aws-sam/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2021",
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "lib": ["esnext"],
7 | "experimentalDecorators": true,
8 | "emitDecoratorMetadata": true,
9 | "esModuleInterop": true,
10 | "noImplicitAny": true,
11 | "noUnusedLocals": true,
12 | "baseUrl": ".",
13 | "rootDir": ".",
14 | "outDir": "dist",
15 | "sourceMap": true,
16 | "skipLibCheck": true
17 | },
18 | "include": [
19 | "**/*"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/examples/aws-sst/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules
3 |
4 | # sst
5 | .sst
6 | .build
7 |
8 | # opennext
9 | .open-next
10 |
11 | # misc
12 | .DS_Store
13 |
14 | # local env files
15 | .env*.local
16 |
--------------------------------------------------------------------------------
/examples/aws-sst/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 Viljami Kuosmanen
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/examples/aws-sst/README.md:
--------------------------------------------------------------------------------
1 | # openapi-backend + SST sample
2 |
3 | [](http://anttiviljami.mit-license.org)
4 |
5 | Example project using [openapi-backend](https://github.com/openapistack/openapi-backend) with [SST](https://sst.dev/)
6 |
7 | ## Quick Start
8 |
9 | ```
10 | pnpm i
11 | pnpm sst dev
12 | # you will get something like https://p4o1hlios0.execute-api.us-east-1.amazonaws.com
13 | ```
14 |
--------------------------------------------------------------------------------
/examples/aws-sst/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openapi-backend-sst-sample",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "scripts": {
7 | "dev": "sst dev",
8 | "build": "sst build",
9 | "deploy": "sst deploy",
10 | "remove": "sst remove",
11 | "console": "sst console",
12 | "typecheck": "tsc --noEmit"
13 | },
14 | "devDependencies": {
15 | "sst": "^2.26.11",
16 | "aws-cdk-lib": "2.95.1",
17 | "constructs": "10.2.69",
18 | "typescript": "^5.2.2",
19 | "@tsconfig/node18": "^18.2.2"
20 | },
21 | "workspaces": [
22 | "packages/*"
23 | ]
24 | }
--------------------------------------------------------------------------------
/examples/aws-sst/packages/core/openapi.yml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.1
2 | info:
3 | title: 'My API'
4 | version: 1.0.0
5 | paths:
6 | '/pets':
7 | get:
8 | operationId: getPets
9 | responses:
10 | '200':
11 | description: ok
12 | '/pets/{id}':
13 | get:
14 | operationId: getPetById
15 | responses:
16 | '200':
17 | description: ok
18 | parameters:
19 | - name: id
20 | in: path
21 | required: true
22 | schema:
23 | type: integer
24 |
25 |
--------------------------------------------------------------------------------
/examples/aws-sst/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@openapi-backend-sst-sample/core",
3 | "version": "0.0.0",
4 | "type": "module",
5 | "main": "src/index.ts",
6 | "scripts": {
7 | "test": "sst bind vitest",
8 | "typecheck": "tsc -noEmit",
9 | "build": "pnpm openapi read ./openapi.yml --json > src/openapi.json"
10 | },
11 | "devDependencies": {
12 | "vitest": "^0.34.6",
13 | "@types/node": "^20.8.2",
14 | "sst": "^2.26.11",
15 | "openapicmd": "^1.17.0"
16 | },
17 | "dependencies": {}
18 | }
--------------------------------------------------------------------------------
/examples/aws-sst/packages/core/src/definition.ts:
--------------------------------------------------------------------------------
1 | import definition from "./openapi.json";
2 |
3 | export { definition };
4 |
--------------------------------------------------------------------------------
/examples/aws-sst/packages/core/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./definition";
2 |
--------------------------------------------------------------------------------
/examples/aws-sst/packages/core/src/openapi.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.1",
3 | "info": {
4 | "title": "My API",
5 | "version": "1.0.0"
6 | },
7 | "paths": {
8 | "/pets": {
9 | "get": {
10 | "operationId": "getPets",
11 | "responses": {
12 | "200": {
13 | "description": "ok"
14 | }
15 | }
16 | }
17 | },
18 | "/pets/{id}": {
19 | "get": {
20 | "operationId": "getPetById",
21 | "responses": {
22 | "200": {
23 | "description": "ok"
24 | }
25 | }
26 | },
27 | "parameters": [
28 | {
29 | "name": "id",
30 | "in": "path",
31 | "required": true,
32 | "schema": {
33 | "type": "integer"
34 | }
35 | }
36 | ]
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/examples/aws-sst/packages/core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node18/tsconfig.json",
3 | "compilerOptions": {
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "resolveJsonModule": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/aws-sst/packages/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@openapi-backend-sst-sample/functions",
3 | "version": "0.0.0",
4 | "type": "module",
5 | "scripts": {
6 | "test": "sst bind vitest",
7 | "typecheck": "tsc -noEmit"
8 | },
9 | "dependencies": {
10 | "openapi-backend": "^5.10.0",
11 | "@openapi-backend-sst-sample/core": "*"
12 | },
13 | "devDependencies": {
14 | "@types/node": "^20.8.2",
15 | "@types/aws-lambda": "^8.10.123",
16 | "vitest": "^0.34.6",
17 | "sst": "^2.26.11"
18 | }
19 | }
--------------------------------------------------------------------------------
/examples/aws-sst/packages/functions/src/lambda.ts:
--------------------------------------------------------------------------------
1 | import { ApiHandler } from "sst/node/api";
2 |
3 | import { definition } from "@openapi-backend-sst-sample/core";
4 |
5 | import { OpenAPIBackend, type Request } from "openapi-backend";
6 | import { type APIGatewayProxyEventV2 } from "aws-lambda";
7 |
8 | const headers = {
9 | "content-type": "application/json",
10 | "access-control-allow-origin": "*",
11 | };
12 |
13 | const api = new OpenAPIBackend({ definition, quick: true });
14 |
15 | api.register({
16 | notFound: async (c, event: APIGatewayProxyEventV2) => ({
17 | statusCode: 404,
18 | body: JSON.stringify({ err: "not found" }),
19 | headers,
20 | }),
21 | validationFail: async (c, event: APIGatewayProxyEventV2) => ({
22 | statusCode: 400,
23 | body: JSON.stringify({ err: c.validation.errors }),
24 | headers,
25 | }),
26 | getPets: async (c, event: APIGatewayProxyEventV2) => ({
27 | statusCode: 200,
28 | body: JSON.stringify({ operationId: c.operation.operationId }),
29 | headers,
30 | }),
31 | getPetById: async (c, event: APIGatewayProxyEventV2) => ({
32 | statusCode: 200,
33 | body: JSON.stringify({ operationId: c.operation.operationId }),
34 | headers,
35 | }),
36 | });
37 |
38 | api.init();
39 |
40 | export const handler = ApiHandler(async (event, context) => {
41 | return await api.handleRequest(
42 | {
43 | method: event.requestContext.http.method,
44 | path: event.rawPath,
45 | query: event.rawQueryString,
46 | body: event.body,
47 | headers: event.headers as Request["headers"],
48 | },
49 | event,
50 | context
51 | );
52 | });
53 |
--------------------------------------------------------------------------------
/examples/aws-sst/packages/functions/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node18/tsconfig.json",
3 | "compilerOptions": {
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "baseUrl": ".",
7 | "paths": {
8 | "@openapi-backend-sst-sample/core/*": [
9 | "../core/src/*"
10 | ]
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/examples/aws-sst/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "packages/**/*"
3 |
--------------------------------------------------------------------------------
/examples/aws-sst/sst.config.ts:
--------------------------------------------------------------------------------
1 | import { SSTConfig } from "sst";
2 | import { OpenAPI } from "./stacks/OpenAPIStack";
3 |
4 | export default {
5 | config(_input) {
6 | return {
7 | name: "openapi-backend-sst-sample",
8 | region: "us-east-1",
9 | };
10 | },
11 | stacks(app) {
12 | app.stack(OpenAPI);
13 | },
14 | } satisfies SSTConfig;
15 |
--------------------------------------------------------------------------------
/examples/aws-sst/stacks/OpenAPIStack.ts:
--------------------------------------------------------------------------------
1 | import { StackContext, Api } from "sst/constructs";
2 |
3 | export function OpenAPI({ stack }: StackContext) {
4 | const api = new Api(stack, "api", {
5 | routes: {
6 | "ANY /{proxy+}": "packages/functions/src/lambda.handler",
7 | },
8 | });
9 |
10 | stack.addOutputs({
11 | ApiEndpoint: api.url,
12 | });
13 | }
14 |
--------------------------------------------------------------------------------
/examples/aws-sst/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node18/tsconfig.json",
3 | "exclude": ["packages"],
4 | "compilerOptions": {
5 | "module": "esnext",
6 | "moduleResolution": "node"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/azure-function/.gitignore:
--------------------------------------------------------------------------------
1 | !*.js
2 | node_modules/*
3 |
--------------------------------------------------------------------------------
/examples/azure-function/README.md:
--------------------------------------------------------------------------------
1 | # OpenAPI Backend Serverless Azure Example
2 | [](http://anttiviljami.mit-license.org)
3 |
4 | Example project using [openapi-backend](https://github.com/openapistack/openapi-backend) on Azure Functions
5 |
6 | ## QuickStart
7 |
8 | Requirements:
9 | - [Azure Functions Core Tools](https://github.com/Azure/azure-functions-core-tools)
10 |
11 | ```
12 | npm install
13 | npm start # API running at http://localhost:9000
14 | ```
15 |
16 | Try the endpoints:
17 |
18 | ```bash
19 | curl -i http://localhost:9000/pets
20 | curl -i http://localhost:9000/pets/1
21 | ```
22 |
23 |
--------------------------------------------------------------------------------
/examples/azure-function/handler/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "disabled": false,
3 | "bindings": [
4 | {
5 | "authLevel": "anonymous",
6 | "type": "httpTrigger",
7 | "direction": "in",
8 | "name": "req",
9 | "methods": [
10 | "get",
11 | "post",
12 | "put",
13 | "patch",
14 | "delete"
15 | ]
16 | },
17 | {
18 | "type": "http",
19 | "direction": "out",
20 | "name": "res"
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/examples/azure-function/handler/index.js:
--------------------------------------------------------------------------------
1 | const OpenAPIBackend = require('openapi-backend').default;
2 |
3 | // define api
4 | const api = new OpenAPIBackend({
5 | definition: {
6 | openapi: '3.0.1',
7 | info: {
8 | title: 'My API',
9 | version: '1.0.0',
10 | },
11 | paths: {
12 | '/pets': {
13 | get: {
14 | operationId: 'getPets',
15 | responses: {
16 | 200: { description: 'ok' },
17 | },
18 | },
19 | },
20 | '/pets/{id}': {
21 | get: {
22 | operationId: 'getPetById',
23 | responses: {
24 | 200: { description: 'ok' },
25 | },
26 | },
27 | parameters: [
28 | {
29 | name: 'id',
30 | in: 'path',
31 | required: true,
32 | schema: {
33 | type: 'integer',
34 | },
35 | },
36 | ],
37 | },
38 | },
39 | },
40 | handlers: {
41 | getPets: (c, context, req) => {
42 | context.res = {
43 | status: 200,
44 | body: JSON.stringify({ operationId: c.operation.operationId }),
45 | headers: {
46 | 'content-type': 'application/json',
47 | },
48 | };
49 | },
50 | getPetById: (c, context, req) => {
51 | context.res = {
52 | status: 200,
53 | body: JSON.stringify({ operationId: c.operation.operationId }),
54 | headers: {
55 | 'content-type': 'application/json',
56 | },
57 | };
58 | },
59 | notFound: (c, context, req) => {
60 | context.res = {
61 | status: 404,
62 | body: JSON.stringify({ err: 'not found' }),
63 | headers: {
64 | 'content-type': 'application/json',
65 | },
66 | };
67 | },
68 | validationFail: (c, context, req) => {
69 | context.res = {
70 | status: 400,
71 | body: JSON.stringify({ err: c.validation.errors }),
72 | headers: {
73 | 'content-type': 'application/json',
74 | },
75 | };
76 | },
77 | },
78 | quick: true,
79 | });
80 |
81 | api.init();
82 |
83 | module.exports = (context, req) =>
84 | api.handleRequest(
85 | {
86 | method: req.method,
87 | path: req.params.path,
88 | query: req.query,
89 | body: req.body,
90 | headers: req.headers,
91 | },
92 | context,
93 | req,
94 | );
95 |
--------------------------------------------------------------------------------
/examples/azure-function/handler/sample.dat:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/examples/azure-function/host.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0"
3 | }
--------------------------------------------------------------------------------
/examples/azure-function/index.test.js:
--------------------------------------------------------------------------------
1 | const { spawn } = require('child_process');
2 | const waitOn = require('wait-on');
3 | const axios = require('axios');
4 |
5 | jest.setTimeout(15000);
6 |
7 | describe('azure functions example', () => {
8 | let start;
9 | let client;
10 |
11 | beforeAll(async () => {
12 | client = axios.create({ baseURL: 'http://localhost:9000', validateStatus: () => true });
13 | start = spawn('npm', ['start'], { cwd: __dirname, detached: true, stdio: 'inherit' });
14 | await waitOn({ resources: ['tcp:localhost:9000'] });
15 | });
16 |
17 | afterAll(() => process.kill(-start.pid));
18 |
19 | test('GET /pets returns 200 with matched operation', async () => {
20 | const res = await client.get('/pets');
21 | expect(res.status).toBe(200);
22 | expect(res.data).toEqual({ operationId: 'getPets' });
23 | });
24 |
25 | test('GET /pets/1 returns 200 with matched operation', async () => {
26 | const res = await client.get('/pets/1');
27 | expect(res.status).toBe(200);
28 | expect(res.data).toEqual({ operationId: 'getPetById' });
29 | });
30 |
31 | test('GET /pets/1a returns 400 with validation error', async () => {
32 | const res = await client.get('/pets/1a');
33 | expect(res.status).toBe(400);
34 | expect(res.data).toHaveProperty('err');
35 | });
36 |
37 | test('GET /unknown returns 404', async () => {
38 | const res = await client.get('/unknown');
39 | expect(res.status).toBe(404);
40 | expect(res.data).toHaveProperty('err');
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/examples/azure-function/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'node'
3 | };
4 |
--------------------------------------------------------------------------------
/examples/azure-function/local.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "IsEncrypted": false,
3 | "Host": {
4 | "LocalHttpPort": 9000,
5 | "CORS": "*"
6 | },
7 | "Values": {
8 | "FUNCTIONS_WORKER_RUNTIME": "node",
9 | "AzureWebJobsStorage": "{AzureWebJobsStorage}"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/examples/azure-function/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openapi-backend-azure-function",
3 | "version": "1.0.0",
4 | "description": "",
5 | "author": "Viljami Kuosmanen ",
6 | "license": "MIT",
7 | "keywords": [],
8 | "scripts": {
9 | "start": "func start",
10 | "test": "jest"
11 | },
12 | "dependencies": {
13 | "openapi-backend": "^5.2.0"
14 | },
15 | "devDependencies": {
16 | "axios": "^0.21.1",
17 | "azure-functions-core-tools": "^2.4.317",
18 | "jest": "^29.3.1",
19 | "wait-on": "^3.2.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/azure-function/proxies.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/proxies",
3 | "proxies": {
4 | "api": {
5 | "backendUri": "https://localhost/api/handler",
6 | "matchCondition": {
7 | "methods": [ "GET" ],
8 | "route": "{*path}"
9 | }
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/examples/bun/.gitignore:
--------------------------------------------------------------------------------
1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
2 |
3 | # Logs
4 |
5 | logs
6 | _.log
7 | npm-debug.log_
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 | .pnpm-debug.log*
12 |
13 | # Caches
14 |
15 | .cache
16 |
17 | # Diagnostic reports (https://nodejs.org/api/report.html)
18 |
19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
20 |
21 | # Runtime data
22 |
23 | pids
24 | _.pid
25 | _.seed
26 | *.pid.lock
27 |
28 | # Directory for instrumented libs generated by jscoverage/JSCover
29 |
30 | lib-cov
31 |
32 | # Coverage directory used by tools like istanbul
33 |
34 | coverage
35 | *.lcov
36 |
37 | # nyc test coverage
38 |
39 | .nyc_output
40 |
41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
42 |
43 | .grunt
44 |
45 | # Bower dependency directory (https://bower.io/)
46 |
47 | bower_components
48 |
49 | # node-waf configuration
50 |
51 | .lock-wscript
52 |
53 | # Compiled binary addons (https://nodejs.org/api/addons.html)
54 |
55 | build/Release
56 |
57 | # Dependency directories
58 |
59 | node_modules/
60 | jspm_packages/
61 | bun.lockb
62 |
63 | # Snowpack dependency directory (https://snowpack.dev/)
64 |
65 | web_modules/
66 |
67 | # TypeScript cache
68 |
69 | *.tsbuildinfo
70 |
71 | # Optional npm cache directory
72 |
73 | .npm
74 |
75 | # Optional eslint cache
76 |
77 | .eslintcache
78 |
79 | # Optional stylelint cache
80 |
81 | .stylelintcache
82 |
83 | # Microbundle cache
84 |
85 | .rpt2_cache/
86 | .rts2_cache_cjs/
87 | .rts2_cache_es/
88 | .rts2_cache_umd/
89 |
90 | # Optional REPL history
91 |
92 | .node_repl_history
93 |
94 | # Output of 'npm pack'
95 |
96 | *.tgz
97 |
98 | # Yarn Integrity file
99 |
100 | .yarn-integrity
101 |
102 | # dotenv environment variable files
103 |
104 | .env
105 | .env.development.local
106 | .env.test.local
107 | .env.production.local
108 | .env.local
109 |
110 | # parcel-bundler cache (https://parceljs.org/)
111 |
112 | .parcel-cache
113 |
114 | # Next.js build output
115 |
116 | .next
117 | out
118 |
119 | # Nuxt.js build / generate output
120 |
121 | .nuxt
122 | dist
123 |
124 | # Gatsby files
125 |
126 | # Comment in the public line in if your project uses Gatsby and not Next.js
127 |
128 | # https://nextjs.org/blog/next-9-1#public-directory-support
129 |
130 | # public
131 |
132 | # vuepress build output
133 |
134 | .vuepress/dist
135 |
136 | # vuepress v2.x temp and cache directory
137 |
138 | .temp
139 |
140 | # Docusaurus cache and generated files
141 |
142 | .docusaurus
143 |
144 | # Serverless directories
145 |
146 | .serverless/
147 |
148 | # FuseBox cache
149 |
150 | .fusebox/
151 |
152 | # DynamoDB Local files
153 |
154 | .dynamodb/
155 |
156 | # TernJS port file
157 |
158 | .tern-port
159 |
160 | # Stores VSCode versions used for testing VSCode extensions
161 |
162 | .vscode-test
163 |
164 | # yarn v2
165 |
166 | .yarn/cache
167 | .yarn/unplugged
168 | .yarn/build-state.yml
169 | .yarn/install-state.gz
170 | .pnp.*
171 |
172 | # IntelliJ based IDEs
173 | .idea
174 |
175 | # Finder (MacOS) folder config
176 | .DS_Store
177 |
--------------------------------------------------------------------------------
/examples/bun/README.md:
--------------------------------------------------------------------------------
1 | # openapi-backend-bun
2 |
3 | To install dependencies:
4 |
5 | ```bash
6 | bun install
7 | ```
8 |
9 | To run:
10 |
11 | ```bash
12 | bun run index.ts
13 | ```
14 |
15 | This project was created using `bun init` in bun v1.0.28. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
16 |
--------------------------------------------------------------------------------
/examples/bun/index.test.ts:
--------------------------------------------------------------------------------
1 | import {$} from 'bun';
2 | import axios from 'axios';
3 | import type { AxiosInstance } from 'axios';
4 | import type { Subprocess } from 'bun';
5 | import {test, expect, describe, beforeAll, afterAll} from "bun:test";
6 |
7 | async function waitForPort(port: number, retryInterval = 1000, timeout = 30000) {
8 | const start = Date.now();
9 |
10 | while (true) {
11 | try {
12 | // Attempt to fetch from the server
13 | await fetch(`http://localhost:${port}`);
14 | // If successful, the port is open; break out of the loop
15 | console.log(`Port ${port} is now open.`);
16 | break;
17 | } catch (e) {
18 | // If there's an error (likely connection refused), the port isn't open yet
19 | if (Date.now() - start > timeout) {
20 | // If we've exceeded our timeout, throw an error
21 | throw new Error(`Timeout waiting for port ${port}`);
22 | }
23 | // Wait for `retryInterval` milliseconds before trying again
24 | await new Promise(resolve => setTimeout(resolve, retryInterval));
25 | }
26 | }
27 | }
28 |
29 | describe('bun-ts example', () => {
30 | let proc: Subprocess<"ignore", "pipe", "inherit">;
31 | let client: AxiosInstance;
32 |
33 | beforeAll(async () => {
34 | client = axios.create({ baseURL: 'http://localhost:9000', validateStatus: () => true });
35 | proc = Bun.spawn(["bun", "run", "index.ts"]);
36 | proc.unref();
37 |
38 | await waitForPort(9000);
39 | });
40 |
41 | afterAll(async () => {
42 | proc.kill();
43 | });
44 |
45 | test('GET /pets returns 200 with matched operation', async () => {
46 | const res = await client.get('/pets');
47 | expect(res.status).toBe(200);
48 | expect(res.data).toEqual({ operationId: 'getPets' });
49 | });
50 |
51 | test('GET /pets/1 returns 200 with matched operation', async () => {
52 | const res = await client.get('/pets/1');
53 | expect(res.status).toBe(200);
54 | expect(res.data).toEqual({ operationId: 'getPetById' });
55 | });
56 |
57 | test('GET /pets/1a returns 400 with validation error', async () => {
58 | const res = await client.get('/pets/1a');
59 | expect(res.status).toBe(400);
60 | expect(res.data).toHaveProperty('err');
61 | });
62 |
63 | test('GET /unknown returns 404', async () => {
64 | const res = await client.get('/unknown');
65 | expect(res.status).toBe(404);
66 | expect(res.data).toHaveProperty('err');
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/examples/bun/index.ts:
--------------------------------------------------------------------------------
1 | import OpenAPIBackend from "openapi-backend";
2 | import {type Serve} from "bun";
3 |
4 | const api = new OpenAPIBackend({
5 | definition: {
6 | openapi: '3.0.1',
7 | info: {
8 | title: 'My API',
9 | version: '1.0.0',
10 | },
11 | paths: {
12 | '/pets': {
13 | get: {
14 | operationId: 'getPets',
15 | responses: {
16 | 200: { description: 'ok' },
17 | },
18 | },
19 | },
20 | '/pets/{id}': {
21 | get: {
22 | operationId: 'getPetById',
23 | responses: {
24 | 200: { description: 'ok' },
25 | },
26 | },
27 | parameters: [
28 | {
29 | name: 'id',
30 | in: 'path',
31 | required: true,
32 | schema: {
33 | type: 'integer',
34 | },
35 | },
36 | ],
37 | },
38 | },
39 | },
40 | handlers: {
41 | getPets: async (c, req, res) => Response.json({ operationId: c.operation.operationId }),
42 | getPetById: async (c, req, res) => Response.json({ operationId: c.operation.operationId }),
43 | validationFail: async (c, req, res) => Response.json({ err: c.validation.errors }, {status: 400}),
44 | notFound: async (c, req, res) => Response.json({ err: 'not found' }, {status: 404}),
45 | },
46 | });
47 |
48 | api.init();
49 |
50 | export default {
51 | port: 9000,
52 | fetch(req) {
53 | const {pathname, search} = new URL(req.url);
54 |
55 | return api.handleRequest({
56 | path: pathname,
57 | query: search,
58 | method: req.method,
59 | headers: req.headers.toJSON(),
60 | body: req.body
61 | }, req, new Response(null, {status: 200}));
62 | },
63 | } satisfies Serve;
64 |
--------------------------------------------------------------------------------
/examples/bun/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openapi-backend-bun",
3 | "module": "index.ts",
4 | "type": "module",
5 | "scripts": {
6 | "dev": "bun run --watch index.ts"
7 | },
8 | "devDependencies": {
9 | "@types/bun": "latest",
10 | "@types/wait-on": "^5.3.4",
11 | "axios": "^1.6.7",
12 | "wait-on": "^7.2.0"
13 | },
14 | "peerDependencies": {
15 | "typescript": "^5.0.0"
16 | },
17 | "dependencies": {
18 | "openapi-backend": "^5.10.6"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/examples/bun/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Enable latest features
4 | "lib": ["ESNext", "DOM"],
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "moduleDetection": "force",
8 | "jsx": "react-jsx",
9 | "allowJs": true,
10 |
11 | // Bundler mode
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "verbatimModuleSyntax": true,
15 | "noEmit": true,
16 |
17 | // Best practices
18 | "strict": true,
19 | "skipLibCheck": true,
20 | "noFallthroughCasesInSwitch": true,
21 |
22 | // Some stricter flags (disabled by default)
23 | "noUnusedLocals": false,
24 | "noUnusedParameters": false,
25 | "noPropertyAccessFromIndexSignature": false
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/examples/express-apikey-auth/.gitignore:
--------------------------------------------------------------------------------
1 | !*.js
2 | node_modules/*
3 |
--------------------------------------------------------------------------------
/examples/express-apikey-auth/README.md:
--------------------------------------------------------------------------------
1 | # OpenAPI Backend API Key Auth Example
2 | [](http://anttiviljami.mit-license.org)
3 |
4 | Example project using [openapi-backend](https://github.com/openapistack/openapi-backend) on [Express](https://expressjs.com/)
5 |
6 | ## QuickStart
7 |
8 | ```
9 | npm install
10 | npm start # API running at http://localhost:9000
11 | ```
12 |
13 | Try the endpoints:
14 |
15 | ```bash
16 | curl -i http://localhost:9000/pets -H x-api-key:secret
17 | curl -i http://localhost:9000/pets/1 -H x-api-key:secret
18 | ```
19 |
20 |
--------------------------------------------------------------------------------
/examples/express-apikey-auth/index.js:
--------------------------------------------------------------------------------
1 | const OpenAPIBackend = require('openapi-backend').default;
2 | const express = require('express');
3 |
4 | const app = express();
5 | app.use(express.json());
6 |
7 | // define api
8 | const api = new OpenAPIBackend({ definition: './openapi.yml' });
9 |
10 | // register handlers
11 | api.register({
12 | getPets: async (c, req, res) => res.status(200).json({ operationId: c.operation.operationId }),
13 | getPetById: async (c, req, res) => res.status(200).json({ operationId: c.operation.operationId }),
14 | validationFail: async (c, req, res) => res.status(400).json({ err: c.validation.errors }),
15 | notFound: async (c, req, res) => res.status(404).json({ err: 'not found' }),
16 | unauthorizedHandler: async (c, req, res) => res.status(401).json({ err: 'unauthorized' }),
17 | });
18 |
19 | // register security handler
20 | api.registerSecurityHandler('apiKey', (c, req, res) => {
21 | const authHeader = c.request.headers['x-api-key'];
22 | return authHeader === 'secret';
23 | });
24 |
25 | api.init();
26 |
27 | // use as express middleware
28 | app.use((req, res) => api.handleRequest(req, req, res));
29 |
30 | // start server
31 | app.listen(9000, () => console.info('api listening at http://localhost:9000'));
32 |
--------------------------------------------------------------------------------
/examples/express-apikey-auth/index.test.js:
--------------------------------------------------------------------------------
1 | const { spawn } = require('child_process');
2 | const waitOn = require('wait-on');
3 | const axios = require('axios');
4 |
5 | jest.setTimeout(30000);
6 |
7 | describe('express api key auth example', () => {
8 | let start;
9 | let client;
10 |
11 | beforeAll(async () => {
12 | client = axios.create({ baseURL: 'http://localhost:9000', validateStatus: () => true });
13 | start = spawn('npm', ['start'], { cwd: __dirname, detached: true, stdio: 'inherit' });
14 | await waitOn({ resources: ['tcp:localhost:9000'] });
15 | });
16 |
17 | afterAll(() => process.kill(-start.pid));
18 |
19 | describe('without api key', () => {
20 | beforeAll(() => {
21 | client.defaults.headers.common['x-api-key'] = null;
22 | });
23 |
24 | test('GET /pets returns 401 error', async () => {
25 | const res = await client.get('/pets');
26 | expect(res.status).toBe(401);
27 | expect(res.data).toHaveProperty('err');
28 | });
29 |
30 | test('GET /pets/1 returns 401 error', async () => {
31 | const res = await client.get('/pets/1');
32 | expect(res.status).toBe(401);
33 | expect(res.data).toHaveProperty('err');
34 | });
35 |
36 | test('GET /pets/1a returns 401 error', async () => {
37 | const res = await client.get('/pets/1a');
38 | expect(res.status).toBe(401);
39 | expect(res.data).toHaveProperty('err');
40 | });
41 | });
42 |
43 | describe('with correct api key', () => {
44 | beforeAll(() => {
45 | client.defaults.headers.common['x-api-key'] = 'secret';
46 | });
47 |
48 | test('GET /pets returns 200 with matched operation', async () => {
49 | const res = await client.get('/pets');
50 | expect(res.status).toBe(200);
51 | expect(res.data).toEqual({ operationId: 'getPets' });
52 | });
53 |
54 | test('GET /pets/1 returns 200 with matched operation', async () => {
55 | const res = await client.get('/pets/1');
56 | expect(res.status).toBe(200);
57 | expect(res.data).toEqual({ operationId: 'getPetById' });
58 | });
59 |
60 | test('GET /pets/1a returns 400 with validation error', async () => {
61 | const res = await client.get('/pets/1a');
62 | expect(res.status).toBe(400);
63 | expect(res.data).toHaveProperty('err');
64 | });
65 | });
66 |
67 | describe('with incorrect api key', () => {
68 | beforeAll(() => {
69 | client.defaults.headers.common['x-api-key'] = '';
70 | });
71 |
72 | test('GET /pets returns 401 error', async () => {
73 | const res = await client.get('/pets');
74 | expect(res.status).toBe(401);
75 | expect(res.data).toHaveProperty('err');
76 | });
77 |
78 | test('GET /pets/1 returns 401 error', async () => {
79 | const res = await client.get('/pets/1');
80 | expect(res.status).toBe(401);
81 | expect(res.data).toHaveProperty('err');
82 | });
83 |
84 | test('GET /pets/1a returns 401 error', async () => {
85 | const res = await client.get('/pets/1a');
86 | expect(res.status).toBe(401);
87 | expect(res.data).toHaveProperty('err');
88 | });
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/examples/express-apikey-auth/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'node'
3 | };
4 |
--------------------------------------------------------------------------------
/examples/express-apikey-auth/openapi.yml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.1
2 | info:
3 | title: 'My API'
4 | version: 1.0.0
5 | paths:
6 | '/pets':
7 | get:
8 | operationId: getPets
9 | responses:
10 | '200':
11 | description: ok
12 | '/pets/{id}':
13 | get:
14 | operationId: getPetById
15 | responses:
16 | '200':
17 | description: ok
18 | parameters:
19 | - name: id
20 | in: path
21 | required: true
22 | schema:
23 | type: integer
24 | components:
25 | securitySchemes:
26 | apiKey:
27 | type: apiKey
28 | in: header
29 | name: x-api-key
30 | security:
31 | - apiKey: []
32 |
--------------------------------------------------------------------------------
/examples/express-apikey-auth/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openapi-backend-express-apikey-auth",
3 | "version": "1.0.0",
4 | "description": "",
5 | "author": "Viljami Kuosmanen ",
6 | "license": "MIT",
7 | "keywords": [],
8 | "scripts": {
9 | "start": "node index.js",
10 | "test": "jest"
11 | },
12 | "dependencies": {
13 | "express": "^4.16.4",
14 | "openapi-backend": "^5.2.0"
15 | },
16 | "devDependencies": {
17 | "axios": "^0.21.1",
18 | "jest": "^29.3.1",
19 | "wait-on": "^3.2.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/express-jwt-auth/.gitignore:
--------------------------------------------------------------------------------
1 | !*.js
2 | node_modules/*
3 |
--------------------------------------------------------------------------------
/examples/express-jwt-auth/README.md:
--------------------------------------------------------------------------------
1 | # OpenAPI Backend JWT Auth Example
2 | [](http://anttiviljami.mit-license.org)
3 |
4 | Example project using [openapi-backend](https://github.com/openapistack/openapi-backend) on [Express](https://expressjs.com/)
5 |
6 | ## QuickStart
7 |
8 | ```
9 | npm install
10 | npm start # API running at http://localhost:9000
11 | ```
12 |
13 | Try the endpoints:
14 |
15 | ```bash
16 | curl -i http://localhost:9000/login
17 | curl -i http://localhost:9000/me -H "Authorization: "
18 | ```
19 |
20 |
--------------------------------------------------------------------------------
/examples/express-jwt-auth/index.js:
--------------------------------------------------------------------------------
1 | const OpenAPIBackend = require('openapi-backend').default;
2 | const express = require('express');
3 | const jwt = require('jsonwebtoken');
4 |
5 | const app = express();
6 | app.use(express.json());
7 |
8 | // define api
9 | const api = new OpenAPIBackend({ definition: './openapi.yml' });
10 |
11 | // register default handlers
12 | api.register({
13 | notFound: async (c, req, res) => res.status(404).json({ err: 'not found' }),
14 | unauthorizedHandler: async (c, req, res) => res.status(401).json({ err: 'unauthorized' }),
15 | });
16 |
17 | // register security handler for jwt auth
18 | api.registerSecurityHandler('jwtAuth', (c, req, res) => {
19 | const authHeader = c.request.headers['authorization'];
20 | if (!authHeader) {
21 | throw new Error('Missing authorization header');
22 | }
23 | const token = authHeader.replace('Bearer ', '');
24 | return jwt.verify(token, 'secret');
25 | });
26 |
27 | // register operation handlers
28 | api.register({
29 | // GET /me
30 | me: async (c, req, res) => {
31 | const tokenData = c.security.jwtAuth;
32 | return res.status(200).json(tokenData);
33 | },
34 | // GET /login
35 | login: async (c, req, res) => {
36 | const token = jwt.sign({ name: 'John Doe', email: 'john@example.com' }, 'secret');
37 | return res.status(200).json({ token });
38 | },
39 | });
40 |
41 | api.init();
42 |
43 | // use as express middleware
44 | app.use((req, res) => api.handleRequest(req, req, res));
45 |
46 | // start server
47 | app.listen(9000, () => console.info('api listening at http://localhost:9000'));
48 |
--------------------------------------------------------------------------------
/examples/express-jwt-auth/index.test.js:
--------------------------------------------------------------------------------
1 | const { spawn } = require('child_process');
2 | const waitOn = require('wait-on');
3 | const axios = require('axios');
4 | const jwt = require('jsonwebtoken');
5 |
6 | jest.setTimeout(30000);
7 |
8 | describe('express jwt example', () => {
9 | let start;
10 | let client;
11 |
12 | beforeAll(async () => {
13 | client = axios.create({ baseURL: 'http://localhost:9000', validateStatus: () => true });
14 | start = spawn('npm', ['start'], { cwd: __dirname, detached: true, stdio: 'inherit' });
15 | await waitOn({ resources: ['tcp:localhost:9000'] });
16 | });
17 |
18 | afterAll(() => process.kill(-start.pid));
19 |
20 | describe('with valid jwt token', () => {
21 | const payload = { user: 'John Doe' };
22 | const token = jwt.sign(payload, 'secret');
23 | beforeAll(() => {
24 | client.defaults.headers.common['Authorization'] = token;
25 | });
26 |
27 | test('GET /me returns 200 with token data', async () => {
28 | const res = await client.get('/me');
29 | expect(res.status).toBe(200);
30 | expect(res.data.user).toEqual(payload.user);
31 | });
32 | });
33 |
34 | describe('without authorization header', () => {
35 | beforeAll(() => {
36 | client.defaults.headers.common['Authorization'] = null;
37 | });
38 |
39 | test('GET /me returns 401', async () => {
40 | const res = await client.get('/me');
41 | expect(res.status).toBe(401);
42 | expect(res.data).toHaveProperty('err');
43 | });
44 |
45 | test('GET /login returns 200 with new jwt token', async () => {
46 | const res = await client.get('/login');
47 | expect(res.status).toBe(200);
48 | expect(res.data).toHaveProperty('token');
49 | });
50 | });
51 |
52 | describe('with invalid jwt token', () => {
53 | beforeAll(() => {
54 | client.defaults.headers.common['Authorization'] = '';
55 | });
56 |
57 | test('GET /me returns 401', async () => {
58 | const res = await client.get('/me');
59 | expect(res.status).toBe(401);
60 | expect(res.data).toHaveProperty('err');
61 | });
62 |
63 | test('GET /login returns 200 with new jwt token', async () => {
64 | const res = await client.get('/login');
65 | expect(res.status).toBe(200);
66 | expect(res.data).toHaveProperty('token');
67 | });
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/examples/express-jwt-auth/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'node'
3 | };
4 |
--------------------------------------------------------------------------------
/examples/express-jwt-auth/openapi.yml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.1
2 | info:
3 | title: 'My API'
4 | version: 1.0.0
5 | paths:
6 | '/me':
7 | get:
8 | operationId: me
9 | responses:
10 | '200':
11 | description: Token data
12 | content:
13 | application/json:
14 | schema:
15 | $ref: '#/components/schemas/TokenData'
16 | '401':
17 | description: Unauthorized
18 | content:
19 | application/json:
20 | schema:
21 | $ref: '#/components/schemas/TokenData'
22 | '/login':
23 | get:
24 | operationId: login
25 | security: [] # no auth needed
26 | responses:
27 | '200':
28 | description: Logged in succesfully
29 | content:
30 | application/json:
31 | schema:
32 | $ref: '#/components/schemas/TokenData'
33 | components:
34 | schemas:
35 | TokenData:
36 | type: object
37 | properties:
38 | name:
39 | type: string
40 | example: John Doe
41 | email:
42 | type: string
43 | format: email
44 | example: john@example.com
45 | iat:
46 | type: integer
47 | example: 1516239022
48 | securitySchemes:
49 | jwtAuth:
50 | type: http
51 | scheme: bearer
52 | bearerFormat: JWT
53 | security:
54 | - jwtAuth: []
55 |
--------------------------------------------------------------------------------
/examples/express-jwt-auth/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openapi-backend-express-jwt-auth",
3 | "version": "1.0.0",
4 | "description": "",
5 | "author": "Viljami Kuosmanen ",
6 | "license": "MIT",
7 | "keywords": [],
8 | "scripts": {
9 | "start": "node index.js",
10 | "test": "jest"
11 | },
12 | "dependencies": {
13 | "express": "^4.16.4",
14 | "jsonwebtoken": "^8.5.1",
15 | "openapi-backend": "^5.2.0"
16 | },
17 | "devDependencies": {
18 | "axios": "^0.21.1",
19 | "jest": "^29.3.1",
20 | "wait-on": "^3.2.0"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/express-ts-mock/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | dist/*
3 |
4 |
--------------------------------------------------------------------------------
/examples/express-ts-mock/README.md:
--------------------------------------------------------------------------------
1 | # OpenAPI Backend Express Mock API Example
2 | [](http://anttiviljami.mit-license.org)
3 |
4 | Example mock API project using [openapi-backend](https://github.com/openapistack/openapi-backend) on [Express](https://expressjs.com/)
5 |
6 | ## QuickStart
7 |
8 | ```
9 | npm install
10 | npm run dev # API running at http://localhost:9000
11 | ```
12 |
13 | Try the endpoints:
14 |
15 | ```bash
16 | curl -i http://localhost:9000/pets
17 | curl -i http://localhost:9000/pets/1
18 | curl -i -X POST -d {} http://localhost:9000/pets
19 | ```
20 |
21 |
--------------------------------------------------------------------------------
/examples/express-ts-mock/index.test.ts:
--------------------------------------------------------------------------------
1 | import { spawn, ChildProcess } from 'child_process';
2 | import axios, { AxiosInstance } from 'axios';
3 | import waitOn from 'wait-on';
4 |
5 | jest.setTimeout(15000);
6 |
7 | describe('express-ts example', () => {
8 | let start: ChildProcess;
9 | let client: AxiosInstance;
10 |
11 | beforeAll(async () => {
12 | client = axios.create({ baseURL: 'http://localhost:9000', validateStatus: () => true });
13 | start = spawn('npm', ['start'], { cwd: __dirname, detached: true, stdio: 'inherit' });
14 | await waitOn({ resources: ['tcp:localhost:9000'] });
15 | });
16 |
17 | afterAll(() => process.kill(-start.pid));
18 |
19 | test('GET /pets returns 200 with mocked result', async () => {
20 | const res = await client.get('/pets');
21 | expect(res.status).toBe(200);
22 | expect(res.data).toEqual([{ id: 1, name: 'Odie' }]);
23 | });
24 |
25 | test('GET /pets/1 returns 200 with mocked result', async () => {
26 | const res = await client.get('/pets/1');
27 | expect(res.status).toBe(200);
28 | expect(res.data).toEqual({ id: 1, name: 'Garfield' });
29 | });
30 |
31 | test('POST /pets returns 201 with mocked result', async () => {
32 | const res = await client.post('/pets', {});
33 | expect(res.status).toBe(201);
34 | expect(res.data).toEqual({ id: 1, name: 'Garfield' });
35 | });
36 |
37 | test('GET /pets/1a returns 400 with validation error', async () => {
38 | const res = await client.get('/pets/1a');
39 | expect(res.status).toBe(400);
40 | expect(res.data).toHaveProperty('err');
41 | });
42 |
43 | test('GET /unknown returns 404', async () => {
44 | const res = await client.get('/unknown');
45 | expect(res.status).toBe(404);
46 | expect(res.data).toHaveProperty('err');
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/examples/express-ts-mock/index.ts:
--------------------------------------------------------------------------------
1 | import 'source-map-support/register';
2 | import path from 'path';
3 | import OpenAPIBackend, { Request } from 'openapi-backend';
4 | import express from 'express';
5 | import morgan from 'morgan';
6 |
7 | import { Request as ExpressReq, Response as ExpressRes } from 'express';
8 |
9 | const app = express();
10 | app.use(express.json());
11 |
12 | // define api
13 | const api = new OpenAPIBackend({
14 | definition: path.join(__dirname, '..', 'openapi.yml'),
15 | handlers: {
16 | validationFail: async (c, req: ExpressReq, res: ExpressRes) => res.status(400).json({ err: c.validation.errors }),
17 | notFound: async (c, req: ExpressReq, res: ExpressRes) => res.status(404).json({ err: 'not found' }),
18 | notImplemented: async (c, req: ExpressReq, res: ExpressRes) => {
19 | const { status, mock } = c.api.mockResponseForOperation(c.operation.operationId);
20 | return res.status(status).json(mock);
21 | },
22 | },
23 | });
24 | api.init();
25 |
26 | // logging
27 | app.use(morgan('combined'));
28 |
29 | // use as express middleware
30 | app.use((req, res) => api.handleRequest(req as Request, req, res));
31 |
32 | // start server
33 | app.listen(9000, () => console.info('api listening at http://localhost:9000'));
34 |
--------------------------------------------------------------------------------
/examples/express-ts-mock/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | testMatch: [ '**/?(*.)+(spec|test).ts?(x)' ]
5 | };
6 |
--------------------------------------------------------------------------------
/examples/express-ts-mock/openapi.yml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.1
2 | info:
3 | title: My API
4 | version: 1.0.0
5 | paths:
6 | /pets:
7 | get:
8 | operationId: getPets
9 | responses:
10 | '200':
11 | $ref: '#/components/responses/ListPetsRes'
12 | post:
13 | operationId: createPet
14 | requestBody:
15 | description: Pet object to create
16 | content:
17 | application/json: {}
18 | responses:
19 | '201':
20 | $ref: '#/components/responses/PetRes'
21 | '/pets/{id}':
22 | get:
23 | operationId: getPetById
24 | responses:
25 | '200':
26 | $ref: '#/components/responses/PetRes'
27 | parameters:
28 | - name: id
29 | in: path
30 | required: true
31 | schema:
32 | type: integer
33 | components:
34 | responses:
35 | ListPetsRes:
36 | description: ok
37 | content:
38 | application/json:
39 | schema:
40 | type: array
41 | items:
42 | type: object
43 | properties:
44 | id:
45 | type: integer
46 | minimum: 1
47 | name:
48 | type: string
49 | example: Odie
50 | PetRes:
51 | description: ok
52 | content:
53 | application/json:
54 | examples:
55 | garfield:
56 | value:
57 | id: 1
58 | name: Garfield
59 |
--------------------------------------------------------------------------------
/examples/express-ts-mock/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openapi-backend-express-ts-mock",
3 | "version": "1.0.0",
4 | "description": "",
5 | "author": "Viljami Kuosmanen ",
6 | "license": "MIT",
7 | "keywords": [],
8 | "scripts": {
9 | "postinstall": "npm run build",
10 | "build": "tsc",
11 | "watch-build": "tsc -w",
12 | "start": "node dist/index.js",
13 | "watch-start": "nodemon --delay 2 -w dist/ -x 'npm run start'",
14 | "dev": "concurrently -k -p '[{name}]' -n 'typescript,api' -c 'yellow.bold,cyan.bold' npm:watch-build npm:watch-start",
15 | "lint": "tslint --format prose --project .",
16 | "test": "jest"
17 | },
18 | "dependencies": {
19 | "express": "^4.16.4",
20 | "morgan": "^1.9.1",
21 | "openapi-backend": "^5.2.0",
22 | "source-map-support": "^0.5.10"
23 | },
24 | "devDependencies": {
25 | "@types/express": "^4.17.12",
26 | "@types/jest": "^29.2.5",
27 | "@types/morgan": "^1.7.35",
28 | "@types/node": "^10.12.26",
29 | "axios": "^0.21.1",
30 | "concurrently": "^4.1.0",
31 | "jest": "^29.3.1",
32 | "nodemon": "^1.18.10",
33 | "ts-jest": "^29.0.3",
34 | "tslint": "^5.12.1",
35 | "typescript": "^4.3.2",
36 | "wait-on": "^3.2.0"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/examples/express-ts-mock/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "lib": ["es7", "esnext.asynciterable"],
7 | "experimentalDecorators": true,
8 | "emitDecoratorMetadata": true,
9 | "esModuleInterop": true,
10 | "noImplicitAny": true,
11 | "noUnusedLocals": true,
12 | "baseUrl": ".",
13 | "rootDir": ".",
14 | "outDir": "dist",
15 | "sourceMap": true,
16 | "skipLibCheck": true,
17 | "paths": {
18 | "*": [
19 | "node_modules/*",
20 | "types/*"
21 | ]
22 | }
23 | },
24 | "include": [
25 | "**/*"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/examples/express-ts-mock/types/wait-on.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'wait-on';
2 |
--------------------------------------------------------------------------------
/examples/express-typescript/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | dist/*
3 |
--------------------------------------------------------------------------------
/examples/express-typescript/README.md:
--------------------------------------------------------------------------------
1 | # OpenAPI Backend Express Example
2 | [](http://anttiviljami.mit-license.org)
3 |
4 | Example project using [openapi-backend](https://github.com/openapistack/openapi-backend) on [Express](hbackendttps://expressjs.com/)
5 |
6 | ## QuickStart
7 |
8 | ```
9 | npm install
10 | npm run dev # API running at http://localhost:9000
11 | ```
12 |
13 | Try the endpoints:
14 |
15 | ```bash
16 | curl -i http://localhost:9000/pets
17 | curl -i http://localhost:9000/pets/1
18 | ```
19 |
20 |
--------------------------------------------------------------------------------
/examples/express-typescript/index.test.ts:
--------------------------------------------------------------------------------
1 | import { spawn, ChildProcess } from 'child_process';
2 | import axios, { AxiosInstance } from 'axios';
3 | import waitOn from 'wait-on';
4 |
5 | jest.setTimeout(15000);
6 |
7 | describe('express-ts example', () => {
8 | let start: ChildProcess;
9 | let client: AxiosInstance;
10 |
11 | beforeAll(async () => {
12 | client = axios.create({ baseURL: 'http://localhost:9000', validateStatus: () => true });
13 | start = spawn('npm', ['start'], { cwd: __dirname, detached: true, stdio: 'inherit' });
14 | await waitOn({ resources: ['tcp:localhost:9000'] });
15 | });
16 |
17 | afterAll(() => process.kill(-start.pid));
18 |
19 | test('GET /pets returns 200 with matched operation', async () => {
20 | const res = await client.get('/pets');
21 | expect(res.status).toBe(200);
22 | expect(res.data).toEqual({ operationId: 'getPets' });
23 | });
24 |
25 | test('GET /pets/1 returns 200 with matched operation', async () => {
26 | const res = await client.get('/pets/1');
27 | expect(res.status).toBe(200);
28 | expect(res.data).toEqual({ operationId: 'getPetById' });
29 | });
30 |
31 | test('GET /pets/1a returns 400 with validation error', async () => {
32 | const res = await client.get('/pets/1a');
33 | expect(res.status).toBe(400);
34 | expect(res.data).toHaveProperty('err');
35 | });
36 |
37 | test('GET /unknown returns 404', async () => {
38 | const res = await client.get('/unknown');
39 | expect(res.status).toBe(404);
40 | expect(res.data).toHaveProperty('err');
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/examples/express-typescript/index.ts:
--------------------------------------------------------------------------------
1 | import 'source-map-support/register';
2 | import OpenAPIBackend from 'openapi-backend';
3 | import Express from 'express';
4 | import morgan from 'morgan';
5 |
6 | import type { Request } from 'openapi-backend';
7 |
8 | const app = Express();
9 | app.use(Express.json());
10 |
11 | // define api
12 | const api = new OpenAPIBackend({
13 | definition: {
14 | openapi: '3.0.1',
15 | info: {
16 | title: 'My API',
17 | version: '1.0.0',
18 | },
19 | paths: {
20 | '/pets': {
21 | get: {
22 | operationId: 'getPets',
23 | responses: {
24 | 200: { description: 'ok' },
25 | },
26 | },
27 | },
28 | '/pets/{id}': {
29 | get: {
30 | operationId: 'getPetById',
31 | responses: {
32 | 200: { description: 'ok' },
33 | },
34 | },
35 | parameters: [
36 | {
37 | name: 'id',
38 | in: 'path',
39 | required: true,
40 | schema: {
41 | type: 'integer',
42 | },
43 | },
44 | ],
45 | },
46 | },
47 | },
48 | handlers: {
49 | getPets: async (c, req: Express.Request, res: Express.Response) =>
50 | res.status(200).json({ operationId: c.operation.operationId }),
51 | getPetById: async (c, req: Express.Request, res: Express.Response) =>
52 | res.status(200).json({ operationId: c.operation.operationId }),
53 | validationFail: async (c, req: Express.Request, res: Express.Response) =>
54 | res.status(400).json({ err: c.validation.errors }),
55 | notFound: async (c, req: Express.Request, res: Express.Response) => res.status(404).json({ err: 'not found' }),
56 | },
57 | });
58 |
59 | api.init();
60 |
61 | // logging
62 | app.use(morgan('combined'));
63 |
64 | // use as express middleware
65 | app.use((req, res) => api.handleRequest(req as Request, req, res));
66 |
67 | // start server
68 | app.listen(9000, () => console.info('api listening at http://localhost:9000'));
69 |
--------------------------------------------------------------------------------
/examples/express-typescript/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | testMatch: [ '**/?(*.)+(spec|test).ts?(x)' ]
5 | };
6 |
--------------------------------------------------------------------------------
/examples/express-typescript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openapi-backend-express-typescript",
3 | "version": "1.0.0",
4 | "description": "",
5 | "author": "Viljami Kuosmanen ",
6 | "license": "MIT",
7 | "keywords": [],
8 | "scripts": {
9 | "postinstall": "npm run build",
10 | "build": "tsc",
11 | "watch-build": "tsc -w",
12 | "start": "node dist/index.js",
13 | "watch-start": "nodemon --delay 2 -w dist/ -x 'npm run start'",
14 | "dev": "concurrently -k -p '[{name}]' -n 'typescript,api' -c 'yellow.bold,cyan.bold' npm:watch-build npm:watch-start",
15 | "lint": "tslint --format prose --project .",
16 | "test": "jest"
17 | },
18 | "dependencies": {
19 | "express": "^4.16.4",
20 | "morgan": "^1.9.1",
21 | "openapi-backend": "^5.2.0",
22 | "source-map-support": "^0.5.10"
23 | },
24 | "devDependencies": {
25 | "@types/express": "^4.17.13",
26 | "@types/jest": "^29.2.5",
27 | "@types/morgan": "^1.7.35",
28 | "@types/node": "^10.12.26",
29 | "axios": "^0.21.1",
30 | "concurrently": "^6.2.0",
31 | "jest": "^29.3.1",
32 | "nodemon": "^1.18.10",
33 | "ts-jest": "^29.0.3",
34 | "tslint": "^5.12.1",
35 | "typescript": "^4.3.2",
36 | "wait-on": "^3.2.0"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/examples/express-typescript/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "lib": ["es7", "esnext.asynciterable"],
7 | "experimentalDecorators": true,
8 | "emitDecoratorMetadata": true,
9 | "esModuleInterop": true,
10 | "noImplicitAny": true,
11 | "noUnusedLocals": true,
12 | "baseUrl": ".",
13 | "rootDir": ".",
14 | "outDir": "dist",
15 | "sourceMap": true,
16 | "skipLibCheck": true,
17 | "paths": {
18 | "*": [
19 | "node_modules/*",
20 | "types/*"
21 | ]
22 | }
23 | },
24 | "include": [
25 | "**/*"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/examples/express-typescript/types/wait-on.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'wait-on';
2 |
--------------------------------------------------------------------------------
/examples/express/.gitignore:
--------------------------------------------------------------------------------
1 | !*.js
2 | node_modules/*
3 |
--------------------------------------------------------------------------------
/examples/express/README.md:
--------------------------------------------------------------------------------
1 | # OpenAPI Backend Simple Express Example
2 | [](http://anttiviljami.mit-license.org)
3 |
4 | Example project using [openapi-backend](https://github.com/openapistack/openapi-backend) on [Express](https://expressjs.com/)
5 |
6 | ## QuickStart
7 |
8 | ```
9 | npm install
10 | npm start # API running at http://localhost:9000
11 | ```
12 |
13 | Try the endpoints:
14 |
15 | ```bash
16 | curl -i http://localhost:9000/pets
17 | curl -i http://localhost:9000/pets/1
18 | ```
19 |
20 |
--------------------------------------------------------------------------------
/examples/express/index.js:
--------------------------------------------------------------------------------
1 | const OpenAPIBackend = require('openapi-backend').default;
2 | const express = require('express');
3 | const app = express();
4 | app.use(express.json());
5 |
6 | // define api
7 | const api = new OpenAPIBackend({
8 | definition: {
9 | openapi: '3.0.1',
10 | info: {
11 | title: 'My API',
12 | version: '1.0.0',
13 | },
14 | paths: {
15 | '/pets': {
16 | get: {
17 | operationId: 'getPets',
18 | responses: {
19 | 200: { description: 'ok' },
20 | },
21 | },
22 | },
23 | '/pets/{id}': {
24 | get: {
25 | operationId: 'getPetById',
26 | responses: {
27 | 200: { description: 'ok' },
28 | },
29 | },
30 | parameters: [
31 | {
32 | name: 'id',
33 | in: 'path',
34 | required: true,
35 | schema: {
36 | type: 'integer',
37 | },
38 | },
39 | ],
40 | },
41 | },
42 | },
43 | handlers: {
44 | getPets: async (c, req, res) => res.status(200).json({ operationId: c.operation.operationId }),
45 | getPetById: async (c, req, res) => res.status(200).json({ operationId: c.operation.operationId }),
46 | validationFail: async (c, req, res) => res.status(400).json({ err: c.validation.errors }),
47 | notFound: async (c, req, res) => res.status(404).json({ err: 'not found' }),
48 | },
49 | });
50 |
51 | api.init();
52 |
53 | // use as express middleware
54 | app.use((req, res) => api.handleRequest(req, req, res));
55 |
56 | // start server
57 | app.listen(9000, () => console.info('api listening at http://localhost:9000'));
58 |
--------------------------------------------------------------------------------
/examples/express/index.test.js:
--------------------------------------------------------------------------------
1 | const { spawn } = require('child_process');
2 | const waitOn = require('wait-on');
3 | const axios = require('axios');
4 |
5 | jest.setTimeout(30000);
6 |
7 | describe('express example', () => {
8 | let start;
9 | let client;
10 |
11 | beforeAll(async () => {
12 | client = axios.create({ baseURL: 'http://localhost:9000', validateStatus: () => true });
13 | start = spawn('npm', ['start'], { cwd: __dirname, detached: true, stdio: 'inherit' });
14 | await waitOn({ resources: ['tcp:localhost:9000'] });
15 | });
16 |
17 | afterAll(() => process.kill(-start.pid));
18 |
19 | test('GET /pets returns 200 with matched operation', async () => {
20 | const res = await client.get('/pets');
21 | expect(res.status).toBe(200);
22 | expect(res.data).toEqual({ operationId: 'getPets' });
23 | });
24 |
25 | test('GET /pets/1 returns 200 with matched operation', async () => {
26 | const res = await client.get('/pets/1');
27 | expect(res.status).toBe(200);
28 | expect(res.data).toEqual({ operationId: 'getPetById' });
29 | });
30 |
31 | test('GET /pets/1a returns 400 with validation error', async () => {
32 | const res = await client.get('/pets/1a');
33 | expect(res.status).toBe(400);
34 | expect(res.data).toHaveProperty('err');
35 | });
36 |
37 | test('GET /unknown returns 404', async () => {
38 | const res = await client.get('/unknown');
39 | expect(res.status).toBe(404);
40 | expect(res.data).toHaveProperty('err');
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/examples/express/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'node'
3 | };
4 |
--------------------------------------------------------------------------------
/examples/express/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openapi-backend-express",
3 | "version": "1.0.0",
4 | "description": "",
5 | "author": "Viljami Kuosmanen ",
6 | "license": "MIT",
7 | "keywords": [],
8 | "scripts": {
9 | "start": "node index.js",
10 | "test": "jest"
11 | },
12 | "dependencies": {
13 | "express": "^4.16.4",
14 | "openapi-backend": "^5.2.0"
15 | },
16 | "devDependencies": {
17 | "axios": "^0.21.1",
18 | "jest": "^29.3.1",
19 | "wait-on": "^3.2.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/fastify/.gitignore:
--------------------------------------------------------------------------------
1 | !*.js
2 | node_modules/*
3 |
--------------------------------------------------------------------------------
/examples/fastify/README.md:
--------------------------------------------------------------------------------
1 | # OpenAPI Backend Simple Fastify Example
2 | [](http://anttiviljami.mit-license.org)
3 |
4 | Example project using [openapi-backend](https://github.com/openapistack/openapi-backend) on [Fastify](https://www.fastify.io/)
5 |
6 | ## QuickStart
7 |
8 | ```
9 | npm install
10 | npm start # API running at http://localhost:9000
11 | ```
12 |
13 | Try the endpoints:
14 |
15 | ```bash
16 | curl -i http://localhost:9000/pets
17 | curl -i http://localhost:9000/pets/1
18 | ```
19 |
20 |
--------------------------------------------------------------------------------
/examples/fastify/index.js:
--------------------------------------------------------------------------------
1 | const OpenAPIBackend = require('openapi-backend').default;
2 |
3 | const fastify = require('fastify')();
4 |
5 | // define api
6 | const api = new OpenAPIBackend({
7 | definition: {
8 | openapi: '3.0.1',
9 | info: {
10 | title: 'My API',
11 | version: '1.0.0',
12 | },
13 | paths: {
14 | '/pets': {
15 | get: {
16 | operationId: 'getPets',
17 | responses: {
18 | 200: { description: 'ok' },
19 | },
20 | },
21 | },
22 | '/pets/{id}': {
23 | get: {
24 | operationId: 'getPetById',
25 | responses: {
26 | 200: { description: 'ok' },
27 | },
28 | },
29 | parameters: [
30 | {
31 | name: 'id',
32 | in: 'path',
33 | required: true,
34 | schema: {
35 | type: 'integer',
36 | },
37 | },
38 | ],
39 | },
40 | },
41 | },
42 | handlers: {
43 | getPets: async (c, request, reply) => reply.code(200).send({ operationId: c.operation.operationId }),
44 | getPetById: async (c, request, reply) => reply.code(200).send({ operationId: c.operation.operationId }),
45 | validationFail: async (c, request, reply) => reply.code(400).send({ err: c.validation.errors }),
46 | notFound: async (c, request, reply) => reply.code(404).send({ err: 'not found' }),
47 | },
48 | });
49 |
50 | api.init();
51 |
52 | // use as fastify middleware
53 | fastify.route({
54 | method: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
55 | url: '/*',
56 | handler: async (request, reply) =>
57 | api.handleRequest(
58 | {
59 | method: request.method,
60 | path: request.url,
61 | body: request.body,
62 | query: request.query,
63 | headers: request.headers,
64 | },
65 | request,
66 | reply,
67 | ),
68 | });
69 |
70 | // start server
71 | const start = async () => {
72 | try {
73 | await fastify.listen({ port: 9000 });
74 | console.info('api listening at http://localhost:9000');
75 | } catch (err) {
76 | fastify.log.error(err);
77 | process.exit(1);
78 | }
79 | };
80 | start();
81 |
--------------------------------------------------------------------------------
/examples/fastify/index.test.js:
--------------------------------------------------------------------------------
1 | const { spawn } = require('child_process');
2 | const waitOn = require('wait-on');
3 | const axios = require('axios');
4 |
5 | jest.setTimeout(30000);
6 |
7 | describe('express example', () => {
8 | let start;
9 | let client;
10 |
11 | beforeAll(async () => {
12 | client = axios.create({ baseURL: 'http://localhost:9000', validateStatus: () => true });
13 | start = spawn('npm', ['start'], { cwd: __dirname, detached: true, stdio: 'inherit' });
14 | await waitOn({ resources: ['tcp:localhost:9000'] });
15 | });
16 |
17 | afterAll(() => process.kill(-start.pid));
18 |
19 | test('GET /pets returns 200 with matched operation', async () => {
20 | const res = await client.get('/pets');
21 | expect(res.status).toBe(200);
22 | expect(res.data).toEqual({ operationId: 'getPets' });
23 | });
24 |
25 | test('GET /pets/1 returns 200 with matched operation', async () => {
26 | const res = await client.get('/pets/1');
27 | expect(res.status).toBe(200);
28 | expect(res.data).toEqual({ operationId: 'getPetById' });
29 | });
30 |
31 | test('GET /pets/1a returns 400 with validation error', async () => {
32 | const res = await client.get('/pets/1a');
33 | expect(res.status).toBe(400);
34 | expect(res.data).toHaveProperty('err');
35 | });
36 |
37 | test('GET /unknown returns 404', async () => {
38 | const res = await client.get('/unknown');
39 | expect(res.status).toBe(404);
40 | expect(res.data).toHaveProperty('err');
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/examples/fastify/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'node',
3 | };
4 |
--------------------------------------------------------------------------------
/examples/fastify/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openapi-backend-fastify",
3 | "version": "1.0.0",
4 | "description": "",
5 | "author": "Viljami Kuosmanen ",
6 | "license": "MIT",
7 | "keywords": [],
8 | "scripts": {
9 | "start": "node index.js",
10 | "test": "jest"
11 | },
12 | "dependencies": {
13 | "fastify": "^4.15.0",
14 | "openapi-backend": "^5.2.0"
15 | },
16 | "devDependencies": {
17 | "axios": "^0.21.1",
18 | "jest": "^29.3.1",
19 | "wait-on": "^3.2.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/hapi-typescript/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | dist/*
3 |
--------------------------------------------------------------------------------
/examples/hapi-typescript/README.md:
--------------------------------------------------------------------------------
1 | # OpenAPI Backend Hapi Example
2 | [](http://anttiviljami.mit-license.org)
3 |
4 | Example project using [openapi-backend](https://github.com/openapistack/openapi-backend) on [Hapi](https://hapijs.com/)
5 |
6 | ## QuickStart
7 |
8 | ```
9 | npm install
10 | npm run dev # API running at http://localhost:9000
11 | ```
12 |
13 | Try the endpoints:
14 |
15 | ```bash
16 | curl -i http://localhost:9000/pets
17 | curl -i http://localhost:9000/pets/1
18 | ```
19 |
20 |
--------------------------------------------------------------------------------
/examples/hapi-typescript/index.test.ts:
--------------------------------------------------------------------------------
1 | import { spawn, ChildProcess } from 'child_process';
2 | import axios, { AxiosInstance } from 'axios';
3 | import waitOn from 'wait-on';
4 |
5 | jest.setTimeout(15000);
6 |
7 | describe('hapi-ts example', () => {
8 | let start: ChildProcess;
9 | let client: AxiosInstance;
10 |
11 | beforeAll(async () => {
12 | client = axios.create({ baseURL: 'http://localhost:9000', validateStatus: () => true });
13 | start = spawn('npm', ['start'], { cwd: __dirname, detached: true, stdio: 'inherit' });
14 | await waitOn({ resources: ['tcp:localhost:9000'] });
15 | });
16 |
17 | afterAll(() => process.kill(-start.pid));
18 |
19 | test('GET /pets returns 200 with matched operation', async () => {
20 | const res = await client.get('/pets');
21 | expect(res.status).toBe(200);
22 | expect(res.data).toEqual({ operationId: 'getPets' });
23 | });
24 |
25 | test('GET /pets/1 returns 200 with matched operation', async () => {
26 | const res = await client.get('/pets/1');
27 | expect(res.status).toBe(200);
28 | expect(res.data).toEqual({ operationId: 'getPetById' });
29 | });
30 |
31 | test('GET /pets/1a returns 400 with validation error', async () => {
32 | const res = await client.get('/pets/1a');
33 | expect(res.status).toBe(400);
34 | expect(res.data).toHaveProperty('err');
35 | });
36 |
37 | test('GET /unknown returns 404', async () => {
38 | const res = await client.get('/unknown');
39 | expect(res.status).toBe(404);
40 | expect(res.data).toHaveProperty('err');
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/examples/hapi-typescript/index.ts:
--------------------------------------------------------------------------------
1 | import 'source-map-support/register';
2 | import OpenAPIBackend from 'openapi-backend';
3 | import Hapi from '@hapi/hapi';
4 |
5 | const server = new Hapi.Server({ port: 9000 });
6 |
7 | // define api
8 | const api = new OpenAPIBackend({
9 | definition: {
10 | openapi: '3.0.1',
11 | info: {
12 | title: 'My API',
13 | version: '1.0.0',
14 | },
15 | paths: {
16 | '/pets': {
17 | get: {
18 | operationId: 'getPets',
19 | responses: {
20 | 200: { description: 'ok' },
21 | },
22 | },
23 | },
24 | '/pets/{id}': {
25 | get: {
26 | operationId: 'getPetById',
27 | responses: {
28 | 200: { description: 'ok' },
29 | },
30 | },
31 | parameters: [
32 | {
33 | name: 'id',
34 | in: 'path',
35 | required: true,
36 | schema: {
37 | type: 'integer',
38 | },
39 | },
40 | ],
41 | },
42 | },
43 | },
44 | handlers: {
45 | getPets: async (context, req: Hapi.Request) => ({ operationId: context.operation.operationId }),
46 | getPetById: async (context, req: Hapi.Request) => ({ operationId: context.operation.operationId }),
47 | validationFail: async (context, req: Hapi.Request, h: Hapi.ResponseToolkit) =>
48 | h.response({ err: context.validation.errors }).code(400),
49 | notFound: async (context, req: Hapi.Request, h: Hapi.ResponseToolkit) =>
50 | h.response({ context, err: 'not found' }).code(404),
51 | },
52 | });
53 |
54 | api.init();
55 |
56 | // use as a catch-all handler
57 | server.route({
58 | method: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
59 | path: '/{path*}',
60 | handler: (req, h) =>
61 | api.handleRequest(
62 | {
63 | method: req.method,
64 | path: req.path,
65 | body: req.payload,
66 | query: req.query,
67 | headers: req.headers,
68 | },
69 | req,
70 | h,
71 | ),
72 | });
73 |
74 | // start server
75 | server.start().then(() => console.info(`listening on ${server.info.uri}`));
76 |
--------------------------------------------------------------------------------
/examples/hapi-typescript/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | testMatch: [ '**/?(*.)+(spec|test).ts?(x)' ]
5 | };
6 |
--------------------------------------------------------------------------------
/examples/hapi-typescript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openapi-backend-hapi-typescript",
3 | "version": "1.0.0",
4 | "description": "",
5 | "author": "Viljami Kuosmanen ",
6 | "license": "MIT",
7 | "keywords": [],
8 | "scripts": {
9 | "postinstall": "npm run build",
10 | "build": "tsc",
11 | "watch-build": "tsc -w",
12 | "start": "node dist/index.js",
13 | "watch-start": "nodemon --delay 2 -w dist/ -x 'npm run start'",
14 | "dev": "concurrently -k -p '[{name}]' -n 'typescript,api' -c 'yellow.bold,cyan.bold' npm:watch-build npm:watch-start",
15 | "lint": "tslint --format prose --project .",
16 | "test": "jest"
17 | },
18 | "dependencies": {
19 | "@hapi/hapi": "^20.1.3",
20 | "openapi-backend": "^5.2.0",
21 | "source-map-support": "^0.5.10"
22 | },
23 | "devDependencies": {
24 | "@types/hapi__hapi": "^20.0.8",
25 | "@types/jest": "^29.2.5",
26 | "@types/node": "^10.12.26",
27 | "axios": "^0.21.1",
28 | "concurrently": "^6.2.0",
29 | "jest": "^29.3.1",
30 | "nodemon": "^1.18.10",
31 | "ts-jest": "^29.0.3",
32 | "tslint": "^5.12.1",
33 | "typescript": "^4.3.2",
34 | "wait-on": "^3.2.0"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/examples/hapi-typescript/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "lib": ["es7", "esnext.asynciterable"],
7 | "experimentalDecorators": true,
8 | "emitDecoratorMetadata": true,
9 | "esModuleInterop": true,
10 | "noImplicitAny": true,
11 | "noUnusedLocals": true,
12 | "baseUrl": ".",
13 | "rootDir": ".",
14 | "outDir": "dist",
15 | "sourceMap": true,
16 | "skipLibCheck": true,
17 | "paths": {
18 | "*": [
19 | "node_modules/*",
20 | "types/*"
21 | ]
22 | }
23 | },
24 | "include": [
25 | "**/*"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/examples/hapi-typescript/types/wait-on.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'wait-on';
2 |
--------------------------------------------------------------------------------
/examples/koa/.gitignore:
--------------------------------------------------------------------------------
1 | !*.js
2 | node_modules/*
3 |
--------------------------------------------------------------------------------
/examples/koa/README.md:
--------------------------------------------------------------------------------
1 | # OpenAPI Backend Simple Koa Example
2 | [](http://anttiviljami.mit-license.org)
3 |
4 | Example project using [openapi-backend](https://github.com/openapistack/openapi-backend) on [Koa](https://koajs.com/)
5 |
6 | ## QuickStart
7 |
8 | ```
9 | npm install
10 | npm start # API running at http://localhost:9000
11 | ```
12 |
13 | Try the endpoints:
14 |
15 | ```bash
16 | curl -i http://localhost:9000/pets
17 | curl -i http://localhost:9000/pets/1
18 | ```
19 |
20 |
--------------------------------------------------------------------------------
/examples/koa/index.js:
--------------------------------------------------------------------------------
1 | const OpenAPIBackend = require('openapi-backend').default;
2 | const Koa = require('koa');
3 | const bodyparser = require('koa-bodyparser');
4 | const app = new Koa();
5 | app.use(bodyparser());
6 |
7 | // define api
8 | const api = new OpenAPIBackend({
9 | definition: {
10 | openapi: '3.0.1',
11 | info: {
12 | title: 'My API',
13 | version: '1.0.0',
14 | },
15 | paths: {
16 | '/pets': {
17 | get: {
18 | operationId: 'getPets',
19 | responses: {
20 | 200: { description: 'ok' },
21 | },
22 | },
23 | post: {
24 | operationId: 'createPet',
25 | requestBody: {
26 | description: 'Pet object to create',
27 | content: {
28 | 'application/json': {},
29 | },
30 | },
31 | responses: {
32 | 200: { description: 'ok' },
33 | },
34 | },
35 | },
36 | '/pets/{id}': {
37 | get: {
38 | operationId: 'getPetById',
39 | responses: {
40 | 200: { description: 'ok' },
41 | },
42 | },
43 | parameters: [
44 | {
45 | name: 'id',
46 | in: 'path',
47 | required: true,
48 | schema: {
49 | type: 'integer',
50 | },
51 | },
52 | ],
53 | },
54 | },
55 | },
56 | handlers: {
57 | getPets: async (c, ctx) => {
58 | ctx.body = { operationId: c.operation.operationId };
59 | },
60 | getPetById: async (c, ctx) => {
61 | ctx.body = { operationId: c.operation.operationId };
62 | },
63 | createPet: async (c, ctx) => {
64 | ctx.body = { operationId: c.operation.operationId };
65 | ctx.status = 201;
66 | },
67 | validationFail: async (c, ctx) => {
68 | ctx.body = { err: c.validation.errors };
69 | ctx.status = 400;
70 | },
71 | notFound: async (c, ctx) => {
72 | console.log(c);
73 | ctx.body = { err: 'not found' };
74 | ctx.status = 404;
75 | },
76 | },
77 | });
78 |
79 | api.init();
80 |
81 | // use as koa middleware
82 | app.use((ctx) => api.handleRequest(ctx.request, ctx));
83 |
84 | // start server
85 | app.listen(9000, () => console.info('api listening at http://localhost:9000'));
86 |
--------------------------------------------------------------------------------
/examples/koa/index.test.js:
--------------------------------------------------------------------------------
1 | const { spawn } = require('child_process');
2 | const waitOn = require('wait-on');
3 | const axios = require('axios');
4 |
5 | jest.setTimeout(30000);
6 |
7 | describe('koa example', () => {
8 | let start;
9 | let client;
10 |
11 | beforeAll(async () => {
12 | client = axios.create({ baseURL: 'http://localhost:9000', validateStatus: () => true });
13 | start = spawn('npm', ['start'], { cwd: __dirname, detached: true, stdio: 'inherit' });
14 | await waitOn({ resources: ['tcp:localhost:9000'] });
15 | });
16 |
17 | afterAll(() => process.kill(-start.pid));
18 |
19 | test('GET /pets returns 200 with matched operation', async () => {
20 | const res = await client.get('/pets');
21 | expect(res.status).toBe(200);
22 | expect(res.data).toEqual({ operationId: 'getPets' });
23 | });
24 |
25 | test('GET /pets/1 returns 200 with matched operation', async () => {
26 | const res = await client.get('/pets/1');
27 | expect(res.status).toBe(200);
28 | expect(res.data).toEqual({ operationId: 'getPetById' });
29 | });
30 |
31 | test('GET /pets/1a returns 400 with validation error', async () => {
32 | const res = await client.get('/pets/1a');
33 | expect(res.status).toBe(400);
34 | expect(res.data).toHaveProperty('err');
35 | });
36 |
37 | test('GET /unknown returns 404', async () => {
38 | const res = await client.get('/unknown');
39 | expect(res.status).toBe(404);
40 | expect(res.data).toHaveProperty('err');
41 | });
42 |
43 | test('POST /pets returns 201 with matched operation', async () => {
44 | const res = await client.post('/pets', {});
45 | expect(res.status).toBe(201);
46 | expect(res.data).toEqual({ operationId: 'createPet' });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/examples/koa/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'node'
3 | };
4 |
--------------------------------------------------------------------------------
/examples/koa/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openapi-backend-koa",
3 | "version": "1.0.0",
4 | "description": "",
5 | "author": "Viljami Kuosmanen ",
6 | "license": "MIT",
7 | "keywords": [],
8 | "scripts": {
9 | "start": "node index.js",
10 | "test": "jest"
11 | },
12 | "dependencies": {
13 | "koa": "^2.7.0",
14 | "koa-bodyparser": "^4.2.1",
15 | "openapi-backend": "^5.2.0"
16 | },
17 | "devDependencies": {
18 | "axios": "^0.21.1",
19 | "jest": "^29.3.1",
20 | "wait-on": "^3.2.0"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/serverless-framework/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | dist/*
3 |
--------------------------------------------------------------------------------
/examples/serverless-framework/README.md:
--------------------------------------------------------------------------------
1 | # OpenAPI Backend Serverless AWS Example
2 | [](http://anttiviljami.mit-license.org)
3 |
4 | Example project using [openapi-backend](https://github.com/openapistack/openapi-backend) on [Serverless Framework](https://serverless.com/)
5 |
6 | ## QuickStart
7 |
8 | ```
9 | npm install
10 | npm run dev # API running at http://localhost:9000
11 | ```
12 |
13 | Try the endpoints:
14 |
15 | ```bash
16 | curl -i http://localhost:9000/pets
17 | curl -i http://localhost:9000/pets/1
18 | ```
19 |
20 |
--------------------------------------------------------------------------------
/examples/serverless-framework/index.test.ts:
--------------------------------------------------------------------------------
1 | import { spawn, ChildProcess } from 'child_process';
2 | import axios, { AxiosInstance } from 'axios';
3 | import waitOn from 'wait-on';
4 |
5 | jest.setTimeout(15000);
6 |
7 | describe('serverless framework example', () => {
8 | let start: ChildProcess;
9 | let client: AxiosInstance;
10 |
11 | beforeAll(async () => {
12 | client = axios.create({ baseURL: 'http://localhost:3000/local', validateStatus: () => true });
13 | start = spawn('npm', ['start'], { cwd: __dirname, detached: true, stdio: 'inherit' });
14 | await waitOn({ resources: ['tcp:localhost:3000'] });
15 | });
16 |
17 | afterAll(() => process.kill(-start.pid));
18 |
19 | test('GET /pets returns 200 with matched operation', async () => {
20 | const res = await client.get('/pets');
21 | expect(res.status).toBe(200);
22 | expect(res.data).toEqual({ operationId: 'getPets' });
23 | });
24 |
25 | test('GET /pets/1 returns 200 with matched operation', async () => {
26 | const res = await client.get('/pets/1');
27 | expect(res.status).toBe(200);
28 | expect(res.data).toEqual({ operationId: 'getPetById' });
29 | });
30 |
31 | test('GET /pets/1a returns 400 with validation error', async () => {
32 | const res = await client.get('/pets/1a');
33 | expect(res.status).toBe(400);
34 | expect(res.data).toHaveProperty('err');
35 | });
36 |
37 | test('GET /unknown returns 404', async () => {
38 | const res = await client.get('/unknown');
39 | expect(res.status).toBe(404);
40 | expect(res.data).toHaveProperty('err');
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/examples/serverless-framework/index.ts:
--------------------------------------------------------------------------------
1 | import 'source-map-support/register';
2 | import * as Lambda from 'aws-lambda';
3 | import OpenAPIBackend from 'openapi-backend';
4 | const headers = {
5 | 'content-type': 'application/json',
6 | 'access-control-allow-origin': '*', // lazy cors config
7 | };
8 |
9 | // create api from definition
10 | const api = new OpenAPIBackend({ definition: './openapi.yml', quick: true });
11 |
12 | // register some handlers
13 | api.register({
14 | notFound: async (c, event: Lambda.APIGatewayProxyEvent, context: Lambda.Context) => ({
15 | statusCode: 404,
16 | body: JSON.stringify({ err: 'not found' }),
17 | headers,
18 | }),
19 | validationFail: async (c, event: Lambda.APIGatewayProxyEvent, context: Lambda.Context) => ({
20 | statusCode: 400,
21 | body: JSON.stringify({ err: c.validation.errors }),
22 | headers,
23 | }),
24 | getPets: async (c, event: Lambda.APIGatewayProxyEvent, context: Lambda.Context) => ({
25 | statusCode: 200,
26 | body: JSON.stringify({ operationId: c.operation.operationId }),
27 | headers,
28 | }),
29 | getPetById: async (c, event: Lambda.APIGatewayProxyEvent, context: Lambda.Context) => ({
30 | statusCode: 200,
31 | body: JSON.stringify({ operationId: c.operation.operationId }),
32 | headers,
33 | }),
34 | });
35 |
36 | // init api
37 | api.init();
38 |
39 | export async function handler(event: Lambda.APIGatewayProxyEvent, context: Lambda.Context) {
40 | return api.handleRequest(
41 | {
42 | method: event.httpMethod,
43 | path: event.path,
44 | query: event.queryStringParameters,
45 | body: event.body,
46 | headers: event.headers,
47 | },
48 | event,
49 | context,
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/examples/serverless-framework/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | testMatch: [ '**/?(*.)+(spec|test).ts?(x)' ]
5 | };
6 |
--------------------------------------------------------------------------------
/examples/serverless-framework/openapi.yml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.1
2 | info:
3 | title: 'My API'
4 | version: 1.0.0
5 | paths:
6 | '/pets':
7 | get:
8 | operationId: getPets
9 | responses:
10 | '200':
11 | description: ok
12 | '/pets/{id}':
13 | get:
14 | operationId: getPetById
15 | responses:
16 | '200':
17 | description: ok
18 | parameters:
19 | - name: id
20 | in: path
21 | required: true
22 | schema:
23 | type: integer
24 |
25 |
--------------------------------------------------------------------------------
/examples/serverless-framework/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openapi-backend-serverless-aws",
3 | "version": "1.0.0",
4 | "description": "",
5 | "author": "Viljami Kuosmanen ",
6 | "license": "MIT",
7 | "keywords": [],
8 | "scripts": {
9 | "postinstall": "npm run build",
10 | "build": "tsc",
11 | "watch-build": "tsc -w",
12 | "start": "serverless offline start --stage local",
13 | "watch-start": "nodemon --delay 2 -w serverless.yml -w dist/ -x 'npm run start'",
14 | "dev": "concurrently -k -p '[{name}]' -n 'typescript,api' -c 'yellow.bold,cyan.bold' npm:watch-build npm:watch-start",
15 | "lint": "tslint --format prose --project .",
16 | "test": "jest"
17 | },
18 | "dependencies": {
19 | "openapi-backend": "^5.2.0",
20 | "source-map-support": "^0.5.10"
21 | },
22 | "devDependencies": {
23 | "@types/aws-lambda": "^8.10.19",
24 | "@types/jest": "^29.2.5",
25 | "@types/node": "^10.12.26",
26 | "axios": "^0.21.1",
27 | "concurrently": "^6.2.0",
28 | "jest": "^29.3.1",
29 | "serverless": "^1.67.0",
30 | "serverless-offline": "^7.0.0",
31 | "ts-jest": "^29.0.3",
32 | "tslint": "^5.12.1",
33 | "typescript": "^4.3.2",
34 | "wait-on": "^3.2.0"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/examples/serverless-framework/serverless.yml:
--------------------------------------------------------------------------------
1 | service: openapi-backend-serverless-aws
2 |
3 | provider:
4 | name: aws
5 | runtime: nodejs8.10
6 | region: eu-west-1
7 | stage: dev
8 |
9 | functions:
10 | api:
11 | handler: dist/index.handler
12 | events:
13 | - http:
14 | path: /{proxy+}
15 | method: ANY
16 |
17 | plugins:
18 | - serverless-offline
19 |
20 | custom:
21 | serverless-offline:
22 | port: 9000
23 |
--------------------------------------------------------------------------------
/examples/serverless-framework/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "lib": ["esnext"],
7 | "experimentalDecorators": true,
8 | "emitDecoratorMetadata": true,
9 | "esModuleInterop": true,
10 | "noImplicitAny": true,
11 | "noUnusedLocals": true,
12 | "baseUrl": ".",
13 | "rootDir": ".",
14 | "outDir": "dist",
15 | "sourceMap": true
16 | },
17 | "include": [
18 | "**/*"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/examples/serverless-framework/types/wait-on.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'wait-on';
2 |
--------------------------------------------------------------------------------
/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openapistack/openapi-backend/a51f5d2eda406d13f6bdbf77d3e4a7dc7d4760d2/header.png
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | testMatch: ['**/?(*.)+(spec|test).ts?(x)'],
5 | testPathIgnorePatterns: ['node_modules', 'examples'],
6 | verbose: true,
7 | silent: true,
8 | };
9 |
--------------------------------------------------------------------------------
/lgtm.yml:
--------------------------------------------------------------------------------
1 | ##
2 | # Classify files under examples/ as example code so they aren't included in the
3 | # LGTM analysis
4 | ##
5 | path_classifiers:
6 | examples:
7 | - examples/
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openapi-backend",
3 | "description": "Build, Validate, Route, Authenticate and Mock using OpenAPI definitions. Framework-agnostic",
4 | "version": "5.13.0",
5 | "author": "Viljami Kuosmanen ",
6 | "funding": "https://github.com/sponsors/anttiviljami",
7 | "license": "MIT",
8 | "keywords": [
9 | "openapi",
10 | "swagger",
11 | "server",
12 | "router",
13 | "validation",
14 | "mock",
15 | "express",
16 | "hapi",
17 | "koa",
18 | "serverless",
19 | "lambda",
20 | "azure"
21 | ],
22 | "homepage": "https://openapistack.co",
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/openapistack/openapi-backend.git"
26 | },
27 | "bugs": {
28 | "url": "https://github.com/openapistack/openapi-backend/issues"
29 | },
30 | "main": "index.js",
31 | "types": "index.d.ts",
32 | "files": [
33 | "*.js",
34 | "*.d.ts",
35 | "*.map",
36 | "!*.test.*",
37 | "!**/*.test.*",
38 | "!scripts",
39 | "!node_modules",
40 | "!examples",
41 | "!src",
42 | "!*.config.js"
43 | ],
44 | "dependencies": {
45 | "@apidevtools/json-schema-ref-parser": "^11.1.0",
46 | "ajv": "^8.6.2",
47 | "bath-es5": "^3.0.3",
48 | "cookie": "^1.0.1",
49 | "dereference-json-schema": "^0.2.1",
50 | "lodash": "^4.17.15",
51 | "mock-json-schema": "^1.0.7",
52 | "openapi-schema-validator": "^12.0.0",
53 | "openapi-types": "^12.0.2",
54 | "qs": "^6.9.3"
55 | },
56 | "devDependencies": {
57 | "@types/cookie": "^1.0.0",
58 | "@types/jest": "^29.5.12",
59 | "@types/json-schema": "^7.0.7",
60 | "@types/lodash": "^4.14.122",
61 | "@types/node": "^20.0.0",
62 | "@types/qs": "^6.9.1",
63 | "@typescript-eslint/eslint-plugin": "^6.7.4",
64 | "@typescript-eslint/parser": "^6.7.4",
65 | "eslint": "^8.51.0",
66 | "jest": "^29.7.0",
67 | "prettier": "^3.0.0",
68 | "source-map-support": "^0.5.10",
69 | "ts-jest": "^29.1.1",
70 | "typescript": "^5.2.2"
71 | },
72 | "scripts": {
73 | "build": "tsc",
74 | "watch-build": "tsc -w",
75 | "lint": "eslint src/ --ext ts",
76 | "prettier": "prettier --write src/**",
77 | "prepublishOnly": "npm run build",
78 | "test": "NODE_ENV=test jest"
79 | },
80 | "engines": {
81 | "node": ">=12.0.0"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { OpenAPIBackend } from './backend';
2 |
3 | export default OpenAPIBackend;
4 |
5 | export * from './backend';
6 | export * from './router';
7 | export * from './validation';
8 |
--------------------------------------------------------------------------------
/src/refparser.ts:
--------------------------------------------------------------------------------
1 | import $RefParser from '@apidevtools/json-schema-ref-parser';
2 |
3 | // fixes issue with newer typescript versions
4 | // https://github.com/APIDevTools/json-schema-ref-parser/issues/139
5 |
6 | $RefParser.dereference = $RefParser.dereference.bind($RefParser);
7 | $RefParser.resolve = $RefParser.resolve.bind($RefParser);
8 | $RefParser.parse = $RefParser.parse.bind($RefParser);
9 |
10 | const { dereference, parse, resolve } = $RefParser;
11 |
12 | export { parse, dereference, resolve };
13 |
--------------------------------------------------------------------------------
/src/router.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 |
3 | import { OpenAPIRouter, Operation } from './router';
4 | import { OpenAPIBackend, Context } from './backend';
5 | import { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
6 |
7 | const headers = { accept: 'application/json' };
8 |
9 | const responses: OpenAPIV3.ResponsesObject & OpenAPIV3_1.ResponsesObject = {
10 | 200: { description: 'ok' },
11 | };
12 |
13 | const pathId: OpenAPIV3_1.ParameterObject = {
14 | name: 'id',
15 | in: 'path',
16 | required: true,
17 | schema: {
18 | type: 'integer',
19 | },
20 | };
21 |
22 | const hobbyId: OpenAPIV3_1.ParameterObject = {
23 | name: 'hobbyId',
24 | in: 'path',
25 | required: true,
26 | schema: {
27 | type: 'integer',
28 | },
29 | };
30 |
31 | const queryLimit: OpenAPIV3_1.ParameterObject = {
32 | name: 'limit',
33 | in: 'query',
34 | schema: {
35 | type: 'integer',
36 | minimum: 1,
37 | maximum: 100,
38 | },
39 | };
40 |
41 | const queryFilter: OpenAPIV3_1.ParameterObject = {
42 | name: 'filter',
43 | in: 'query',
44 | content: {
45 | 'application/json': {
46 | schema: {
47 | type: 'object',
48 | properties: {
49 | hasOwner: {
50 | type: 'boolean',
51 | },
52 | age: {
53 | type: 'integer',
54 | },
55 | name: {
56 | type: 'string',
57 | },
58 | },
59 | },
60 | },
61 | },
62 | };
63 |
64 | const definition: OpenAPIV3_1.Document = {
65 | openapi: '3.1.0',
66 | info: {
67 | title: 'api',
68 | version: '1.0.0',
69 | },
70 | paths: {
71 | '/': {
72 | get: {
73 | operationId: 'apiRoot',
74 | responses,
75 | },
76 | },
77 | '/pets': {
78 | get: {
79 | operationId: 'getPets',
80 | responses,
81 | },
82 | post: {
83 | operationId: 'createPet',
84 | responses,
85 | },
86 | parameters: [queryLimit, queryFilter],
87 | },
88 | '/pets/{id}': {
89 | get: {
90 | operationId: 'getPetById',
91 | responses,
92 | },
93 | put: {
94 | operationId: 'replacePetById',
95 | responses,
96 | },
97 | patch: {
98 | operationId: 'updatePetById',
99 | responses,
100 | },
101 | delete: {
102 | operationId: 'deletePetById',
103 | responses,
104 | },
105 | parameters: [pathId],
106 | },
107 | '/pets/{id}/owner': {
108 | get: {
109 | operationId: 'getOwnerByPetId',
110 | responses,
111 | },
112 | parameters: [pathId],
113 | },
114 | '/pets/{id}/hobbies/{hobbyId}': {
115 | get: {
116 | operationId: 'getPetHobbies',
117 | responses,
118 | },
119 | parameters: [pathId, hobbyId],
120 | },
121 | '/pets/meta': {
122 | get: {
123 | operationId: 'getPetsMeta',
124 | responses,
125 | },
126 | },
127 | },
128 | };
129 |
130 | describe('OpenAPIRouter', () => {
131 | describe('.parseRequest', () => {
132 | const api = new OpenAPIRouter({ definition });
133 |
134 | test('parses requests', () => {
135 | const request = { path: '/', method: 'get', headers };
136 |
137 | const parsedRequest = api.parseRequest(request);
138 |
139 | expect(parsedRequest.path).toEqual('/');
140 | expect(parsedRequest.method).toEqual('get');
141 | expect(parsedRequest.query).toEqual({});
142 | expect(parsedRequest.headers).toEqual(headers);
143 | });
144 |
145 | test('parses request body passed as object', () => {
146 | const payload = { horse: 1 };
147 | const request = { path: '/pets', method: 'post', body: payload, headers };
148 |
149 | const parsedRequest = api.parseRequest(request);
150 |
151 | expect(parsedRequest.requestBody).toEqual(payload);
152 | });
153 |
154 | test('parses request body passed as JSON', () => {
155 | const payload = { horse: 1 };
156 | const request = { path: '/pets', method: 'post', body: JSON.stringify(payload), headers };
157 |
158 | const parsedRequest = api.parseRequest(request);
159 |
160 | expect(parsedRequest.requestBody).toEqual(payload);
161 | });
162 |
163 | test('parses path parameters', () => {
164 | const request = { path: '/pets/123', method: 'get', headers };
165 | const operation = api.getOperation('getPetById')!;
166 |
167 | const parsedRequest = api.parseRequest(request, operation);
168 | expect(parsedRequest.params).toEqual({ id: '123' });
169 | });
170 |
171 | test('parses query string from path prop', () => {
172 | const request = { path: '/pets?limit=10', method: 'get', headers };
173 |
174 | const parsedRequest = api.parseRequest(request);
175 |
176 | expect(parsedRequest.query).toEqual({ limit: '10' });
177 | });
178 |
179 | test('parses query string from query prop', () => {
180 | const request = { path: '/pets', query: 'limit=10', method: 'get', headers };
181 |
182 | const parsedRequest = api.parseRequest(request);
183 |
184 | expect(parsedRequest.query).toEqual({ limit: '10' });
185 | });
186 |
187 | test('parses query string from query prop starting with ?', () => {
188 | const request = { path: '/pets', query: '?limit=10', method: 'get', headers };
189 |
190 | const parsedRequest = api.parseRequest(request);
191 |
192 | expect(parsedRequest.query).toEqual({ limit: '10' });
193 | });
194 |
195 | test("parses query string content 'application/json' as JSON", () => {
196 | const filterValue = { age: 4, hasOwner: true, name: 'Spot' };
197 | const encoded = encodeURI(JSON.stringify(filterValue));
198 | const request = { path: `/pets?filter=${encoded}`, method: 'get', headers };
199 |
200 | const operation = api.getOperation('getPets')!;
201 | const parsedRequest = api.parseRequest(request, operation);
202 |
203 | expect(parsedRequest.query.filter).toEqual(filterValue);
204 | });
205 |
206 | test('parses query string arrays', () => {
207 | const request = { path: '/pets?limit=10&limit=20', method: 'get', headers };
208 | const parsedRequest = api.parseRequest(request);
209 | expect(parsedRequest.query).toEqual({ limit: ['10', '20'] });
210 | });
211 |
212 | test('parses query string arrays when style=form, explode=false', () => {
213 | const request = { path: '/pets?limit=10,20', method: 'get', headers };
214 | const operation = api.getOperation('createPet')!;
215 | operation.parameters = [
216 | {
217 | in: 'query',
218 | name: 'limit',
219 | style: 'form',
220 | explode: false,
221 | },
222 | ];
223 |
224 | const parsedRequest = api.parseRequest(request, operation);
225 | expect(parsedRequest.query).toEqual({ limit: ['10', '20'] });
226 | });
227 |
228 | test('parses query parameter arrays when style=form, explode=false', () => {
229 | const request = { path: '/pets', query: { limit: '10,20' }, method: 'get', headers };
230 | const operation = api.getOperation('createPet')!;
231 | operation.parameters = [
232 | {
233 | in: 'query',
234 | name: 'limit',
235 | style: 'form',
236 | explode: false,
237 | },
238 | ];
239 |
240 | const parsedRequest = api.parseRequest(request, operation);
241 | expect(parsedRequest.query).toEqual({ limit: ['10', '20'] });
242 | });
243 |
244 | test('parses query string arrays with encoded commas when style=form, explode=false', () => {
245 | const request = { path: '/pets?limit=10%2C20', method: 'get', headers };
246 | const operation = api.getOperation('createPet')!;
247 | operation.parameters = [
248 | {
249 | in: 'query',
250 | name: 'limit',
251 | style: 'form',
252 | explode: false,
253 | },
254 | ];
255 |
256 | const parsedRequest = api.parseRequest(request, operation);
257 | expect(parsedRequest.query).toEqual({ limit: ['10', '20'] });
258 | });
259 |
260 | test('parses query parameter arrays with encoded commas when style=form, explode=false', () => {
261 | const request = { path: '/pets', query: { limit: '10%2C20' }, method: 'get', headers };
262 | const operation = api.getOperation('createPet')!;
263 | operation.parameters = [
264 | {
265 | in: 'query',
266 | name: 'limit',
267 | style: 'form',
268 | explode: false,
269 | },
270 | ];
271 |
272 | const parsedRequest = api.parseRequest(request, operation);
273 | expect(parsedRequest.query).toEqual({ limit: ['10', '20'] });
274 | });
275 |
276 | test('parses query string arrays when style=spaceDelimited, explode=false', () => {
277 | const request = { path: '/pets?limit=10%2020', method: 'get', headers };
278 | const operation = api.getOperation('createPet')!;
279 | operation.parameters = [
280 | {
281 | in: 'query',
282 | name: 'limit',
283 | style: 'spaceDelimited',
284 | explode: false,
285 | },
286 | ];
287 |
288 | const parsedRequest = api.parseRequest(request, operation);
289 | expect(parsedRequest.query).toEqual({ limit: ['10', '20'] });
290 | });
291 |
292 | test('parses query parameter arrays when style=spaceDelimited, explode=false', () => {
293 | const request = { path: '/pets', query: { limit: '10%2020' }, method: 'get', headers };
294 | const operation = api.getOperation('createPet')!;
295 | operation.parameters = [
296 | {
297 | in: 'query',
298 | name: 'limit',
299 | style: 'spaceDelimited',
300 | explode: false,
301 | },
302 | ];
303 |
304 | const parsedRequest = api.parseRequest(request, operation);
305 | expect(parsedRequest.query).toEqual({ limit: ['10', '20'] });
306 | });
307 |
308 | test('parses query string arrays when style=pipeDelimited, explode=false', () => {
309 | const request = { path: '/pets?limit=10|20', method: 'get', headers };
310 | const operation = api.getOperation('createPet')!;
311 | operation.parameters = [
312 | {
313 | in: 'query',
314 | name: 'limit',
315 | style: 'pipeDelimited',
316 | explode: false,
317 | },
318 | ];
319 |
320 | const parsedRequest = api.parseRequest(request, operation);
321 | expect(parsedRequest.query).toEqual({ limit: ['10', '20'] });
322 | });
323 |
324 | test('parses query parameter arrays when style=pipeDelimited, explode=false', () => {
325 | const request = { path: '/pets', query: { limit: '10|20' }, method: 'get', headers };
326 | const operation = api.getOperation('createPet')!;
327 | operation.parameters = [
328 | {
329 | in: 'query',
330 | name: 'limit',
331 | style: 'pipeDelimited',
332 | explode: false,
333 | },
334 | ];
335 |
336 | const parsedRequest = api.parseRequest(request, operation);
337 | expect(parsedRequest.query).toEqual({ limit: ['10', '20'] });
338 | });
339 | });
340 |
341 | describe('.matchOperation', () => {
342 | const api = new OpenAPIRouter({ definition });
343 |
344 | test('matches GET /', async () => {
345 | const { operationId } = api.matchOperation({ path: '/', method: 'get', headers }) as Operation;
346 | expect(operationId).toEqual('apiRoot');
347 | });
348 |
349 | test('matches GET /pets', async () => {
350 | const { operationId } = api.matchOperation({ path: '/pets', method: 'get', headers }) as Operation;
351 | expect(operationId).toEqual('getPets');
352 | });
353 |
354 | test('matches GET /pets/ with trailing slash', async () => {
355 | const { operationId } = api.matchOperation({ path: '/pets/', method: 'get', headers }) as Operation;
356 | expect(operationId).toEqual('getPets');
357 | });
358 |
359 | test('matches POST /pets', async () => {
360 | const { operationId } = api.matchOperation({ path: '/pets', method: 'post', headers }) as Operation;
361 | expect(operationId).toEqual('createPet');
362 | });
363 |
364 | test('matches GET /pets/{id}', async () => {
365 | const { operationId } = api.matchOperation({ path: '/pets/1', method: 'get', headers }) as Operation;
366 | expect(operationId).toEqual('getPetById');
367 | });
368 |
369 | test('matches PUT /pets/{id}', async () => {
370 | const { operationId } = api.matchOperation({ path: '/pets/1', method: 'put', headers }) as Operation;
371 | expect(operationId).toEqual('replacePetById');
372 | });
373 |
374 | test('matches PATCH /pets/{id}', async () => {
375 | const { operationId } = api.matchOperation({ path: '/pets/1', method: 'patch', headers }) as Operation;
376 | expect(operationId).toEqual('updatePetById');
377 | });
378 |
379 | test('matches DELETE /pets/{id}', async () => {
380 | const { operationId } = api.matchOperation({ path: '/pets/1', method: 'delete', headers }) as Operation;
381 | expect(operationId).toEqual('deletePetById');
382 | });
383 |
384 | test('matches GET /pets/{id}/owner', async () => {
385 | const { operationId } = api.matchOperation({ path: '/pets/1/owner', method: 'get', headers }) as Operation;
386 | expect(operationId).toEqual('getOwnerByPetId');
387 | });
388 |
389 | test('matches GET /pets/{id}/hobbies/{hobbyId}', async () => {
390 | const { operationId } = api.matchOperation({ path: '/pets/1/hobbies/3', method: 'get', headers }) as Operation;
391 | expect(operationId).toEqual('getPetHobbies');
392 | });
393 |
394 | test('matches GET /pets/meta', async () => {
395 | const { operationId } = api.matchOperation({ path: '/pets/meta', method: 'get', headers }) as Operation;
396 | expect(operationId).toEqual('getPetsMeta');
397 | });
398 |
399 | test('does not match GET /v2/pets', async () => {
400 | const operation = api.matchOperation({ path: '/v2/pets', method: 'get', headers }) as Operation;
401 | expect(operation).toBe(undefined);
402 | });
403 | });
404 |
405 | describe('.matchOperation with ignoreTrailingSlashes=false', () => {
406 | const api = new OpenAPIRouter({ definition, ignoreTrailingSlashes: false });
407 |
408 | test('matches GET /', async () => {
409 | const { operationId } = api.matchOperation({ path: '/', method: 'get', headers }) as Operation;
410 | expect(operationId).toEqual('apiRoot');
411 | });
412 |
413 | test('matches GET /pets', async () => {
414 | const { operationId } = api.matchOperation({ path: '/pets', method: 'get', headers }) as Operation;
415 | expect(operationId).toEqual('getPets');
416 | });
417 |
418 | test('does not match GET /pets/ with trailing slash', async () => {
419 | const operation = api.matchOperation({ path: '/pets/', method: 'get', headers }) as Operation;
420 | expect(operation).toBe(undefined);
421 | });
422 | });
423 |
424 | describe('.matchOperation with apiRoot = /api', () => {
425 | const api = new OpenAPIRouter({ definition, apiRoot: '/api' });
426 |
427 | test('matches GET /api as apiRoot', async () => {
428 | const { operationId } = api.matchOperation({ path: '/api', method: 'get', headers }) as Operation;
429 | expect(operationId).toEqual('apiRoot');
430 | });
431 |
432 | test('matches GET /api/pets as getPets', async () => {
433 | const { operationId } = api.matchOperation({ path: '/api/pets', method: 'get', headers }) as Operation;
434 | expect(operationId).toEqual('getPets');
435 | });
436 |
437 | test('does not match GET /pets', async () => {
438 | const operation = api.matchOperation({ path: '/pets', method: 'get', headers }) as Operation;
439 | expect(operation).toBe(undefined);
440 | });
441 | });
442 |
443 | describe('.matchOperation with strict mode', () => {
444 | const api = new OpenAPIRouter({ definition });
445 |
446 | test('matches GET /', async () => {
447 | const { operationId } = api.matchOperation({ path: '/', method: 'get', headers }, true);
448 | expect(operationId).toEqual('apiRoot');
449 | });
450 |
451 | test('matches GET /pets', async () => {
452 | const { operationId } = api.matchOperation({ path: '/pets', method: 'get', headers }, true);
453 | expect(operationId).toEqual('getPets');
454 | });
455 |
456 | test('throws a 404 for GET /humans', async () => {
457 | const call = () => api.matchOperation({ path: '/humans', method: 'get', headers }, true);
458 | expect(call).toThrowError('404-notFound: no route matches request');
459 | });
460 |
461 | test('throws a 405 for DELETE /pets', async () => {
462 | const call = () => api.matchOperation({ path: '/pets', method: 'delete', headers }, true);
463 | expect(call).toThrowError('405-methodNotAllowed: this method is not registered for the route');
464 | });
465 | });
466 | });
467 |
468 | describe('OpenAPIBackend', () => {
469 | describe('.handleRequest', () => {
470 | const dummyHandlers: { [operationId: string]: jest.Mock } = {};
471 | const dummyHandler = (operationId: string) => (dummyHandlers[operationId] = jest.fn(() => ({ operationId })));
472 | const api = new OpenAPIBackend({
473 | definition,
474 | handlers: {
475 | apiRoot: dummyHandler('apiRoot'),
476 | getPets: dummyHandler('getPets'),
477 | getPetById: dummyHandler('getPetById'),
478 | createPet: dummyHandler('createPet'),
479 | updatePetById: dummyHandler('updatePetById'),
480 | notImplemented: dummyHandler('notImplemented'),
481 | notFound: dummyHandler('notFound'),
482 | },
483 | });
484 | beforeAll(() => api.init());
485 |
486 | test('handles GET / and passes context', async () => {
487 | const res = await api.handleRequest({ method: 'GET', path: '/', headers });
488 | expect(res).toEqual({ operationId: 'apiRoot' });
489 | expect(dummyHandlers['apiRoot']).toBeCalled();
490 |
491 | const contextArg = dummyHandlers['apiRoot'].mock.calls.slice(-1)[0][0];
492 | expect(contextArg.request).toMatchObject({ method: 'get', path: '/', headers });
493 | expect(contextArg.operation.operationId).toEqual('apiRoot');
494 | expect(contextArg.validation.errors).toBeFalsy();
495 | });
496 |
497 | test('handles GET /pets and passes context', async () => {
498 | const res = await api.handleRequest({ method: 'GET', path: '/pets', headers });
499 | expect(res).toEqual({ operationId: 'getPets' });
500 | expect(dummyHandlers['getPets']).toBeCalled();
501 |
502 | const contextArg = dummyHandlers['getPets'].mock.calls.slice(-1)[0][0];
503 | expect(contextArg.request).toMatchObject({ method: 'get', path: '/pets', headers });
504 | expect(contextArg.operation.operationId).toEqual('getPets');
505 | expect(contextArg.validation.errors).toBeFalsy();
506 | });
507 |
508 | test('handles POST /pets and passes context', async () => {
509 | const res = await api.handleRequest({ method: 'POST', path: '/pets', headers });
510 | expect(res).toEqual({ operationId: 'createPet' });
511 | expect(dummyHandlers['createPet']).toBeCalled();
512 |
513 | const contextArg = dummyHandlers['createPet'].mock.calls.slice(-1)[0][0];
514 | expect(contextArg.request).toMatchObject({ method: 'post', path: '/pets', headers });
515 | expect(contextArg.operation.operationId).toEqual('createPet');
516 | expect(contextArg.validation.errors).toBeFalsy();
517 | });
518 |
519 | test('handles GET /pets/1 and passes context', async () => {
520 | const res = await api.handleRequest({ method: 'GET', path: '/pets/1', headers });
521 | expect(res).toEqual({ operationId: 'getPetById' });
522 | expect(dummyHandlers['getPetById']).toBeCalled();
523 |
524 | const contextArg = dummyHandlers['getPetById'].mock.calls.slice(-1)[0][0];
525 | expect(contextArg.request).toMatchObject({ method: 'get', path: '/pets/1', params: { id: '1' }, headers });
526 | expect(contextArg.operation.operationId).toEqual('getPetById');
527 | expect(contextArg.validation.errors).toBeFalsy();
528 | });
529 |
530 | test('handles PATCH /pets/1 and passes context', async () => {
531 | const res = await api.handleRequest({ method: 'PATCH', path: '/pets/1', headers });
532 | expect(res).toEqual({ operationId: 'updatePetById' });
533 | expect(dummyHandlers['updatePetById']).toBeCalled();
534 |
535 | const contextArg = dummyHandlers['updatePetById'].mock.calls.slice(-1)[0][0];
536 | expect(contextArg.request).toMatchObject({ method: 'patch', path: '/pets/1', params: { id: '1' }, headers });
537 | expect(contextArg.operation.operationId).toEqual('updatePetById');
538 | expect(contextArg.validation.errors).toBeFalsy();
539 | });
540 |
541 | test('handles a 404 for unregistered endpoint GET /humans and passes context', async () => {
542 | const res = await api.handleRequest({ method: 'GET', path: '/humans', headers });
543 | expect(res).toEqual({ operationId: 'notFound' });
544 |
545 | const contextArg = dummyHandlers['notFound'].mock.calls.slice(-1)[0][0];
546 | expect(contextArg.request).toMatchObject({ method: 'get', path: '/humans', headers });
547 | expect(contextArg.operation).toBeFalsy();
548 | });
549 |
550 | test('handles a 501 for not implemented endpoint DELETE /pets/1 and passes context', async () => {
551 | const res = await api.handleRequest({ method: 'DELETE', path: '/pets/1', headers });
552 | expect(res).toEqual({ operationId: 'notImplemented' });
553 | expect(dummyHandlers['notImplemented']).toBeCalled();
554 |
555 | const contextArg = dummyHandlers['notImplemented'].mock.calls.slice(-1)[0][0];
556 | expect(contextArg.request).toMatchObject({ method: 'delete', path: '/pets/1', params: { id: '1' }, headers });
557 | expect(contextArg.operation.operationId).toEqual('deletePetById');
558 | expect(contextArg.validation.errors).toBeFalsy();
559 | });
560 |
561 | test('handles GET /pets/ with trailing slash and passes context', async () => {
562 | const res = await api.handleRequest({ method: 'GET', path: '/pets/', headers });
563 | expect(res).toEqual({ operationId: 'getPets' });
564 | expect(dummyHandlers['getPets']).toBeCalled();
565 |
566 | const contextArg = dummyHandlers['getPets'].mock.calls.slice(-1)[0][0];
567 | expect(contextArg.request).toMatchObject({ method: 'get', path: '/pets/', headers });
568 | expect(contextArg.operation.operationId).toEqual('getPets');
569 | expect(contextArg.validation.errors).toBeFalsy();
570 | });
571 |
572 | test('handles GET /pets/?limit=10 with query string and passes context', async () => {
573 | const res = await api.handleRequest({ method: 'GET', path: '/pets/?limit=10', headers });
574 | expect(res).toEqual({ operationId: 'getPets' });
575 | expect(dummyHandlers['getPets']).toBeCalled();
576 |
577 | const contextArg = dummyHandlers['getPets'].mock.calls.slice(-1)[0][0];
578 | expect(contextArg.request).toMatchObject({ method: 'get', path: '/pets/', query: { limit: '10' }, headers });
579 | expect(contextArg.operation.operationId).toEqual('getPets');
580 | expect(contextArg.validation.errors).toBeFalsy();
581 | });
582 | });
583 |
584 | describe('.handleRequest postResponseHandler', () => {
585 | const dummyHandlers: { [operationId: string]: jest.Mock } = {};
586 | const dummyHandler = (operationId: string) => (dummyHandlers[operationId] = jest.fn(() => ({ operationId })));
587 | const api = new OpenAPIBackend({
588 | definition,
589 | handlers: {
590 | apiRoot: dummyHandler('apiRoot'),
591 | getPets: dummyHandler('getPets'),
592 | getPetById: dummyHandler('getPetById'),
593 | createPet: dummyHandler('createPet'),
594 | updatePetById: dummyHandler('updatePetById'),
595 | notImplemented: dummyHandler('notImplemented'),
596 | notFound: dummyHandler('notFound'),
597 | },
598 | });
599 | beforeAll(() => api.init());
600 |
601 | test('handles GET / and passes response to postResponseHandler', async () => {
602 | const postResponseHandler = jest.fn((c: Context) => c && c.response);
603 | api.register({ postResponseHandler });
604 |
605 | const res = await api.handleRequest({ method: 'GET', path: '/', headers });
606 | expect(dummyHandlers['apiRoot']).toBeCalled();
607 | expect(postResponseHandler).toBeCalled();
608 | expect(res).toEqual({ operationId: 'apiRoot' });
609 |
610 | const contextArg = postResponseHandler.mock.calls.slice(-1)[0][0] as Context;
611 | expect(contextArg.response).toMatchObject({ operationId: 'apiRoot' });
612 | expect(contextArg.request).toMatchObject({ method: 'get', path: '/', headers });
613 | });
614 |
615 | test('handles GET /pets and passes response to postResponseHandler', async () => {
616 | const postResponseHandler = jest.fn((c: Context) => c && c.response);
617 | api.register({ postResponseHandler });
618 |
619 | const res = await api.handleRequest({ method: 'GET', path: '/pets', headers });
620 | expect(dummyHandlers['getPets']).toBeCalled();
621 | expect(postResponseHandler).toBeCalled();
622 | expect(res).toEqual({ operationId: 'getPets' });
623 |
624 | const contextArg = postResponseHandler.mock.calls.slice(-1)[0][0] as Context;
625 | expect(contextArg.response).toMatchObject({ operationId: 'getPets' });
626 | expect(contextArg.request).toMatchObject({ method: 'get', path: '/pets', headers });
627 | });
628 |
629 | test('handles GET /pets and allows postResponseHandler to intercept response', async () => {
630 | const postResponseHandler = jest.fn((_ctx) => ({ you: 'have been intercepted' }));
631 | api.register({ postResponseHandler });
632 |
633 | const res = await api.handleRequest({ method: 'GET', path: '/pets', headers });
634 | expect(dummyHandlers['getPets']).toBeCalled();
635 | expect(postResponseHandler).toBeCalled();
636 | expect(res).toEqual({ you: 'have been intercepted' });
637 |
638 | const contextArg = postResponseHandler.mock.calls.slice(-1)[0][0];
639 | expect(contextArg.response).toMatchObject({ operationId: 'getPets' });
640 | expect(contextArg.request).toMatchObject({ method: 'get', path: '/pets', headers });
641 | });
642 | });
643 | });
644 |
--------------------------------------------------------------------------------
/src/router.ts:
--------------------------------------------------------------------------------
1 | import * as _ from 'lodash';
2 | import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
3 | import bath from 'bath-es5';
4 | import * as cookie from 'cookie';
5 | import { parse as parseQuery } from 'qs';
6 | import { Parameters } from 'bath-es5/_/types';
7 | import { PickVersionElement } from './backend';
8 |
9 | // alias Document to OpenAPIV3_1.Document
10 | type Document = OpenAPIV3_1.Document | OpenAPIV3.Document;
11 |
12 | /**
13 | * OperationObject
14 | * @typedef {(OpenAPIV3_1.OperationObject | OpenAPIV3.OperationObject)} OperationObject
15 | */
16 |
17 | /**
18 | * OAS Operation Object containing the path and method so it can be placed in a flat array of operations
19 | *
20 | * @export
21 | * @interface Operation
22 | * @extends {OperationObject}
23 | */
24 | export type Operation = PickVersionElement<
25 | D,
26 | OpenAPIV3.OperationObject,
27 | OpenAPIV3_1.OperationObject
28 | > & {
29 | path: string;
30 | method: string;
31 | };
32 |
33 | /* eslint-disable @typescript-eslint/no-explicit-any */
34 | export type AnyRequestBody = any;
35 | export type UnknownParams = any;
36 | /* eslint-enable @typescript-eslint/no-explicit-any */
37 |
38 | export interface Request {
39 | method: string;
40 | path: string;
41 | headers: {
42 | [key: string]: string | string[];
43 | };
44 | query?:
45 | | {
46 | [key: string]: string | string[];
47 | }
48 | | string;
49 | body?: AnyRequestBody;
50 | params?: {
51 | [key: string]: string;
52 | };
53 | }
54 |
55 | export interface ParsedRequest<
56 | RequestBody = AnyRequestBody,
57 | Params = UnknownParams,
58 | Query = UnknownParams,
59 | Headers = UnknownParams,
60 | Cookies = UnknownParams,
61 | > {
62 | method: string;
63 | path: string;
64 | requestBody: RequestBody;
65 | params: Params;
66 | query: Query;
67 | headers: Headers;
68 | cookies: Cookies;
69 | body?: AnyRequestBody;
70 | }
71 |
72 | /**
73 | * Class that handles routing
74 | *
75 | * @export
76 | * @class OpenAPIRouter
77 | */
78 | export class OpenAPIRouter {
79 | public definition: D;
80 | public apiRoot: string;
81 |
82 | private ignoreTrailingSlashes: boolean;
83 |
84 | /**
85 | * Creates an instance of OpenAPIRouter
86 | *
87 | * @param opts - constructor options
88 | * @param {D} opts.definition - the OpenAPI definition, file path or Document object
89 | * @param {string} opts.apiRoot - the root URI of the api. all paths are matched relative to apiRoot
90 | * @memberof OpenAPIRouter
91 | */
92 | constructor(opts: { definition: D; apiRoot?: string; ignoreTrailingSlashes?: boolean }) {
93 | this.definition = opts.definition;
94 | this.apiRoot = opts.apiRoot || '/';
95 | this.ignoreTrailingSlashes = opts.ignoreTrailingSlashes ?? true;
96 | }
97 |
98 | /**
99 | * Matches a request to an API operation (router)
100 | *
101 | * @param {Request} req
102 | * @param {boolean} [strict] strict mode, throw error if operation is not found
103 | * @returns {Operation}
104 | * @memberof OpenAPIRouter
105 | */
106 | public matchOperation(req: Request): Operation | undefined;
107 | public matchOperation(req: Request, strict: boolean): Operation;
108 | public matchOperation(req: Request, strict?: boolean) {
109 | // normalize request for matching
110 | req = this.normalizeRequest(req);
111 |
112 | // if request doesn't match apiRoot, throw 404
113 | if (!req.path.startsWith(this.apiRoot)) {
114 | if (strict) {
115 | throw Error('404-notFound: no route matches request');
116 | } else {
117 | return undefined;
118 | }
119 | }
120 |
121 | // get relative path
122 | const normalizedPath = this.normalizePath(req.path);
123 |
124 | // get all operations matching exact path
125 | const exactPathMatches = this.getOperations().filter(({ path }) => path === normalizedPath);
126 |
127 | // check if there's one with correct method and return if found
128 | const exactMatch = exactPathMatches.find(({ method }) => method === req.method);
129 | if (exactMatch) {
130 | return exactMatch;
131 | }
132 |
133 | // check with path templates
134 | const templatePathMatches = this.getOperations().filter(({ path }) => {
135 | // convert openapi path template to a regex pattern i.e. /{id}/ becomes /[^/]+/
136 | const pathPattern = `^${path.replace(/\{.*?\}/g, '[^/]+')}$`;
137 | return Boolean(normalizedPath.match(new RegExp(pathPattern, 'g')));
138 | });
139 |
140 | // if no operations match the path, throw 404
141 | if (!templatePathMatches.length) {
142 | if (strict) {
143 | throw Error('404-notFound: no route matches request');
144 | } else {
145 | return undefined;
146 | }
147 | }
148 |
149 | // find matching operation
150 | const match = _.chain(templatePathMatches)
151 | // order matches by length (specificity)
152 | .orderBy((op) => op.path.replace(RegExp(/\{.*?\}/g), '').length, 'desc')
153 | // then check if one of the matched operations matches the method
154 | .find(({ method }) => method === req.method)
155 | .value();
156 |
157 | if (!match) {
158 | if (strict) {
159 | throw Error('405-methodNotAllowed: this method is not registered for the route');
160 | } else {
161 | return undefined;
162 | }
163 | }
164 |
165 | return match;
166 | }
167 |
168 | /**
169 | * Flattens operations into a simple array of Operation objects easy to work with
170 | *
171 | * @returns {Operation[]}
172 | * @memberof OpenAPIRouter
173 | */
174 | public getOperations(): Operation[] {
175 | const paths = this.definition?.paths || {};
176 | return _.chain(paths)
177 | .entries()
178 | .flatMap(([path, pathBaseObject]) => {
179 | const methods = _.pick(pathBaseObject, ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']);
180 | return _.entries(methods).map(([method, operation]) => {
181 | const op = operation as Operation;
182 | return {
183 | ...op,
184 | path,
185 | method,
186 | // append the path base object's parameters to the operation's parameters
187 | parameters: [
188 | ...((op.parameters as PickVersionElement[]) ||
189 | []),
190 | ...((pathBaseObject?.parameters as PickVersionElement<
191 | D,
192 | OpenAPIV3.ParameterObject,
193 | OpenAPIV3_1.ParameterObject
194 | >[]) || []), // path base object parameters
195 | ],
196 | // operation-specific security requirement override global requirements
197 | security: op.security || this.definition.security || [],
198 | };
199 | });
200 | })
201 | .value();
202 | }
203 |
204 | /**
205 | * Gets a single operation based on operationId
206 | *
207 | * @param {string} operationId
208 | * @returns {Operation}
209 | * @memberof OpenAPIRouter
210 | */
211 | public getOperation(operationId: string): Operation | undefined {
212 | return this.getOperations().find((op) => op.operationId === operationId);
213 | }
214 |
215 | /**
216 | * Normalises request:
217 | * - http method to lowercase
218 | * - remove path leading slash
219 | * - remove path query string
220 | *
221 | * @export
222 | * @param {Request} req
223 | * @returns {Request}
224 | */
225 | public normalizeRequest(req: Request): Request {
226 | let path = req.path?.trim() || '';
227 |
228 | // add leading prefix to path
229 | if (!path.startsWith('/')) {
230 | path = `/${path}`;
231 | }
232 |
233 | // remove query string from path
234 | path = path.split('?')[0];
235 |
236 | // normalize method to lowercase
237 | const method = req.method.trim().toLowerCase();
238 |
239 | return { ...req, path, method };
240 | }
241 |
242 | /**
243 | * Normalises path for matching: strips apiRoot prefix from the path
244 | *
245 | * Also depending on configuration, will remove trailing slashes
246 | *
247 | * @export
248 | * @param {string} path
249 | * @returns {string}
250 | */
251 | public normalizePath(pathInput: string) {
252 | let path = pathInput.trim();
253 |
254 | // strip apiRoot from path
255 | if (path.startsWith(this.apiRoot)) {
256 | path = path.replace(new RegExp(`^${this.apiRoot}/?`), '/');
257 | }
258 |
259 | // remove trailing slashes from path if ignoreTrailingSlashes = true
260 | while (this.ignoreTrailingSlashes && path.length > 1 && path.endsWith('/')) {
261 | path = path.substr(0, path.length - 1);
262 | }
263 |
264 | return path;
265 | }
266 |
267 | /**
268 | * Parses and normalizes a request
269 | * - parse json body
270 | * - parse query string
271 | * - parse cookies from headers
272 | * - parse path params based on uri template
273 | *
274 | * @export
275 | * @param {Request} req
276 | * @param {Operation} [operation]
277 | * @param {string} [patbh]
278 | * @returns {ParsedRequest}
279 | */
280 | public parseRequest(req: Request, operation?: Operation): ParsedRequest {
281 | let requestBody = req.body;
282 | if (req.body && typeof req.body !== 'object') {
283 | try {
284 | // attempt to parse json
285 | requestBody = JSON.parse(req.body.toString());
286 | } catch {
287 | // suppress json parsing errors
288 | // we will emit error if validation requires it later
289 | }
290 | }
291 |
292 | // header keys are converted to lowercase, so Content-Type becomes content-type
293 | const headers = _.mapKeys(req.headers, (val, header) => header.toLowerCase());
294 |
295 | // parse cookie from headers
296 | const cookieHeader = headers['cookie'];
297 | const cookies = cookie.parse(_.flatten([cookieHeader]).join('; '));
298 |
299 | // parse query
300 | const queryString = typeof req.query === 'string' ? req.query.replace('?', '') : req.path.split('?')[1];
301 | const query = typeof req.query === 'object' ? _.cloneDeep(req.query) : parseQuery(queryString);
302 |
303 | // normalize
304 | req = this.normalizeRequest(req);
305 |
306 | let params: Parameters = {};
307 | if (operation) {
308 | // get relative path
309 | const normalizedPath = this.normalizePath(req.path);
310 |
311 | // parse path params if path is given
312 | const pathParams = bath(operation.path);
313 | params = pathParams.params(normalizedPath) || {};
314 | // parse query parameters with specified style for parameter
315 | for (const queryParam in query) {
316 | if (query[queryParam]) {
317 | const parameter = operation.parameters?.find(
318 | (param) => !('$ref' in param) && param?.in === 'query' && param?.name === queryParam,
319 | ) as PickVersionElement;
320 |
321 | if (parameter) {
322 | if (parameter.content && parameter.content['application/json']) {
323 | query[queryParam] = JSON.parse(query[queryParam]);
324 | } else if (parameter.explode === false && queryString) {
325 | let commaQueryString = queryString.replace(/%2C/g, ',');
326 | if (parameter.style === 'spaceDelimited') {
327 | commaQueryString = commaQueryString.replace(/ /g, ',').replace(/%20/g, ',');
328 | }
329 | if (parameter.style === 'pipeDelimited') {
330 | commaQueryString = commaQueryString.replace(/\|/g, ',').replace(/%7C/g, ',');
331 | }
332 | // use comma parsing e.g. &a=1,2,3
333 | const commaParsed = parseQuery(commaQueryString, { comma: true });
334 | query[queryParam] = commaParsed[queryParam];
335 | } else if (parameter.explode === false) {
336 | let decoded = query[queryParam].replace(/%2C/g, ',');
337 | if (parameter.style === 'spaceDelimited') {
338 | decoded = decoded.replace(/ /g, ',').replace(/%20/g, ',');
339 | }
340 | if (parameter.style === 'pipeDelimited') {
341 | decoded = decoded.replace(/\|/g, ',').replace(/%7C/g, ',');
342 | }
343 | query[queryParam] = decoded.split(',');
344 | }
345 | }
346 | }
347 | }
348 | }
349 |
350 | return {
351 | ...req,
352 | params,
353 | headers,
354 | query,
355 | cookies,
356 | requestBody,
357 | };
358 | }
359 | }
360 |
--------------------------------------------------------------------------------
/src/types/swagger-parser.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'swagger-parser' {
2 | type Document = import('openapi-types').OpenAPIV3_1.Document;
3 | interface Options {
4 | allow?: {
5 | json?: boolean;
6 | yaml?: boolean;
7 | empty?: boolean;
8 | unknown?: boolean;
9 | };
10 | $ref?: {
11 | internal?: boolean;
12 | external?: boolean;
13 | circular?: boolean | 'ignore';
14 | };
15 | validate?: {
16 | schema?: boolean;
17 | spec?: boolean;
18 | };
19 | cache?: {
20 | fs?: number;
21 | http?: number;
22 | https?: number;
23 | };
24 | }
25 | function parse(api: string | Document, options?: Options): Promise;
26 | function validate(api: string | Document, options?: Options): Promise;
27 | function dereference(api: string | Document, options?: Options): Promise;
28 | function dereference(basePath: string, api: string | Document, options?: Options): Promise;
29 | function bundle(api: string | Document, options?: Options): Promise;
30 | }
31 |
--------------------------------------------------------------------------------
/src/utils.test.ts:
--------------------------------------------------------------------------------
1 | import OpenAPIUtils from './utils';
2 |
3 | describe('OpenAPIUtils', () => {
4 | describe('.findStatusCodeMatch', () => {
5 | test('mismatches', async () => {
6 | const value = OpenAPIUtils.findStatusCodeMatch(302, {
7 | '200': 'OK',
8 | '201': 'Created',
9 | });
10 | expect(value).toEqual(undefined);
11 | });
12 |
13 | test('matches 200', async () => {
14 | const value = OpenAPIUtils.findStatusCodeMatch(200, {
15 | '200': 'OK',
16 | '201': 'Created',
17 | });
18 | expect(value).toEqual('OK');
19 | });
20 |
21 | test('matches 201', async () => {
22 | const value = OpenAPIUtils.findStatusCodeMatch(201, {
23 | '200': 'OK',
24 | '201': 'Created',
25 | });
26 | expect(value).toEqual('Created');
27 | });
28 |
29 | test('matches 404', async () => {
30 | const value = OpenAPIUtils.findStatusCodeMatch(404, {
31 | '200': 'OK',
32 | '404': 'Not Found',
33 | '201': 'Created',
34 | });
35 | expect(value).toEqual('Not Found');
36 | });
37 |
38 | test('matches 500 (not string)', async () => {
39 | const value = OpenAPIUtils.findStatusCodeMatch(500, {
40 | '200': 'OK',
41 | '500': ['a', { test: 'it works' }, 'b'],
42 | '201': 'Created',
43 | });
44 | expect(value[1].test).toEqual('it works');
45 | });
46 |
47 | test('matches 500 (null)', async () => {
48 | const value = OpenAPIUtils.findStatusCodeMatch(500, {
49 | '200': 'OK',
50 | '500': null,
51 | '201': 'Created',
52 | });
53 | expect(value).toEqual(null);
54 | });
55 |
56 | test('matches 500 (undefined)', async () => {
57 | const value = OpenAPIUtils.findStatusCodeMatch(500, {
58 | '200': 'OK',
59 | '500': undefined,
60 | '201': 'Created',
61 | });
62 | expect(value).toEqual(undefined);
63 | });
64 |
65 | test('matches 400 (when pattern present)', async () => {
66 | const value = OpenAPIUtils.findStatusCodeMatch(400, {
67 | '200': 'OK',
68 | '401': 'Unauthorized',
69 | '4XX': 'Error (patterned)',
70 | '400': 'Bad Request',
71 | });
72 | expect(value).toEqual('Bad Request');
73 | });
74 |
75 | test('matches 401 (when pattern present)', async () => {
76 | const value = OpenAPIUtils.findStatusCodeMatch(401, {
77 | '200': 'OK',
78 | '401': 'Unauthorized',
79 | '4XX': 'Error (patterned)',
80 | '400': 'Bad Request',
81 | });
82 | expect(value).toEqual('Unauthorized');
83 | });
84 |
85 | test('matches 403 (via pattern)', async () => {
86 | const value = OpenAPIUtils.findStatusCodeMatch(403, {
87 | '200': 'OK',
88 | '401': 'Unauthorized',
89 | '4XX': 'Error (patterned)',
90 | '400': 'Bad Request',
91 | });
92 | expect(value).toEqual('Error (patterned)');
93 | });
94 |
95 | test('not matches default (on pattern when default present)', async () => {
96 | const value = OpenAPIUtils.findStatusCodeMatch(402, {
97 | '200': 'OK',
98 | default: 'Default value',
99 | '401': 'Unauthorized',
100 | '4XX': 'Error (patterned)',
101 | '400': 'Bad Request',
102 | });
103 | expect(value).toEqual('Error (patterned)');
104 | });
105 |
106 | test('matches default', async () => {
107 | const value = OpenAPIUtils.findStatusCodeMatch(500, {
108 | '200': 'OK',
109 | default: 'Default value',
110 | '401': 'Unauthorized',
111 | '4XX': 'Error (patterned)',
112 | '400': 'Bad Request',
113 | });
114 | expect(value).toEqual('Default value');
115 | });
116 |
117 | test('wrong pattern fallback', async () => {
118 | // not even sure this is a relevent test, but let's make sure this doesn't return '200-299 codes'
119 | const value = OpenAPIUtils.findStatusCodeMatch(23456, {
120 | '1XX': '100-199 codes',
121 | '2XX': '200-299 codes',
122 | '3XX': '300-399 codes',
123 | });
124 | expect(value).toEqual(undefined);
125 | });
126 | });
127 |
128 | describe('.findDefaultStatusCodeMatch', () => {
129 | test('matches 200', async () => {
130 | const value = OpenAPIUtils.findDefaultStatusCodeMatch({
131 | '200': '200',
132 | '201': '201',
133 | });
134 | expect(value.res).toEqual('200');
135 | });
136 |
137 | test('matches 201', async () => {
138 | const value = OpenAPIUtils.findDefaultStatusCodeMatch({
139 | '201': '201',
140 | '300': '300',
141 | });
142 | expect(value.res).toEqual('201');
143 | });
144 |
145 | test('matches 201 with 2XX fallback', async () => {
146 | const value = OpenAPIUtils.findDefaultStatusCodeMatch({
147 | '201': '201',
148 | '2XX': '2XX',
149 | '300': '300',
150 | });
151 | expect(value.res).toEqual('201');
152 | });
153 |
154 | test('matches 201 with default fallback', async () => {
155 | const value = OpenAPIUtils.findDefaultStatusCodeMatch({
156 | default: 'default',
157 | '201': '201',
158 | '2XX': '2XX',
159 | '300': '300',
160 | });
161 | expect(value.res).toEqual('201');
162 | });
163 |
164 | test('matches 2XX', async () => {
165 | const value = OpenAPIUtils.findDefaultStatusCodeMatch({
166 | '2XX': '2XX',
167 | '300': '300',
168 | });
169 | expect(value.res).toEqual('2XX');
170 | });
171 |
172 | test('matches 2XX with default fallback', async () => {
173 | const value = OpenAPIUtils.findDefaultStatusCodeMatch({
174 | '2XX': '2XX',
175 | default: 'default',
176 | });
177 | expect(value.res).toEqual('2XX');
178 | });
179 |
180 | test('matches first one', async () => {
181 | const value = OpenAPIUtils.findDefaultStatusCodeMatch({
182 | '305': '305',
183 | '3XX': '3XX',
184 | });
185 | expect(value.res).toEqual('305');
186 | });
187 | });
188 | });
189 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | // library code, any is fine
2 | /* eslint-disable @typescript-eslint/no-explicit-any */
3 |
4 | import * as _ from 'lodash';
5 | import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
6 | import { Operation } from './router';
7 |
8 | // alias Document to OpenAPIV3_1.Document
9 | type Document = OpenAPIV3_1.Document | OpenAPIV3.Document;
10 |
11 | export default class OpenAPIUtils {
12 | /**
13 | * Finds the value for a given key (status code) in an object,
14 | * based on the OpenAPI specification for patterned field.
15 | * Returns the value in the 'obj' argument for which the key matches the 'statusCode' argument,
16 | * based on pattern matching, or undefined otherwise.
17 | * @param {number} statusCode The status code representing the key to match in 'obj' argument.
18 | * @param {Object.} obj The object containing values referenced by possibly patterned status code key.
19 | * @returns {*}
20 | */
21 | public static findStatusCodeMatch(statusCode: number, obj: { [patternedStatusCode: string]: any }): any {
22 | let value: any = obj[statusCode];
23 |
24 | if (value !== undefined) {
25 | return value;
26 | }
27 |
28 | // The specification allows statusCode to be 1XX, 2XX, ...
29 | const strStatusCode = Math.floor(statusCode / 100) + 'XX';
30 |
31 | value = obj[strStatusCode];
32 |
33 | if (value !== undefined) {
34 | return value;
35 | }
36 |
37 | return obj['default'];
38 | }
39 |
40 | /**
41 | * Finds the default most appropriate value in an object, based on the following rule
42 | * 1. check for a 20X res
43 | * 2. check for a 2XX res
44 | * 3. check for the "default" res
45 | * 4. pick first res code in list
46 | * Returns the value in the 'obj' argument.
47 | * @param {Object.} obj The object containing values referenced by possibly patterned status code key.
48 | * @returns {{status: string, res: *}}
49 | */
50 | public static findDefaultStatusCodeMatch(obj: { [patternedStatusCode: string]: any }): { status: number; res: any } {
51 | // 1. check for a 20X response
52 | for (const ok of _.range(200, 204)) {
53 | if (obj[ok]) {
54 | return {
55 | status: ok,
56 | res: obj[ok],
57 | };
58 | }
59 | }
60 |
61 | // 2. check for a 2XX response
62 | if (obj['2XX']) {
63 | return {
64 | status: 200,
65 | res: obj['2XX'],
66 | };
67 | }
68 |
69 | // 3. check for the "default" response
70 | if (obj.default) {
71 | return {
72 | status: 200,
73 | res: obj.default,
74 | };
75 | }
76 |
77 | // 4. pick first response code in list
78 | const code = Object.keys(obj)[0];
79 | return {
80 | status: Number(code),
81 | res: obj[code],
82 | };
83 | }
84 |
85 | /**
86 | * Get operationId, (or generate one) for an operation
87 | *
88 | * @static
89 | * @param {Operation} operation
90 | * @returns {string} OperationId of the given operation
91 | * @memberof OpenAPIUtils
92 | */
93 | public static getOperationId(operation: Operation): string {
94 | if (!operation?.operationId) {
95 | // TODO: generate a default substitute for operationId
96 | return `unknown`;
97 | }
98 | return operation.operationId;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": false,
4 | "target": "es2019",
5 | "module": "commonjs",
6 | "moduleResolution": "node",
7 | "lib": ["es2019"],
8 | "esModuleInterop": true,
9 | "experimentalDecorators": true,
10 | "emitDecoratorMetadata": true,
11 | "noImplicitAny": false,
12 | "strictPropertyInitialization": false,
13 | "skipLibCheck": true,
14 | "baseUrl": ".",
15 | "rootDir": "src/",
16 | "outDir": "",
17 | "sourceMap": true,
18 | "declaration": true,
19 | "paths": {
20 | "*": [
21 | "node_modules/*",
22 | "src/types/*"
23 | ]
24 | }
25 | },
26 | "include": [
27 | "src/**/*"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------