Repository: openapistack/openapi-client-axios Branch: main Commit: 9ec1b7060333 Files: 24 Total size: 107.9 KB Directory structure: gitextract_dvpnw2jp/ ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── ci.yml │ └── codeql.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DOCS.md ├── LICENSE ├── README.md ├── jest.config.ts ├── package.json ├── src/ │ ├── __tests__/ │ │ ├── fixtures.ts │ │ └── resources/ │ │ ├── example-pet-api.openapi.json │ │ └── example-pet-api.openapi.yml │ ├── client.test.ts │ ├── client.ts │ ├── index.ts │ ├── query-serializer.ts │ └── types/ │ └── client.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # http://editorconfig.org root = true [*] indent_style = space indent_size = 2 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false ================================================ FILE: .github/FUNDING.yml ================================================ github: anttiviljami open_collective: openapi-stack custom: - https://buymeacoff.ee/anttiviljami ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: ["main"] tags: ["*"] pull_request: branches: ["main"] permissions: id-token: write # Required for OIDC contents: read jobs: test: name: Test strategy: matrix: axios_version: - 0.25.0 # oldest supported - 0.*.* # latest 0.x - ^1.0.0 # latest 1.x - latest runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v6 with: node-version: "20" - run: npm ci - run: npm install axios@${{ matrix.axios_version }} && npm why axios - run: npm run lint - run: npm test publish: name: Publish runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') needs: - test steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v6 with: node-version: "24" registry-url: https://registry.npmjs.org/ - run: npm ci - run: npm publish ================================================ FILE: .github/workflows/codeql.yml ================================================ name: "CodeQL" on: push: branches: ["main"] pull_request: branches: ["main"] schedule: - cron: "26 21 * * 5" jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [javascript] steps: - name: Checkout uses: actions/checkout@v3 - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} queries: +security-and-quality - name: Autobuild uses: github/codeql-action/autobuild@v2 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 with: category: "/language:${{ matrix.language }}" ================================================ FILE: .gitignore ================================================ # build *.js *.js.map *.d.ts # include types !src/types/*.d.ts # include jest config !jest.config.js # npm node_modules npm_debug.log* ================================================ FILE: .husky/pre-commit ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npm run lint npm test ================================================ FILE: .prettierignore ================================================ *.d.ts *.js ================================================ FILE: .prettierrc ================================================ { "parser": "typescript", "arrowParens": "always", "trailingComma": "all", "singleQuote": true, "printWidth": 120 } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at viljami@viljami.io. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing OpenAPI Client Axios is Free and Open Source Software. Issues and pull requests are more than welcome! ================================================ FILE: DOCS.md ================================================ # Documentation OpenAPI Client Axios documentation has moved to [openapistack.co](https://openapistack.co) See: https://openapistack.co/docs/openapi-client-axios ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2021 Viljami Kuosmanen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

openapi-client-axios

[![CI](https://github.com/openapistack/openapi-client-axios/workflows/CI/badge.svg)](https://github.com/openapistack/openapi-client-axios/actions?query=workflow%3ACI) [![License](http://img.shields.io/:license-mit-blue.svg)](https://github.com/openapistack/openapi-client-axios/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/openapi-client-axios.svg)](https://www.npmjs.com/package/openapi-client-axios) [![npm downloads](https://img.shields.io/npm/dw/openapi-client-axios.svg)](https://www.npmjs.com/package/openapi-client-axios) [![bundle size](https://img.shields.io/bundlephobia/minzip/openapi-client-axios.svg?label=gzip%20bundle)](https://bundlephobia.com/package/openapi-client-axios) [![Libraries.io dependency status for latest release](https://img.shields.io/librariesio/release/npm/openapi-client-axios.svg)](https://www.npmjs.com/package/openapi-client-axios?activeTab=dependencies) ![npm type definitions](https://img.shields.io/npm/types/openapi-client-axios.svg) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/openapistack/openapi-client-axios) [![Buy me a coffee](https://img.shields.io/badge/donate-buy%20me%20a%20coffee-orange)](https://buymeacoff.ee/anttiviljami)

JavaScript client library for consuming OpenAPI-enabled APIs with axios. Types included.

## Features - [x] Create API clients from [OpenAPI v3 definitions](https://github.com/OAI/OpenAPI-Specification) - [x] Client is configured in runtime. **No generated code!** - [x] Generate TypeScript definitions (.d.ts) for your APIs with full IntelliSense support - [x] Easy to use API to call API operations using JavaScript methods - `client.getPet(1)` - `client.searchPets()` - `client.searchPets({ ids: [1, 2, 3] })` - `client.updatePet(1, payload)` - [x] Built on top of the robust [axios](https://github.com/axios/axios) JavaScript library - [x] Isomorphic, works both in browser and Node.js ## Documentation **New!** OpenAPI Client Axios documentation is now found on [openapistack.co](https://openapistack.co) https://openapistack.co/docs/openapi-client-axios/intro ## Quick Start ``` npm install --save axios openapi-client-axios ``` ``` yarn add axios openapi-client-axios ``` With promises / CommonJS syntax: ```javascript const OpenAPIClientAxios = require('openapi-client-axios').default; const api = new OpenAPIClientAxios({ definition: 'https://example.com/api/openapi.json' }); api.init() .then(client => client.getPetById(1)) .then(res => console.log('Here is pet id:1 from the api', res.data)); ``` With async-await / ES6 syntax: ```javascript import OpenAPIClientAxios from 'openapi-client-axios'; const api = new OpenAPIClientAxios({ definition: 'https://example.com/api/openapi.json' }); api.init(); async function createPet() { const client = await api.getClient(); const res = await client.createPet(null, { name: 'Garfield' }); console.log('Pet created', res.data); } ``` ## Client OpenAPI Client Axios uses [operationIds](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#operation-object) in OpenAPIv3 definitions to call API operations. After initializing `OpenAPIClientAxios`, an axios client instance extended with OpenAPI capabilities is exposed. Example: ```javascript const api = new OpenAPIClientAxios({ definition: 'https://example.com/api/openapi.json' }); api.init().then((client) => { client.updatePet(1, { age: 12 }); }); ``` `client` is an [axios instance](https://github.com/axios/axios#creating-an-instance) initialized with baseURL from OpenAPI definitions and extended with extra operation methods for calling API operations. It also has a reference to OpenAPIClientAxios at `client.api` ## Operation methods OpenAPIClientAxios operation methods take 3 arguments: ```javascript client.operationId(parameters?, data?, config?) ``` ### Parameters The first argument is used to pass parameters available for the operation. ```javascript // GET /pets/{petId} client.getPet({ petId: 1 }) ``` For syntactic sugar purposes, you can also specify a single implicit parameter value, in which case OpenAPIClientAxios will look for the first required parameter for the operation. Usually this is will be a path parameter. ```javascript // GET /pets/{petId} - getPet client.getPet(1) ``` Alternatively, you can explicitly specify parameters in array form. This method allows you to set custom parameters not defined in the OpenAPI spec. ```javascript // GET /pets?search=Garfield - searchPets client.searchPets([{ name: 'search', value: 'Garfield', in: 'query' }]) ``` The type of the parameters can be any of: - query - header - path - cookie ### Data The second argument is used to pass the requestPayload ```javascript // PUT /pets/1 - updatePet client.updatePet(1, { name: 'Odie' }) ``` More complex payloads, such as Node.js streams or FormData supported by Axios can be used. The first argument can be set to null if there are no parameters required for the operation. ```javascript // POST /pets - createPet client.updatePet(null, { name: 'Garfield' }) ``` ### Config object The last argument is the config object. The config object is an [`AxiosRequestConfig`](https://github.com/axios/axios#request-config) object. You can use it to override axios request config parameters, such as `headers`, `timeout`, `withCredentials` and many more. ```javascript // POST /user - createUser client.createUser(null, { user: 'admin', pass: '123' }, { headers: { 'x-api-key': 'secret' } }); ``` ## Paths Dictionary OpenAPI Client Axios also allows calling API operations via their path and HTTP method, using the paths dictionary. Example: ```javascript client.paths['/pets'].get(); // GET /pets, same as calling client.getPets() client.paths['/pets'].post(); // POST /pets client.paths['/pets/{petId}'].put(1); // PUT /pets/1 client.paths['/pets/{petId}/owner/{ownerId}'].get({ petId: 1, ownerId: 2 }) ; // GET /pets/1/owner/2 ``` This allows calling operation methods without using their operationIds, which may be sometimes preferred. ## Typesafe Clients ![TypeScript IntelliSense](https://openapistack.co/assets/images/intellisense-b61ace10fd35746dd5bfefa977c0645e.gif) `openapi-client-axios` comes with a CLI command `openapicmd typegen` to generate Typescript types for type safety and code autocomplete. ``` npx openapicmd typegen ./openapi.yaml > src/types/openapi.d.ts ``` The output of `typegen` exports a type called `Client`, which can be used for instances created with `OpenAPIClientAxios`. Both the `api.getClient()` and `api.init()` methods support passing in a Client type. ```typescript import { Client as PetStoreClient } from './client.d.ts'; const client = await api.init(); const client = await api.getClient(); ``` `openapicmd typegen` supports using both local and remote URLs for OpenAPI definition files. ``` $ npx openapicmd typegen ./petstore.yaml $ npx openapicmd typegen https://petstore3.swagger.io/api/v3/openapi.json ``` ## Commercial support For assistance with openapi-client-axios in your company, reach out at support@openapistack.co. ## Contributing OpenAPI Client Axios is Free and Open Source Software. Issues and pull requests are more than welcome! ================================================ FILE: jest.config.ts ================================================ import type { JestConfigWithTsJest } from 'ts-jest' const jestConfig: JestConfigWithTsJest = { testEnvironment: 'node', testMatch: ['**/?(*.)+(spec|test).ts?(x)'], transform: { '^.+\\.[tj]s?$': [ 'ts-jest', { useESM: true, }, ], }, } export default jestConfig ================================================ FILE: package.json ================================================ { "name": "openapi-client-axios", "description": "JavaScript client library for consuming OpenAPI-enabled APIs with axios. Types included.", "version": "7.9.0", "license": "MIT", "keywords": [ "openapi", "swagger", "client", "axios", "frontend", "browser", "mock", "typescript" ], "author": "Viljami Kuosmanen ", "funding": "https://github.com/sponsors/anttiviljami", "homepage": "https://openapistack.co", "repository": { "type": "git", "url": "git+https://github.com/openapistack/openapi-client-axios.git" }, "bugs": { "url": "https://github.com/openapistack/openapi-client-axios/issues" }, "main": "index.js", "types": "index.d.ts", "files": [ "*.js", "*.d.ts", "*.map", "**/*.js", "**/*.d.ts", "**/*.map", "!*.test.*", "!**/*.test.*", "!__tests__/**", "!src/**", "!*.config.js" ], "peerDependencies": { "axios": ">=0.25.0", "js-yaml": "^4.1.0" }, "dependencies": { "bath-es5": "^3.0.3", "dereference-json-schema": "^0.2.1", "openapi-types": "^12.1.3" }, "devDependencies": { "@types/jest": "^29.5.5", "@types/js-yaml": "^4.0.5", "@types/json-schema": "^7.0.6", "axios": "^1.13.5", "axios-mock-adapter": "^1.22.0", "jest": "^29.7.0", "json-schema": "^0.4.0", "json-schema-deref-sync": "^0.14.0", "msw": "^1.3.2", "prettier": "^3.0.3", "source-map-support": "^0.5.10", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "typescript": "^4.5.5" }, "scripts": { "build": "tsc", "watch-build": "tsc -w", "prepare": "npm run build", "test": "NODE_ENV=test jest", "lint": "prettier --check src/**/*.ts __tests__/**/*.ts", "lint:fix": "prettier --write src/**/*.ts __tests__/**/*.ts" }, "gitHead": "6ea364b653a2264ddd2de4d2f1ddabc5cf3cbfd5" } ================================================ FILE: src/__tests__/fixtures.ts ================================================ import { OpenAPIV3 } from 'openapi-types'; export const baseURL = 'http://localhost:8080'; export const baseURLAlternative = 'http://localhost:9090/'; export const baseURLWithVariable = 'http://{foo1}.localhost:9090/{foo2}/{foo3}/'; export const baseURLWithVariableResolved = 'http://bar1.localhost:9090/bar2a/bar3b/'; export const baseURLV2 = 'http://localhost:8080/v2'; export const responses: OpenAPIV3.ResponsesObject = { 200: { description: 'ok' }, }; export const petId: OpenAPIV3.ParameterObject = { name: 'petId', in: 'path', required: true, schema: { type: 'integer', }, }; export const petShopId: OpenAPIV3.ParameterObject = { name: 'x-petshop-id', in: 'header', schema: { type: 'string', }, }; export const ownerId: OpenAPIV3.ParameterObject = { name: 'ownerId', in: 'path', required: true, schema: { type: 'integer', }, }; export const createDefinition = (overrides: Partial = {}): OpenAPIV3.Document => ({ openapi: '3.0.0', info: { title: 'api', version: '1.0.0', }, servers: [], paths: {}, ...overrides, }); export const definition: OpenAPIV3.Document = { openapi: '3.0.0', info: { title: 'api', version: '1.0.0', }, servers: [ { url: baseURL }, { url: baseURLAlternative, description: 'Alternative server', }, { url: baseURLWithVariable, description: 'server with variable baseURL', variables: { foo1: { default: 'bar1', enum: ['bar1', 'bar1a'], }, foo2: { default: 'bar2b', enum: ['bar2a', 'bar2b'], }, foo3: { default: 'bar3a', enum: ['bar3a', 'bar3b'], }, }, }, ], paths: { '/pets': { get: { operationId: 'getPets', responses: { 200: { $ref: '#/components/responses/PetsListRes', }, }, parameters: [ { name: 'q', in: 'query', schema: { type: 'array', items: { type: 'string', }, }, }, ], }, post: { operationId: 'createPet', responses: { 201: { $ref: '#/components/responses/PetRes', }, }, }, }, '/pets/{petId}': { get: { operationId: 'getPetById', responses: { 200: { $ref: '#/components/responses/PetRes', }, }, parameters: [petShopId], }, put: { operationId: 'replacePetById', responses: { 200: { $ref: '#/components/responses/PetRes', }, }, }, patch: { operationId: 'updatePetById', responses: { 200: { $ref: '#/components/responses/PetRes', }, }, }, delete: { operationId: 'deletePetById', responses: { 200: { $ref: '#/components/responses/PetRes', }, }, }, parameters: [petId], }, '/pets/{petId}/owner': { get: { operationId: 'getOwnerByPetId', responses, }, parameters: [petId], }, '/pets/{petId}/owner/{ownerId}': { get: { operationId: 'getPetOwner', responses, }, parameters: [petId, ownerId], }, '/pets/meta': { get: { operationId: 'getPetsMeta', responses, }, }, '/pets/relative': { servers: [{ url: baseURLV2 }], get: { operationId: 'getPetsRelative', responses, }, }, }, components: { schemas: { PetWithName: { type: 'object', properties: { id: { type: 'integer', minimum: 1, }, name: { type: 'string', example: 'Garfield', }, }, }, }, responses: { PetRes: { description: 'ok', content: { 'application/json': { schema: { $ref: '#/components/schemas/PetWithName', }, }, }, }, PetsListRes: { description: 'ok', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/PetWithName', }, }, }, }, }, }, }, }; ================================================ FILE: src/__tests__/resources/example-pet-api.openapi.json ================================================ { "openapi": "3.0.0", "info": { "title": "Example API", "description": "Example CRUD API for pets", "version": "1.0.0" }, "tags": [ { "name": "pets", "description": "Pet operations" } ], "servers": [ { "url": "http://localhost:8080" } ], "paths": { "/pets": { "get": { "operationId": "getPets", "summary": "List pets", "description": "Returns all pets in database", "tags": [ "pets" ], "responses": { "200": { "description": "List of pets in database" } }, "parameters": [ { "name": "limit", "in": "query", "description": "Number of items to return", "required": false, "schema": { "$ref": "#/components/schemas/QueryLimit" } }, { "name": "offset", "in": "query", "description": "Starting offset for returning items", "required": false, "schema": { "$ref": "#/components/schemas/QueryOffset" } } ] }, "post": { "operationId": "createPet", "summary": "Create a pet", "description": "Crete a new pet into the database", "tags": [ "pets" ], "responses": { "201": { "description": "Pet created succesfully" } }, "parameters": [], "requestBody": { "$ref": "#/components/requestBodies/PetPayload" } } }, "/pets/{id}": { "get": { "operationId": "getPetById", "summary": "Get a pet", "description": "Returns a pet by its id in database", "tags": [ "pets" ], "responses": { "200": { "description": "Pet object corresponding to id" }, "404": { "description": "Pet not found" } }, "parameters": [ { "name": "id", "in": "path", "description": "Unique identifier for pet in database", "required": true, "schema": { "$ref": "#/components/schemas/PetId" } }, { "name": "x-petshop-id", "in": "header", "description": "Optional header parameter to pass petshop id", "schema": { "type": "string", "example": "123" } } ] }, "put": { "operationId": "replacePetById", "summary": "Replace pet", "description": "Replace an existing pet in the database", "tags": [ "pets" ], "responses": { "200": { "description": "Pet replaced succesfully" }, "404": { "description": "Pet not found" } }, "parameters": [ { "name": "id", "in": "path", "description": "Unique identifier for pet in database", "required": true, "schema": { "$ref": "#/components/schemas/PetId" } } ], "requestBody": { "$ref": "#/components/requestBodies/PetPayload" } }, "patch": { "operationId": "updatePetById", "summary": "Update pet", "description": "Update an existing pet in the database", "tags": [ "pets" ], "responses": { "200": { "description": "Pet updated succesfully" }, "404": { "description": "Pet not found" } }, "parameters": [ { "name": "id", "in": "path", "description": "Unique identifier for pet in database", "required": true, "schema": { "$ref": "#/components/schemas/PetId" } } ], "requestBody": { "$ref": "#/components/requestBodies/PetPayload" } }, "delete": { "operationId": "deletePetById", "summary": "Delete a pet", "description": "Deletes a pet by its id in database", "tags": [ "pets" ], "responses": { "200": { "description": "Pet deleted succesfully" }, "404": { "description": "Pet not found" } }, "parameters": [ { "name": "id", "in": "path", "description": "Unique identifier for pet in database", "required": true, "schema": { "$ref": "#/components/schemas/PetId" } } ] } }, "/pets/{id}/owner": { "get": { "operationId": "getOwnerByPetId", "summary": "Get a pet's owner", "description": "Get the owner for a pet", "tags": [ "pets" ], "responses": { "200": { "description": "Human corresponding pet id" }, "404": { "description": "Human or pet not found" } }, "parameters": [ { "name": "id", "in": "path", "description": "Unique identifier for pet in database", "required": true, "schema": { "$ref": "#/components/schemas/PetId" } } ] } }, "/pets/{petId}/owner/{ownerId}": { "get": { "operationId": "getPetOwner", "summary": "Get owner by id", "description": "Get the owner for a pet", "tags": [ "pets" ], "parameters": [ { "name": "petId", "in": "path", "description": "Unique identifier for pet in database", "required": true, "schema": { "$ref": "#/components/schemas/PetId" } }, { "name": "ownerId", "in": "path", "description": "Unique identifier for owner in database", "required": true, "schema": { "$ref": "#/components/schemas/PetId" } } ], "responses": { "200": { "description": "Human corresponding owner id" }, "404": { "description": "Human or pet not found" } } } }, "/pets/meta": { "get": { "operationId": "getPetsMeta", "summary": "Get pet metadata", "description": "Returns a list of metadata about pets and their relations in the database", "tags": [ "pets" ], "responses": { "200": { "description": "Metadata for pets" } } } }, "/pets/relative": { "servers": [ { "url": "baseURLV2" } ], "get": { "operationId": "getPetsRelative", "summary": "Get pet metadata", "description": "Returns a list of metadata about pets and their relations in the database", "tags": [ "pets" ], "responses": { "200": { "description": "Metadata for pets" } } } } }, "components": { "schemas": { "PetId": { "description": "Unique identifier for pet in database", "example": 1, "title": "PetId", "type": "integer" }, "PetPayload": { "type": "object", "properties": { "name": { "description": "Name of the pet", "example": "Garfield", "title": "PetName", "type": "string" } }, "additionalProperties": false, "required": [ "name" ] }, "QueryLimit": { "description": "Number of items to return", "example": 25, "title": "QueryLimit", "type": "integer" }, "QueryOffset": { "description": "Starting offset for returning items", "example": 0, "title": "QueryOffset", "type": "integer", "minimum": 0 } }, "requestBodies": { "PetPayload": { "description": "Request payload containing a pet object", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PetPayload" } } } } } } } ================================================ FILE: src/__tests__/resources/example-pet-api.openapi.yml ================================================ openapi: 3.0.0 info: title: Example API description: Example CRUD API for pets version: 1.0.0 tags: - name: pets description: Pet operations servers: - url: http://localhost:8080 paths: /pets: get: operationId: getPets summary: List pets description: Returns all pets in database tags: - pets responses: '200': description: List of pets in database parameters: - name: limit in: query description: Number of items to return required: false schema: $ref: '#/components/schemas/QueryLimit' - name: offset in: query description: Starting offset for returning items required: false schema: $ref: '#/components/schemas/QueryOffset' post: operationId: createPet summary: Create a pet description: Crete a new pet into the database tags: - pets responses: '201': description: Pet created succesfully parameters: [] requestBody: $ref: '#/components/requestBodies/PetPayload' '/pets/{id}': get: operationId: getPetById summary: Get a pet description: Returns a pet by its id in database tags: - pets responses: '200': description: Pet object corresponding to id '404': description: Pet not found parameters: - name: id in: path description: Unique identifier for pet in database required: true schema: $ref: '#/components/schemas/PetId' - name: x-petshop-id in: header description: Optional header parameter to pass petshop id schema: type: string example: "123" put: operationId: replacePetById summary: Replace pet description: Replace an existing pet in the database tags: - pets responses: '200': description: Pet replaced succesfully '404': description: Pet not found parameters: - name: id in: path description: Unique identifier for pet in database required: true schema: $ref: '#/components/schemas/PetId' requestBody: $ref: '#/components/requestBodies/PetPayload' patch: operationId: updatePetById summary: Update pet description: Update an existing pet in the database tags: - pets responses: '200': description: Pet updated succesfully '404': description: Pet not found parameters: - name: id in: path description: Unique identifier for pet in database required: true schema: $ref: '#/components/schemas/PetId' requestBody: $ref: '#/components/requestBodies/PetPayload' delete: operationId: deletePetById summary: Delete a pet description: Deletes a pet by its id in database tags: - pets responses: '200': description: Pet deleted succesfully '404': description: Pet not found parameters: - name: id in: path description: Unique identifier for pet in database required: true schema: $ref: '#/components/schemas/PetId' '/pets/{id}/owner': get: operationId: getOwnerByPetId summary: Get a pet's owner description: Get the owner for a pet tags: - pets responses: '200': description: Human corresponding pet id '404': description: Human or pet not found parameters: - name: id in: path description: Unique identifier for pet in database required: true schema: $ref: '#/components/schemas/PetId' '/pets/{petId}/owner/{ownerId}': get: operationId: getPetOwner summary: Get owner by id description: Get the owner for a pet tags: - pets parameters: - name: petId in: path description: Unique identifier for pet in database required: true schema: $ref: '#/components/schemas/PetId' - name: ownerId in: path description: Unique identifier for owner in database required: true schema: $ref: '#/components/schemas/PetId' responses: '200': description: Human corresponding owner id '404': description: Human or pet not found /pets/meta: get: operationId: getPetsMeta summary: Get pet metadata description: Returns a list of metadata about pets and their relations in the database tags: - pets responses: '200': description: Metadata for pets /pets/relative: servers: [{ url: baseURLV2 }] get: operationId: 'getPetsRelative' summary: Get pet metadata description: Returns a list of metadata about pets and their relations in the database tags: - pets responses: '200': description: Metadata for pets components: schemas: PetId: description: Unique identifier for pet in database example: 1 title: PetId type: integer PetPayload: type: object properties: name: description: Name of the pet example: Garfield title: PetName type: string additionalProperties: false required: - name QueryLimit: description: Number of items to return example: 25 title: QueryLimit type: integer QueryOffset: description: Starting offset for returning items example: 0 title: QueryOffset type: integer minimum: 0 requestBodies: PetPayload: description: 'Request payload containing a pet object' content: application/json: schema: $ref: '#/components/schemas/PetPayload' ================================================ FILE: src/client.test.ts ================================================ import path from 'path'; import fs from 'fs'; import { rest } from 'msw' import { setupServer } from 'msw/node' import MockAdapter from 'axios-mock-adapter'; import { definition, baseURL, baseURLV2, baseURLAlternative, baseURLWithVariableResolved, createDefinition } from './__tests__/fixtures'; import { OpenAPIClientAxios, OpenAPIClient } from './client'; import axios, { AxiosResponse } from 'axios'; const testsDir = path.join(__dirname, '.', '__tests__'); const examplePetAPIJSON = path.join(testsDir, 'resources', 'example-pet-api.openapi.json'); const examplePetAPIYAML = path.join(testsDir, 'resources', 'example-pet-api.openapi.yml'); const server = setupServer( rest.get('http://localhost/example-pet-api.openapi.json', (_req, res, ctx) => { return res(ctx.body(fs.readFileSync(examplePetAPIJSON)), ctx.set('Content-Type', 'application/json')); }), rest.get('http://localhost/example-pet-api.openapi.yml', (_req, res, ctx) => { return res(ctx.body(fs.readFileSync(examplePetAPIYAML)), ctx.set('Content-Type', 'application/yaml')); }) ); beforeAll(() => server.listen()); describe('OpenAPIClientAxios', () => { const checkHasOperationMethods = (client: OpenAPIClient) => { expect(client).toHaveProperty('getPets'); expect(client).toHaveProperty('createPet'); expect(client).toHaveProperty('getPetById'); expect(client).toHaveProperty('replacePetById'); expect(client).toHaveProperty('updatePetById'); expect(client).toHaveProperty('deletePetById'); expect(client).toHaveProperty('getOwnerByPetId'); expect(client).toHaveProperty('getPetOwner'); expect(client).toHaveProperty('getPetsMeta'); expect(client).toHaveProperty('getPetsRelative'); }; describe('init', () => { test('can be initalised with a valid OpenAPI document as JS Object', async () => { const api = new OpenAPIClientAxios({ definition }); await api.init(); expect(api.initialized).toEqual(true); expect(api.client.api).toBe(api); checkHasOperationMethods(api.client); }); test('operation method names are configurable', async () => { const api = new OpenAPIClientAxios({ definition, transformOperationName: (operation) => operation.toUpperCase(), }); await api.init(); expect(api.client).toHaveProperty('GETPETS'); expect(api.client).toHaveProperty('CREATEPET'); expect(api.client).toHaveProperty('GETPETBYID'); }); test('dereferences the input document', async () => { const api = new OpenAPIClientAxios({ definition }); await api.init(); expect(JSON.stringify(api.inputDocument)).toMatch('$ref'); expect(JSON.stringify(api.definition)).not.toMatch('$ref'); }); test('can be initialized using a valid YAML file', async () => { const api = new OpenAPIClientAxios({ definition: 'http://localhost/example-pet-api.openapi.yml' }); await api.init(); expect(api.initialized).toEqual(true); expect(api.client.api).toBe(api); checkHasOperationMethods(api.client); }); test('can be initialized using a valid JSON file', async () => { const api = new OpenAPIClientAxios({ definition: 'http://localhost/example-pet-api.openapi.json' }); await api.init(); expect(api.initialized).toEqual(true); expect(api.client.api).toBe(api); checkHasOperationMethods(api.client); }); test('can be initialized using alternative server using index', async () => { const api = new OpenAPIClientAxios({ definition, withServer: 1 }); await api.init(); expect(api.getBaseURL()).toEqual(baseURLAlternative); expect(api.client.api).toBe(api); checkHasOperationMethods(api.client); }); test('can be initialized using alternative server using description', async () => { const api = new OpenAPIClientAxios({ definition, withServer: 'Alternative server' }); await api.init(); expect(api.getBaseURL()).toEqual(baseURLAlternative); expect(api.client.api).toBe(api); checkHasOperationMethods(api.client); }); test('can be initialized using alternative server with variable in baseURL', async () => { const api = new OpenAPIClientAxios({ definition, withServer: 2, baseURLVariables: { foo2: 'bar2a', foo3: 1 }, }); await api.init(); expect(api.getBaseURL()).toEqual(baseURLWithVariableResolved); expect(api.client.api).toBe(api); checkHasOperationMethods(api.client); }); test('can be initialized using alternative server using object', async () => { const url = 'http://examplde.com/v5'; const api = new OpenAPIClientAxios({ definition, withServer: { url } }); await api.init(); expect(api.getBaseURL()).toEqual(url); expect(api.client.api).toBe(api); checkHasOperationMethods(api.client); }); test('can be initialized using default baseUrl resolver', async () => { const api = new OpenAPIClientAxios({ definition }); await api.init(); expect(api.getBaseURL()).toEqual(baseURL); expect(api.client.api).toBe(api); checkHasOperationMethods(api.client); }); }); describe('withServer', () => { test('can set default server as object', async () => { const api = new OpenAPIClientAxios({ definition }); await api.init(); expect(api.getBaseURL()).toEqual(baseURL); const newServer = { url: 'http://example.com/apiv4', description: 'example api v4', }; api.withServer(newServer); expect(api.getBaseURL()).toEqual(newServer.url); }); test('can set default server by using description', async () => { const api = new OpenAPIClientAxios({ definition }); await api.init(); expect(api.getBaseURL()).toEqual(baseURL); const newServer = 'Alternative server'; api.withServer(newServer); expect(api.getBaseURL()).toEqual(baseURLAlternative); }); test('can set default server by index', async () => { const api = new OpenAPIClientAxios({ definition }); await api.init(); expect(api.getBaseURL()).toEqual(baseURL); const newServer = 1; api.withServer(newServer); expect(api.getBaseURL()).toEqual(baseURLAlternative); }); test('can set default server with variables', async () => { const api = new OpenAPIClientAxios({ definition }); await api.init(); expect(api.getBaseURL()).toEqual(baseURL); const newServer = 2; const newServerVars = { foo2: 'bar2a', foo3: 1 }; api.withServer(newServer, newServerVars); expect(api.getBaseURL()).toEqual(baseURLWithVariableResolved); }); }); describe('initSync', () => { test('can be initialized synchronously with a valid OpenAPI document as JS Object', () => { const api = new OpenAPIClientAxios({ definition }); api.initSync(); expect(api.initialized).toEqual(true); expect(api.client.api).toBe(api); checkHasOperationMethods(api.client); }); test('dereferences the input document', () => { const api = new OpenAPIClientAxios({ definition }); api.initSync(); expect(JSON.stringify(api.inputDocument)).toMatch('$ref'); expect(JSON.stringify(api.definition)).not.toMatch('$ref'); }); test('throws an error when initialized using a URL', () => { const api = new OpenAPIClientAxios({ definition: '/example-pet-api.openapi.json' }); expect(api.initSync).toThrowError(); }); }); describe('client', () => { test('has set default baseURL to the first server in config', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); expect(client.defaults.baseURL).toBe(baseURL); }); test('can override axios default config', async () => { const api = new OpenAPIClientAxios({ definition, axiosConfigDefaults: { maxRedirects: 1, withCredentials: true }, }); const client = await api.init(); expect(client.defaults.maxRedirects).toBe(1); expect(client.defaults.withCredentials).toBe(true); }); test('request defaults from user-provided axios instance are not modified', async () => { const userRequestTransformer = (data: object, headers?: object) => data; const userResponseTransformer = (data: object) => data; const userParamsSerializer = () => ""; const userAdapter = () => new Promise>(() => ({})); const userOnUploadProgress = () => {}; const userOnDownloadProgress = () => {}; const userValidateStatus = () => true; const userCancelToken = new axios.CancelToken(() => {}); const userAxiosInstance = axios.create({ url: '/hello', method: 'post', baseURL: 'https://some-domain.com/api', transformRequest: userRequestTransformer, transformResponse: userResponseTransformer, headers: {'X-Requested-With': 'fake'}, params: { ID: 123456789, }, paramsSerializer: userParamsSerializer, data: { hello: 'world', }, timeout: 1234, withCredentials: true, adapter: userAdapter, auth: { username: 'fake', password: 'fakepassword' }, responseType: 'stream', responseEncoding: 'fakeEncoding', xsrfCookieName: 'HELLO-WORLD', xsrfHeaderName: 'HELLO-WORLD-2', onUploadProgress: userOnUploadProgress, onDownloadProgress: userOnDownloadProgress, maxContentLength: 1234, maxBodyLength: 5678, validateStatus: userValidateStatus, maxRedirects: 99, socketPath: '/fake/path/example', proxy: { host: '1.2.3.4', port: 9876, auth: { username: 'anotherFakeUsername', password: 'anotherFakePassword' } }, cancelToken: userCancelToken, decompress: false }); const api = new OpenAPIClientAxios({ definition, axiosInstance: userAxiosInstance, }); const client = await api.init(); const d = client.defaults; expect(d.url).toBe('/hello'); expect(d.method).toBe('post'); expect(d.baseURL).toBe('https://some-domain.com/api'); expect(d.transformRequest).toBe(userRequestTransformer); expect(d.transformResponse).toBe(userResponseTransformer); expect((d.headers as unknown as Record)['X-Requested-With']).toBe('fake'); expect(d.params.ID).toBe(123456789); expect(d.paramsSerializer).toBe(userParamsSerializer); expect(d.data.hello).toBe('world'); expect(d.timeout).toBe(1234); expect(d.withCredentials).toBe(true); expect(d.adapter).toBe(userAdapter); expect(d.auth).toStrictEqual({ username: 'fake', password: 'fakepassword' }), expect(d.responseType).toBe('stream'); expect(d.responseEncoding).toBe('fakeEncoding'); expect(d.xsrfCookieName).toBe('HELLO-WORLD'); expect(d.xsrfHeaderName).toBe('HELLO-WORLD-2'); expect(d.onUploadProgress).toBe(userOnUploadProgress); expect(d.onDownloadProgress).toBe(userOnDownloadProgress); expect(d.maxContentLength).toBe(1234); expect(d.maxBodyLength).toBe(5678); expect(d.validateStatus).toBe(userValidateStatus); expect(d.maxRedirects).toBe(99); expect(d.socketPath).toBe('/fake/path/example'); expect(d.proxy).toStrictEqual({ host: '1.2.3.4', port: 9876, auth: { username: 'anotherFakeUsername', password: 'anotherFakePassword' }, }); expect(d.cancelToken).toBe(userCancelToken); expect(d.decompress).toBe(false); }); }); describe('operation methods', () => { test('getPets() calls GET /pets', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = [{ id: 1, name: 'Garfield' }]; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets').reply((config) => mockHandler(config)); const res = await client.getPets(); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); test("getPets({ q: 'cats' }) calls GET /pets?q=cats", async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = [{ id: 1, name: 'Garfield' }]; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets').reply((config) => mockHandler(config)); const params = { q: 'cats ' }; const res = await client.getPets(params); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); const mockContext = mockHandler.mock.calls[mockHandler.mock.calls.length - 1][0]; expect(mockContext.params).toEqual(params); }); test("getPets({ q: ['cats', 'dogs'] }) calls GET /pets?q=cats&q=dogs", async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = [{ id: 1, name: 'Garfield' }]; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets').reply((config) => mockHandler(config)); const params = { q: ['cats', 'dogs'] }; const res = await client.getPets(params); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); const mockContext = mockHandler.mock.calls[mockHandler.mock.calls.length - 1][0]; expect(mockContext.params).toEqual(params); }); test('getPetById({ petId: 1 }) calls GET /pets/1', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { id: 1, name: 'Garfield' }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets/1').reply((config) => mockHandler(config)); const res = await client.getPetById({ petId: 1 }); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); test('getPetById(1) calls GET /pets/1', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { id: 1, name: 'Garfield' }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets/1').reply((config) => mockHandler(config)); const res = await client.getPetById(1); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); test('getPetById({ petId: 1 }) calls GET /pets/1', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { id: 1, name: 'Garfield' }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets/1').reply((config) => mockHandler(config)); const res = await client.getPetById({ petId: 1 }); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); test('getPetById({ petId: 1, "x-petshop-id": "test-shop" }) calls GET /pets/1 with request header', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { id: 1, name: 'Garfield' }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets/1').reply((config) => mockHandler(config)); const res = await client.getPetById({ petId: 1, 'x-petshop-id': 'test-shop' }); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalledWith(expect.objectContaining({ headers: expect.objectContaining({ 'x-petshop-id': 'test-shop' }) })) }); test('getPetById({ petId: 1, "x-petshop-id": "test-shop" }) calls GET /pets/1 with request header and extern config', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { id: 1, name: 'Garfield' }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets/1').reply((config) => mockHandler(config)); const config = { headers: { authorization: 'Bearer abc' } }; const res = await client.getPetById({ petId: 1, 'x-petshop-id': 'test-shop' }, undefined, config); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalledWith( expect.objectContaining({ headers: expect.objectContaining({ 'x-petshop-id': 'test-shop', authorization: 'Bearer abc' }), }), ); }); test('getPetById([{ name: "petId", value: "1", in: "path" }]) calls GET /pets/1', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { id: 1, name: 'Garfield' }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets/1').reply((config) => mockHandler(config)); const res = await client.getPetById([{ name: 'petId', value: '1', in: 'path' }]); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); test('getPetById([{ name: "petId", value: "1", in: "path" }, { name: "new", value: "2", in: "query" }]) calls GET /pets/1?new=2', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { id: 1, name: 'Garfield' }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets/1').reply((config) => mockHandler(config)); const res = await client.getPetById([{ name: 'petId', value: '1', in: 'path' }]); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); test('createPet(pet) calls POST /pets with JSON payload', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const pet = { name: 'Garfield' }; const mockResponse = { id: 1, ...pet }; const mockHandler = jest.fn((config) => [201, mockResponse]); mock.onPost('/pets').reply((config) => mockHandler(config)); const res = await client.createPet(null, pet); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); const mockContext = mockHandler.mock.calls[mockHandler.mock.calls.length - 1][0]; expect(mockContext.data).toEqual(JSON.stringify(pet)); }); test('replacePetById(1, pet) calls PUT /pets/1 with JSON payload', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const pet = { id: 1, name: 'Garfield' }; const mockResponse = pet; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onPut('/pets/1').reply((config) => mockHandler(config)); const res = await client.replacePetById(1, pet); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); const mockContext = mockHandler.mock.calls[mockHandler.mock.calls.length - 1][0]; expect(mockContext.data).toEqual(JSON.stringify(pet)); }); test('deletePetById(1) calls DELETE /pets/1', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { id: 1, name: 'Garfield' }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onDelete('/pets/1').reply((config) => mockHandler(config)); const res = await client.deletePetById(1); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); test('getOwnerByPetId(1) calls GET /pets/1/owner', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { name: 'Jon' }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets/1/owner').reply((config) => mockHandler(config)); const res = await client.getOwnerByPetId(1); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); test('getPetOwner([1, 2]) calls GET /pets/1/owner/2', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { name: 'Jon' }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets/1/owner/2').reply((config) => mockHandler(config)); const res = await client.getPetOwner({ petId: 1, ownerId: 2 }); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); test('getPetOwner({ petId: 1, ownerId: 2 }) calls GET /pets/1/owner/2', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { name: 'Jon' }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets/1/owner/2').reply((config) => mockHandler(config)); const res = await client.getPetOwner({ petId: 1, ownerId: 2 }); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); test('getPetsMeta() calls GET /pets/meta', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { totalPets: 10 }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets/meta').reply((config) => mockHandler(config)); const res = await client.getPetsMeta(); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); test('getPetsRelative() calls GET /v2/pets/relative', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockHandler = jest.fn((config) => [200, config.baseURL]); mock.onGet('/pets/relative').reply((config) => mockHandler(config)); const res = await client.getPetsRelative(); expect(res.data).toEqual(baseURLV2); expect(mockHandler).toBeCalled(); }); }); describe('paths dictionary', () => { test(`paths['/pets'].get() calls GET /pets`, async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = [{ id: 1, name: 'Garfield' }]; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets').reply((config) => mockHandler(config)); const res = await client.paths['/pets'].get(); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); test(`paths['/pets/{petId}'].get(1) calls GET /pets/1`, async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { id: 1, name: 'Garfield' }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets/1').reply((config) => mockHandler(config)); const res = await client.paths['/pets/{petId}'].get(1); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); test(`paths['/pets'].post() calls POST /pets`, async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const pet = { name: 'Garfield' }; const mockResponse = { id: 1, ...pet }; const mockHandler = jest.fn((config) => [201, mockResponse]); mock.onPost('/pets').reply((config) => mockHandler(config)); const res = await client.paths['/pets'].post(null, pet); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); const mockContext = mockHandler.mock.calls[mockHandler.mock.calls.length - 1][0]; expect(mockContext.data).toEqual(JSON.stringify(pet)); }); test(`paths['/pets/{petId}'].put(1) calls PUT /pets/1`, async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const pet = { id: 1, name: 'Garfield' }; const mockResponse = pet; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onPut('/pets/1').reply((config) => mockHandler(config)); const res = await client.paths['/pets/{petId}'].put(1, pet); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); const mockContext = mockHandler.mock.calls[mockHandler.mock.calls.length - 1][0]; expect(mockContext.data).toEqual(JSON.stringify(pet)); }); test(`paths['/pets/{petId}'].delete(1) calls DELETE /pets/1`, async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { id: 1, name: 'Garfield' }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onDelete('/pets/1').reply((config) => mockHandler(config)); const res = await client.paths['/pets/{petId}'].delete(1); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); test(`paths['/pets/{petId}/owner/{ownerId}'].get({ petId: 1, ownerId: 2 }) calls GET /pets/1/owner/2`, async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { name: 'Jon' }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets/1/owner/2').reply((config) => mockHandler(config)); const res = await client.paths['/pets/{petId}/owner/{ownerId}'].get({ petId: 1, ownerId: 2 }); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); }); describe('getRequestConfigForOperation()', () => { test('getPets() calls GET /pets', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const config = api.getRequestConfigForOperation('getPets', []); expect(config.method).toEqual('get'); expect(config.path).toEqual('/pets'); }); test('getPets({ q: "cat" }) calls GET /pets?q=cat', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const config = api.getRequestConfigForOperation('getPets', [{ q: 'cat' }]); expect(config.method).toEqual('get'); expect(config.path).toEqual('/pets'); expect(config.url).toMatch('/pets?q=cat'); expect(config.query).toEqual({ q: 'cat' }); expect(config.queryString).toEqual('q=cat'); }); test('getPets({ q: ["cat"] }) calls GET /pets?q=cat', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const config = api.getRequestConfigForOperation('getPets', [{ q: ['cat'] }]); expect(config.method).toEqual('get'); expect(config.path).toEqual('/pets'); expect(config.url).toMatch('/pets?q=cat'); expect(config.query).toEqual({ q: ['cat'] }); expect(config.queryString).toEqual('q=cat'); }); test('getPetById({ petId: 1 }) calls GET /pets/1', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const config = api.getRequestConfigForOperation('getPetById', [{ petId: 1 }]); expect(config.method).toEqual('get'); expect(config.path).toEqual('/pets/1'); expect(config.pathParams).toEqual({ petId: '1' }); }); test('getPetById(1) calls GET /pets/1', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const config = api.getRequestConfigForOperation('getPetById', [1]); expect(config.method).toEqual('get'); expect(config.path).toEqual('/pets/1'); expect(config.pathParams).toEqual({ petId: '1' }); }); test('createPet(null, pet) calls POST /pets with JSON payload', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const pet = { name: 'Garfield' }; const config = api.getRequestConfigForOperation('createPet', [null, pet]); expect(config.method).toEqual('post'); expect(config.path).toEqual('/pets'); expect(config.payload).toEqual(pet); }); test('replacePetById(1, pet) calls PUT /pets/1 with JSON payload', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const pet = { id: 1, name: 'Garfield' }; const config = api.getRequestConfigForOperation('replacePetById', [1, pet]); expect(config.method).toEqual('put'); expect(config.path).toEqual('/pets/1'); expect(config.pathParams).toEqual({ petId: '1' }); expect(config.payload).toEqual(pet); }); test('deletePetById(1) calls DELETE /pets/1', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const config = api.getRequestConfigForOperation('deletePetById', [1]); expect(config.method).toEqual('delete'); expect(config.path).toEqual('/pets/1'); expect(config.pathParams).toEqual({ petId: '1' }); }); test('getOwnerByPetId(1) calls GET /pets/1/owner', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const config = api.getRequestConfigForOperation('getOwnerByPetId', [1]); expect(config.method).toEqual('get'); expect(config.path).toEqual('/pets/1/owner'); expect(config.pathParams).toEqual({ petId: '1' }); }); test('getPetOwner({ petId: 1, ownerId: 2 }) calls GET /pets/1/owner/2', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const config = api.getRequestConfigForOperation('getPetOwner', [{ petId: 1, ownerId: 2 }]); expect(config.method).toEqual('get'); expect(config.path).toEqual('/pets/1/owner/2'); expect(config.pathParams).toEqual({ petId: '1', ownerId: '2' }); }); test('getPetsMeta() calls GET /pets/meta', async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const config = api.getRequestConfigForOperation('getPetsMeta', []); expect(config.method).toEqual('get'); expect(config.path).toEqual('/pets/meta'); }); test('should url encode path parameters', async () => { const api = new OpenAPIClientAxios({ definition: createDefinition({ paths: { '/discounts/{name}': { get: { operationId: 'getDiscount', parameters: [{ name: 'name', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': { description: 'ok' } } }, } } })}); const client = await api.init(); const config = api.getRequestConfigForOperation('getDiscount', ['20% / 30% off']); expect(config.path).toEqual('/discounts/20%25%20%2F%2030%25%20off'); }); }); describe('query parameter array serialization', () => { test('array query param with default style (form, explode: true) serializes as repeated params', async () => { const api = new OpenAPIClientAxios({ definition: createDefinition({ paths: { '/items': { get: { operationId: 'getItems', parameters: [{ name: 'id', in: 'query', schema: { type: 'array', items: { type: 'integer' } }, // default is style: 'form', explode: true }], responses: { '200': { description: 'ok' } } } } } })}); await api.init(); const config = api.getRequestConfigForOperation('getItems', [{ id: [3, 4, 5] }]); expect(config.queryString).toEqual('id=3&id=4&id=5'); }); test('array query param with style: form, explode: false serializes as comma-separated', async () => { const api = new OpenAPIClientAxios({ definition: createDefinition({ paths: { '/items': { get: { operationId: 'getItems', parameters: [{ name: 'id', in: 'query', schema: { type: 'array', items: { type: 'integer' } }, style: 'form', explode: false }], responses: { '200': { description: 'ok' } } } } } })}); await api.init(); const config = api.getRequestConfigForOperation('getItems', [{ id: [3, 4, 5] }]); expect(config.queryString).toEqual('id=3,4,5'); }); test('array query param with style: spaceDelimited serializes as space-separated', async () => { const api = new OpenAPIClientAxios({ definition: createDefinition({ paths: { '/items': { get: { operationId: 'getItems', parameters: [{ name: 'id', in: 'query', schema: { type: 'array', items: { type: 'integer' } }, style: 'spaceDelimited', explode: false }], responses: { '200': { description: 'ok' } } } } } })}); await api.init(); const config = api.getRequestConfigForOperation('getItems', [{ id: [3, 4, 5] }]); expect(config.queryString).toEqual('id=3%204%205'); }); test('array query param with style: pipeDelimited serializes as pipe-separated', async () => { const api = new OpenAPIClientAxios({ definition: createDefinition({ paths: { '/items': { get: { operationId: 'getItems', parameters: [{ name: 'id', in: 'query', schema: { type: 'array', items: { type: 'integer' } }, style: 'pipeDelimited', explode: false }], responses: { '200': { description: 'ok' } } } } } })}); await api.init(); const config = api.getRequestConfigForOperation('getItems', [{ id: [3, 4, 5] }]); expect(config.queryString).toEqual('id=3%7C4%7C5'); }); test('object query param with style: deepObject serializes with bracket notation', async () => { const api = new OpenAPIClientAxios({ definition: createDefinition({ paths: { '/users': { get: { operationId: 'getUsers', parameters: [{ name: 'filter', in: 'query', schema: { type: 'object' }, style: 'deepObject', explode: true }], responses: { '200': { description: 'ok' } } } } } })}); await api.init(); const config = api.getRequestConfigForOperation('getUsers', [{ filter: { role: 'admin', firstName: 'Alex' } } as any]); expect(config.queryString).toMatch(/filter\[role\]=admin/); expect(config.queryString).toMatch(/filter\[firstName\]=Alex/); }); test('object query param with style: form, explode: true serializes as flat params', async () => { const api = new OpenAPIClientAxios({ definition: createDefinition({ paths: { '/users': { get: { operationId: 'getUsers', parameters: [{ name: 'filter', in: 'query', schema: { type: 'object' }, style: 'form', explode: true }], responses: { '200': { description: 'ok' } } } } } })}); await api.init(); const config = api.getRequestConfigForOperation('getUsers', [{ filter: { role: 'admin', firstName: 'Alex' } } as any]); expect(config.queryString).toMatch(/role=admin/); expect(config.queryString).toMatch(/firstName=Alex/); }); test('object query param with style: form, explode: false serializes as comma-separated key-value pairs', async () => { const api = new OpenAPIClientAxios({ definition: createDefinition({ paths: { '/users': { get: { operationId: 'getUsers', parameters: [{ name: 'filter', in: 'query', schema: { type: 'object' }, style: 'form', explode: false }], responses: { '200': { description: 'ok' } } } } } })}); await api.init(); const config = api.getRequestConfigForOperation('getUsers', [{ filter: { role: 'admin', firstName: 'Alex' } } as any]); expect(config.queryString).toMatch(/filter=role,admin,firstName,Alex/); }); test('array of strings with special characters is properly encoded', async () => { const api = new OpenAPIClientAxios({ definition: createDefinition({ paths: { '/items': { get: { operationId: 'getItems', parameters: [{ name: 'tags', in: 'query', schema: { type: 'array', items: { type: 'string' } }, style: 'form', explode: true }], responses: { '200': { description: 'ok' } } } } } })}); await api.init(); const config = api.getRequestConfigForOperation('getItems', [{ tags: ['foo bar', 'baz&qux'] }]); expect(config.queryString).toEqual('tags=foo%20bar&tags=baz%26qux'); }); }); describe('axios methods', () => { test("get('/pets') calls GET /pets", async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = [{ id: 1, name: 'Garfield' }]; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets').reply((config) => mockHandler(config)); const res = await client.get('/pets'); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); test("({ method: 'get', url: '/pets' }) calls GET /pets", async () => { const api = new OpenAPIClientAxios({ definition }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = [{ id: 1, name: 'Garfield' }]; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets').reply((config) => mockHandler(config)); const res = await client({ method: 'get', url: '/pets' }); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); }); describe('transforms', () => { test('transformOperationName', async () => { const api = new OpenAPIClientAxios({ definition, transformOperationName: (operationName) => `${operationName}V1`, }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { name: 'Jon' }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets/1/owner/2').reply((config) => mockHandler(config)); const res = await client.getPetOwnerV1({ petId: 1, ownerId: 2 }); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); }); test('transformOperationMethod', async () => { const api = new OpenAPIClientAxios({ definition, transformOperationMethod: (operationMethod) => { return (params: any, body, config) => { params['petId'] = 1; params['ownerId'] = 2; return operationMethod(params, body, config); }; }, }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { name: 'Jon' }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets/1/owner/2').reply((config) => mockHandler(config)); const res = await client.getPetOwner({}); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); test('transformOperationMethod based on operation', async () => { const api = new OpenAPIClientAxios({ definition, transformOperationMethod: (operationMethod, operation) => { return (params: any, body, config) => { if (operation.operationId === 'getPetOwner') { params['petId'] = 1; params['ownerId'] = 2; } return operationMethod(params, body, config); }; }, }); const client = await api.init(); const mock = new MockAdapter(api.client); const mockResponse = { name: 'Jon' }; const mockHandler = jest.fn((config) => [200, mockResponse]); mock.onGet('/pets/1/owner/2').reply((config) => mockHandler(config)); const res = await client.getPetOwner({}); expect(res.data).toEqual(mockResponse); expect(mockHandler).toBeCalled(); }); }); ================================================ FILE: src/client.ts ================================================ import axios, { AxiosInstance, AxiosRequestConfig, AxiosRequestHeaders, AxiosResponse, Method } from 'axios'; import bath from 'bath-es5'; import { dereferenceSync } from 'dereference-json-schema'; import { Document, Operation, UnknownOperationMethod, OperationMethodArguments, UnknownOperationMethods, RequestConfig, ParamType, HttpMethod, UnknownPathsDictionary, Server, ParameterObject, } from './types/client'; import { serializeQueryParameter } from './query-serializer'; /** * OpenAPIClient is an AxiosInstance extended with operation methods */ export type OpenAPIClient< OperationMethods = UnknownOperationMethods, PathsDictionary = UnknownPathsDictionary, > = AxiosInstance & OperationMethods & { api: OpenAPIClientAxios; paths: PathsDictionary; }; /** * By default OpenAPIClient will use axios as request runner. You can register a different runner, * in case you want to switch over from axios. */ export declare type Runner = { runRequest: RunRequestFunc; context?: UnknownContext; }; /** * Context to be injected into Runner.runRequest */ export declare type UnknownContext = Record; /** * Type for runRequest function. It allows extending/switching from axios to another method of running http requests. */ export declare type RunRequestFunc = ( axiosConfig: AxiosRequestConfig, operation: Operation, context?: UnknownContext, ) => Promise; const DefaultRunnerKey = 'default'; export type OpenAPIClientAxiosOptions = { definition: Document | string; quick?: boolean; withServer?: number | string | Server; baseURLVariables?: { [key: string]: string | number }; applyMethodCommonHeaders?: boolean; transformOperationName?: (operation: string) => string; transformOperationMethod?: ( operationMethod: UnknownOperationMethod, operationToTransform: Operation, ) => UnknownOperationMethod; axiosRunner?: (axiosConfig: AxiosRequestConfig) => Promise; axiosConfigDefaults?: AxiosRequestConfig; } & ({ axiosConfigDefaults?: AxiosRequestConfig; axiosInstance?: never; } | { axiosConfigDefaults?: never; axiosInstance?: AxiosInstance; }); /** * Main class and the default export of the 'openapi-client-axios' module * * @export * @class OpenAPIClientAxios */ export class OpenAPIClientAxios { public document: Document; public inputDocument: Document | string; public definition: Document; public quick: boolean; public initialized: boolean; public instance: any; public axiosConfigDefaults: AxiosRequestConfig; private defaultServer: number | string | Server; private baseURLVariables: { [key: string]: string | number }; private applyMethodCommonHeaders: boolean; private transformOperationName: (operation: string) => string; private transformOperationMethod: ( operationMethod: UnknownOperationMethod, operationToTransform: Operation, ) => UnknownOperationMethod; // maps operationId to Runner private runners: Record; /** * Creates an instance of OpenAPIClientAxios. * * @param opts - constructor options * @param {Document | string} opts.definition - the OpenAPI definition, file path or Document object * @param {boolean} opts.quick - quick mode, skips validation and doesn't guarantee document is unchanged * @param {boolean} opts.applyMethodCommonHeaders Should method (patch / post / put / etc.) specific default headers (from axios.defaults.headers.{method}) be applied to operation methods? * @param {boolean} opts.axiosConfigDefaults - default axios config for the instance * @param {boolean} opts.axiosInstance - axios instance to use * @memberof OpenAPIClientAxios */ constructor(opts: OpenAPIClientAxiosOptions) { this.inputDocument = opts.definition; this.quick = opts.quick ?? false; this.axiosConfigDefaults = opts.axiosConfigDefaults ?? {}; this.instance = opts.axiosInstance; this.defaultServer = opts.withServer ?? 0; this.baseURLVariables = opts.baseURLVariables ?? {}; this.applyMethodCommonHeaders = opts.applyMethodCommonHeaders ?? false; this.transformOperationName = opts.transformOperationName ?? ((operationId: string) => operationId); this.transformOperationMethod = opts.transformOperationMethod ?? ((operationMethod: UnknownOperationMethod) => operationMethod); this.runners = { [DefaultRunnerKey]: { runRequest: opts.axiosRunner ?? ((axiosConfig: AxiosRequestConfig) => this.client.request(axiosConfig)), }, }; } /** * Returns the instance of OpenAPIClient * * @readonly * @type {OpenAPIClient} * @memberof OpenAPIClientAxios */ get client() { return this.instance as OpenAPIClient; } /** * Returns the instance of OpenAPIClient * * @returns * @memberof OpenAPIClientAxios */ public getClient = async (): Promise => { if (!this.initialized) { return this.init(); } return this.instance as Client; }; public withServer(server: number | string | Server, variables: { [key: string]: string | number } = {}) { this.defaultServer = server; this.baseURLVariables = variables; } /** * Initializes OpenAPIClientAxios and creates a member axios client instance * * The init() method should be called right after creating a new instance of OpenAPIClientAxios * * @returns AxiosInstance * @memberof OpenAPIClientAxios */ public init = async (): Promise => { await this.loadDocument(); // dereference the document into definition this.definition = dereferenceSync(this.document) as Document; // create axios instance this.instance = this.createAxiosInstance(); // we are now initialized this.initialized = true; return this.instance as Client; }; /** * Loads document from inputDocument * * Supports loading from a string (url) or an object (json) * * @memberof OpenAPIClientAxios */ public async loadDocument() { if (typeof this.inputDocument === 'object') { this.document = this.inputDocument; } else { // create temporary instance to get the document const client = this.getAxiosInstance(); // load the document const documentRes = await client.get(this.inputDocument); // set document if (typeof documentRes.data === 'object') { // json response this.document = documentRes.data; } else if (typeof documentRes.data === 'string' && documentRes.headers['content-type']?.match(/ya?ml/)) { // yaml response const yaml = await import('js-yaml'); this.document = yaml.load(documentRes.data) as Document; } else { const err = new Error(`Invalid response fetching OpenAPI definition: ${documentRes}`) as any; err.response = documentRes; throw err; } } return this.document; } /** * Synchronous version of .init() * * Note: Only works when the input definition is a valid OpenAPI v3 object (URLs are not supported) * * @memberof OpenAPIClientAxios */ public initSync = (): Client => { if (typeof this.inputDocument !== 'object') { throw new Error(`.initSync() can't be called with a non-object definition. Please use .init()`); } // set document this.document = this.inputDocument; // dereference the document into definition this.definition = dereferenceSync(this.document) as Document; // create axios instance this.instance = this.createAxiosInstance(); // we are now initialized this.initialized = true; return this.instance as Client; }; /** * Creates a new axios instance, if necessary, and returns it */ public getAxiosInstance = (): AxiosInstance => { let instance = this.instance; if (!instance) { instance = axios.create(this.axiosConfigDefaults) as OpenAPIClient; } return instance; }; /** * Creates a new axios instance, extends it and returns it * * @memberof OpenAPIClientAxios */ public createAxiosInstance = (): Client => { // create axios instance const instance = this.getAxiosInstance() as OpenAPIClient; // set baseURL to the one found in the definition servers (if not set in axios defaults) const baseURL = this.getBaseURL(); if (baseURL && !instance.defaults.baseURL) { instance.defaults.baseURL = baseURL; } // create methods for operationIds const operations = this.getOperations(); for (const operation of operations) { const { operationId } = operation; if (operationId) { instance[this.transformOperationName(operationId)] = this.createOperationMethod(operation); } } // create paths dictionary // Example: api.paths['/pets/{id}'].get({ id: 1 }); instance.paths = {}; for (const path in this.definition.paths) { if (this.definition.paths[path]) { if (!instance.paths[path]) { instance.paths[path] = {}; } const methods = this.definition.paths[path]; for (const m in methods) { if (methods[m as HttpMethod] && Object.values(HttpMethod).includes(m as HttpMethod)) { const method = m as HttpMethod; const operation = this.getOperations().find((op) => op.method === method && op.path === path); instance.paths[path][method] = this.createOperationMethod(operation); } } } } // add reference to parent class instance instance.api = this; return instance as any as Client; }; /** * Gets the API baseurl defined in the first OpenAPI specification servers property * * @returns string * @memberof OpenAPIClientAxios */ public getBaseURL = (operation?: Operation): string | undefined => { if (!this.definition) { return undefined; } if (operation) { if (typeof operation === 'string') { operation = this.getOperation(operation); } if (operation.servers && operation.servers[0]) { return operation.servers[0].url; } } // get the target server from this.defaultServer let targetServer; if (typeof this.defaultServer === 'number') { if (this.definition.servers && this.definition.servers[this.defaultServer]) { targetServer = this.definition.servers[this.defaultServer]; } } else if (typeof this.defaultServer === 'string') { for (const server of this.definition.servers) { if (server.description === this.defaultServer) { targetServer = server; break; } } } else if (this.defaultServer.url) { targetServer = this.defaultServer; } // if no targetServer is found, return undefined if (!targetServer) { return undefined; } const baseURL = targetServer.url; const baseURLVariableSet = targetServer.variables; // get baseURL var names const baseURLBuilder = bath(baseURL); // if there are no variables to resolve: return baseURL as is if (!baseURLBuilder.names.length) { return baseURL; } // object to place variables resolved from this.baseURLVariables const baseURLVariablesResolved: { [key: string]: string } = {}; // step through names and assign value from this.baseURLVariables or the default value // note: any variables defined in baseURLVariables but not actually variables in baseURL are ignored for (const name of baseURLBuilder.names) { const varValue = this.baseURLVariables[name]; if (varValue !== undefined && baseURLVariableSet[name].enum) { // if varValue exists assign to baseURLVariablesResolved object if (typeof varValue === 'number') { // if number, get value from enum array const enumVal = baseURLVariableSet[name].enum[varValue]; if (enumVal) { baseURLVariablesResolved[name] = enumVal; } else { // if supplied value out of range: throw error throw new Error( `index ${varValue} out of range for enum of baseURL variable: ${name}; \ enum max index is ${baseURLVariableSet[name].enum.length - 1}`, ); } } else if (typeof varValue === 'string') { // if string, validate against enum array if (baseURLVariableSet[name].enum.includes(varValue)) { baseURLVariablesResolved[name] = varValue; } else { // if supplied value doesn't exist on enum: throw error throw new Error( `${varValue} is not a valid entry for baseURL variable ${name}; \ variable must be of the following: ${baseURLVariableSet[name].enum.join(', ')}`, ); } } } else { // if varValue doesn't exist: get default baseURLVariablesResolved[name] = baseURLVariableSet[name].default; } } // return resolved baseURL return baseURLBuilder.path(baseURLVariablesResolved); }; /** * Creates an axios config object for operation + arguments * @memberof OpenAPIClientAxios */ public getAxiosConfigForOperation = ( operation: Operation | string, args: OperationMethodArguments, ): AxiosRequestConfig => { if (typeof operation === 'string') { operation = this.getOperation(operation); } const request = this.getRequestConfigForOperation(operation, args); // construct axios request config const axiosConfig: AxiosRequestConfig = { method: request.method as Method, url: request.path, data: request.payload, params: request.query, headers: request.headers, }; // allow overriding baseURL with operation / path specific servers const { servers } = operation; if (servers && servers[0]) { axiosConfig.baseURL = servers[0].url; } // allow overriding any parameters in AxiosRequestConfig const [, , config] = args; return { ...axiosConfig, ...config, params: { ...axiosConfig?.params, ...config?.params, }, headers: { ...axiosConfig?.headers, ...config?.headers, }, }; }; /** * Creates a generic request config object for operation + arguments. * * This function contains the logic that handles operation method parameters. * * @memberof OpenAPIClientAxios */ public getRequestConfigForOperation = (operation: Operation | string, args: OperationMethodArguments) => { if (typeof operation === 'string') { operation = this.getOperation(operation); } const pathParams = {} as RequestConfig['pathParams']; const query = {} as RequestConfig['query']; const queryStringParts: string[] = []; const headers = {} as RequestConfig['headers']; const cookies = {} as RequestConfig['cookies']; const parameters = (operation.parameters || []) as ParameterObject[]; const setRequestParam = (name: string, value: any, type: ParamType | string) => { switch (type) { case ParamType.Path: pathParams[name] = value; break; case ParamType.Query: { query[name] = value; // Find the parameter definition to get style and explode const param = parameters.find((p) => p.name === name && p.in === 'query'); const serialized = serializeQueryParameter(param, name, value); queryStringParts.push(...serialized); break; } case ParamType.Header: headers[name] = value; break; case ParamType.Cookie: cookies[name] = value; break; } }; const getParamType = (paramName: string): ParamType => { const param = parameters.find(({ name }) => name === paramName); if (param) { return param.in as ParamType; } // default all params to query if operation doesn't specify param return ParamType.Query; }; const getFirstOperationParam = () => { const firstRequiredParam = parameters.find(({ required }) => required === true); if (firstRequiredParam) { return firstRequiredParam; } const firstParam = parameters[0]; if (firstParam) { return firstParam; } }; const [paramsArg, payload] = args; if (Array.isArray(paramsArg)) { // ParamsArray for (const param of paramsArg) { setRequestParam(param.name, param.value, param.in || getParamType(param.name)); } } else if (typeof paramsArg === 'object') { // ParamsObject for (const name in paramsArg) { if (paramsArg[name] !== undefined) { setRequestParam(name, paramsArg[name], getParamType(name)); } } } else if (paramsArg) { const firstParam = getFirstOperationParam(); if (!firstParam) { throw new Error(`No parameters found for operation ${operation.operationId}`); } setRequestParam(firstParam.name, paramsArg, firstParam.in as ParamType); } // path parameters const pathBuilder = bath(operation.path); // make sure all path parameters are set for (const name of pathBuilder.names) { const value = pathParams[name]; pathParams[name] = `${value}`; } const path = pathBuilder.path(pathParams); // queryString parameter const queryString = queryStringParts.join('&'); // full url with query string const url = `${this.getBaseURL(operation)}${path}${queryString ? `?${queryString}` : ''}`; // add default common headers const defaultHeaders = this.client.defaults.headers; for (const [key, val] of Object.entries(defaultHeaders.common ?? {})) { headers[key] = val; } // add method specific default headers if (this.applyMethodCommonHeaders) { const methodHeaders: AxiosRequestHeaders = (defaultHeaders as any)[operation.method] ?? {}; for (const [key, val] of Object.entries(methodHeaders)) { headers[key] = val; } } // construct request config const config: RequestConfig = { method: operation.method, url, path, pathParams, query, queryString, headers, cookies, payload, }; return config; }; /** * Flattens operations into a simple array of Operation objects easy to work with * * @returns {Operation[]} * @memberof OpenAPIBackend */ public getOperations = (): Operation[] => { const paths = this.definition?.paths || {}; return Object.entries(paths).flatMap(([path, pathObject]) => { return Object.values(HttpMethod) .map((method) => ({ path, method, operation: pathObject[method] })) .filter(({ operation }) => operation) .map(({ operation, method }) => { const op: Partial = { ...(typeof operation === 'object' ? operation : {}), path, method: method as HttpMethod, }; if (pathObject.parameters) { op.parameters = [...(op.parameters || []), ...pathObject.parameters]; } if (pathObject.servers) { op.servers = [...(op.servers || []), ...pathObject.servers]; } op.security = op.security ?? this.definition.security; return op as Operation; }); }); }; /** * Gets a single operation based on operationId * * @param {string} operationId * @returns {Operation} * @memberof OpenAPIBackend */ public getOperation = (operationId: string): Operation | undefined => { return this.getOperations().find((op) => op.operationId === operationId); }; /** * By default OpenAPIClient will use axios as request runner. You can register a different runner, * in case you want to switch over from axios. This allows transitioning from axios to your library of choice. * @param runner - request runner to be registered, either for all operations, or just one operation. * @param operationId - optional parameter. If provided, runner will be registered for a single operation. Else, it will be registered for all operations. */ public registerRunner(runner: Runner, operationId?: string) { this.runners[operationId ?? DefaultRunnerKey] = runner; } private getRunner(operationId: string) { return this.runners[operationId] ?? this.runners[DefaultRunnerKey]; } /** * Creates an axios method for an operation * (...pathParams, data?, config?) => Promise * * @param {Operation} operation * @memberof OpenAPIClientAxios */ private createOperationMethod = (operation: Operation): UnknownOperationMethod => { const originalOperationMethod = async (...args: OperationMethodArguments) => { const axiosConfig = this.getAxiosConfigForOperation(operation, args); // run the axios request with the registered runner // by default: axios runner const runner = this.getRunner(operation.operationId); return runner.runRequest(axiosConfig, operation, runner.context); }; return this.transformOperationMethod(originalOperationMethod, operation); }; } ================================================ FILE: src/index.ts ================================================ import { OpenAPIClientAxios } from './client'; export default OpenAPIClientAxios; export * from './client'; export * from './types/client'; // re-export axios types export type { Axios, AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'; ================================================ FILE: src/query-serializer.ts ================================================ import { ParameterObject } from './types/client'; /** * Serializes a query parameter according to OpenAPI 3.0 specification * * @param param - The parameter definition from OpenAPI spec * @param name - The parameter name * @param value - The parameter value (can be primitive, array, or object) * @returns Array of query string parts (key=value pairs) */ export const serializeQueryParameter = ( param: ParameterObject | undefined, name: string, value: any, ): string[] => { // Get style and explode from parameter definition with defaults // Per OpenAPI spec: default style for query is 'form', default explode is true const style = param?.style || 'form'; const explode = param?.explode !== undefined ? param.explode : true; // Handle null/undefined values if (value === null || value === undefined) { return []; } // Handle arrays if (Array.isArray(value)) { return serializeArrayParameter(name, value, style, explode); } // Handle objects if (typeof value === 'object') { return serializeObjectParameter(name, value, style, explode); } // Handle primitive values return [`${encodeURIComponent(name)}=${encodeURIComponent(String(value))}`]; }; /** * Serializes an array query parameter */ const serializeArrayParameter = ( name: string, value: any[], style: string, explode: boolean, ): string[] => { if (value.length === 0) { return []; } switch (style) { case 'form': if (explode) { // form explode=true: id=3&id=4&id=5 return value.map((item) => `${encodeURIComponent(name)}=${encodeURIComponent(String(item))}`); } else { // form explode=false: id=3,4,5 const encodedValues = value.map((item) => encodeURIComponent(String(item))).join(','); return [`${encodeURIComponent(name)}=${encodedValues}`]; } case 'spaceDelimited': if (explode) { // spaceDelimited with explode=true is not valid per spec, but fallback to form explode=true return value.map((item) => `${encodeURIComponent(name)}=${encodeURIComponent(String(item))}`); } else { // spaceDelimited explode=false: id=3%204%205 const encodedValues = value.map((item) => encodeURIComponent(String(item))).join('%20'); return [`${encodeURIComponent(name)}=${encodedValues}`]; } case 'pipeDelimited': if (explode) { // pipeDelimited with explode=true is not valid per spec, but fallback to form explode=true return value.map((item) => `${encodeURIComponent(name)}=${encodeURIComponent(String(item))}`); } else { // pipeDelimited explode=false: id=3%7C4%7C5 const encodedValues = value.map((item) => encodeURIComponent(String(item))).join('%7C'); return [`${encodeURIComponent(name)}=${encodedValues}`]; } default: // Default to form explode=true return value.map((item) => `${encodeURIComponent(name)}=${encodeURIComponent(String(item))}`); } }; /** * Serializes an object query parameter */ const serializeObjectParameter = ( name: string, value: Record, style: string, explode: boolean, ): string[] => { const keys = Object.keys(value); if (keys.length === 0) { return []; } switch (style) { case 'deepObject': if (explode) { // deepObject explode=true: filter[role]=admin&filter[firstName]=Alex return keys.map((key) => `${encodeURIComponent(name)}[${encodeURIComponent(key)}]=${encodeURIComponent(String(value[key]))}` ); } else { // deepObject with explode=false is not valid per spec, but fallback to form const pairs: string[] = []; for (const key of keys) { pairs.push(encodeURIComponent(key)); pairs.push(encodeURIComponent(String(value[key]))); } return [`${encodeURIComponent(name)}=${pairs.join(',')}`]; } case 'form': if (explode) { // form explode=true: role=admin&firstName=Alex (flattened) return keys.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value[key]))}` ); } else { // form explode=false: filter=role,admin,firstName,Alex const pairs: string[] = []; for (const key of keys) { pairs.push(encodeURIComponent(key)); pairs.push(encodeURIComponent(String(value[key]))); } return [`${encodeURIComponent(name)}=${pairs.join(',')}`]; } default: // Default to form explode=true return keys.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value[key]))}` ); } }; ================================================ FILE: src/types/client.ts ================================================ import type { AxiosResponse, AxiosRequestConfig } from 'axios'; import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'; export type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'; /** * Type alias for OpenAPI document. We only support v3 */ export declare type Document = OpenAPIV3.Document | OpenAPIV3_1.Document; export declare type Server = OpenAPIV3.ServerObject | OpenAPIV3_1.ServerObject; export declare type ParameterObject = OpenAPIV3.ParameterObject | OpenAPIV3_1.ParameterObject; /** * OpenAPI allowed HTTP methods */ export enum HttpMethod { Get = 'get', Put = 'put', Post = 'post', Patch = 'patch', Delete = 'delete', Options = 'options', Head = 'head', Trace = 'trace', } /** * OpenAPI parameters "in" */ export enum ParamType { Query = 'query', Header = 'header', Path = 'path', Cookie = 'cookie', } /** * Operation method spec */ export declare type ImplicitParamValue = string | number; export interface ExplicitParamValue { value: string | number; name: string; in?: ParamType | string; } export interface UnknownParamsObject { [parameter: string]: ImplicitParamValue | ImplicitParamValue[]; } export declare type ParamsArray = ExplicitParamValue[]; export declare type SingleParam = ImplicitParamValue; export declare type Parameters = ParamsObject | ParamsArray | SingleParam; export declare type RequestPayload = any; export declare type OperationMethodArguments = [Parameters?, RequestPayload?, AxiosRequestConfig?]; export declare type OperationResponse = Promise>; export declare type UnknownOperationMethod = ( parameters?: Parameters, data?: RequestPayload, config?: AxiosRequestConfig, ) => OperationResponse; export interface UnknownOperationMethods { [operationId: string]: UnknownOperationMethod; } /** * Generic request config object */ export interface RequestConfig { method: HttpMethod; url: string; path: string; pathParams: { [key: string]: string; }; query: { [key: string]: string | string[]; }; queryString: string; headers: AxiosRequestConfig['headers']; cookies: { [cookie: string]: string; }; payload?: RequestPayload; } /** * Operation object extended with path and method for easy looping */ export interface Operation extends OpenAPIV3.OperationObject { path: string; method: HttpMethod; } /** * A dictionary of paths and their methods */ export interface UnknownPathsDictionary { [path: string]: { [method in HttpMethod]?: UnknownOperationMethod; }; } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "strict": true, "target": "es5", "module": "commonjs", "moduleResolution": "node", "lib": ["esnext", "dom"], "esModuleInterop": true, "noImplicitAny": true, "strictPropertyInitialization": false, "strictNullChecks": false, "baseUrl": ".", "rootDir": "src/", "outDir": "", "sourceMap": true, "declaration": true, "downlevelIteration": true, "allowSyntheticDefaultImports": true }, "include": [ "src/**/*" ], "exclude": [ "**/*.test.ts" ] }