├── .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 |

openapi-backend

4 | 5 | [![CI](https://github.com/openapistack/openapi-backend/workflows/CI/badge.svg)](https://github.com/openapistack/openapi-backend/actions?query=workflow%3ACI) 6 | [![License](http://img.shields.io/:license-mit-blue.svg)](https://github.com/openapistack/openapi-backend/blob/main/LICENSE) 7 | [![npm version](https://img.shields.io/npm/v/openapi-backend.svg)](https://www.npmjs.com/package/openapi-backend) 8 | [![npm downloads](https://img.shields.io/npm/dw/openapi-backend.svg)](https://www.npmjs.com/package/openapi-backend) 9 | [![Libraries.io dependency status for latest release](https://img.shields.io/librariesio/release/npm/openapi-backend.svg)](https://www.npmjs.com/package/openapi-backend?activeTab=dependencies) 10 | ![npm type definitions](https://img.shields.io/npm/types/openapi-backend.svg) 11 | [![Buy me a coffee](https://img.shields.io/badge/donate-buy%20me%20a%20coffee-orange)](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 | [![License](http://img.shields.io/:license-mit-blue.svg)](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 | [![License](http://img.shields.io/:license-mit-blue.svg)](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 | [![License](http://img.shields.io/:license-mit-blue.svg)](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 | [![License](http://img.shields.io/:license-mit-blue.svg)](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 | [![License](http://img.shields.io/:license-mit-blue.svg)](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 | [![License](http://img.shields.io/:license-mit-blue.svg)](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 | [![License](http://img.shields.io/:license-mit-blue.svg)](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 | [![License](http://img.shields.io/:license-mit-blue.svg)](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 | [![License](http://img.shields.io/:license-mit-blue.svg)](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 | [![License](http://img.shields.io/:license-mit-blue.svg)](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 | [![License](http://img.shields.io/:license-mit-blue.svg)](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 | [![License](http://img.shields.io/:license-mit-blue.svg)](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 | [![License](http://img.shields.io/:license-mit-blue.svg)](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 | --------------------------------------------------------------------------------