├── .changeset └── config.json ├── .github └── workflows │ ├── ci.yml │ ├── compatibility-check.yml │ └── release.yml ├── .gitignore ├── .lefthook.yml ├── .node-version ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslint.config.mjs ├── exports └── main.ts ├── logo.svg ├── package-lock.json ├── package.json ├── prettier.config.mjs ├── src ├── api-spec.ts ├── http-status-wildcard.ts ├── openapi-http-utilities.ts ├── openapi-http.test.ts ├── openapi-http.ts ├── path-mapping.test.ts ├── path-mapping.ts ├── query-params.ts ├── request.ts ├── response-resolver.ts ├── response.ts └── type-utils.ts ├── test ├── fixtures │ ├── .redocly.yml │ ├── http-methods.api.yml │ ├── http-utilities.api.yml │ ├── no-content.api.yml │ ├── options.api.yml │ ├── path-fragments.api.yml │ ├── query-params.api.yml │ ├── request-body.api.yml │ └── response-content.api.yml ├── http-methods.test-d.ts ├── http-utilities.test-d.ts ├── no-content.test-d.ts ├── no-content.test.ts ├── options.test.ts ├── path-fragments.test-d.ts ├── path-fragments.test.ts ├── query-params.test-d.ts ├── query-params.test.ts ├── request-body.test-d.ts ├── request-body.test.ts ├── response-content.test-d.ts └── response-content.test.ts ├── tsconfig.build.json ├── tsconfig.cjs.json ├── tsconfig.json └── vitest.config.ts /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "christoph-fricke/openapi-msw" } 6 | ], 7 | "commit": false, 8 | "access": "public", 9 | "baseBranch": "main" 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | env: 9 | CI: true 10 | 11 | jobs: 12 | build: 13 | runs-on: macos-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version-file: ".node-version" 21 | cache: "npm" 22 | - name: Install Dependencies 23 | run: npm ci 24 | - name: Lint Project 25 | run: npm run lint 26 | - name: Run Unit Tests 27 | run: npm run test:unit 28 | - name: Build Package 29 | run: npm run build 30 | - name: Generate Test Fixtures 31 | run: npm run generate 32 | - name: Run Integration Tests 33 | run: npm run test:int 34 | -------------------------------------------------------------------------------- /.github/workflows/compatibility-check.yml: -------------------------------------------------------------------------------- 1 | name: compatibility-check 2 | 3 | on: 4 | schedule: 5 | - cron: "21 12 * * 5" 6 | workflow_dispatch: 7 | 8 | env: 9 | CI: true 10 | 11 | jobs: 12 | check: 13 | runs-on: macos-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version-file: ".node-version" 21 | cache: "npm" 22 | - name: Install Dependencies 23 | run: npm ci 24 | - name: Build Package 25 | run: npm run build 26 | - name: Install latest MSW and OpenAPI-TS versions 27 | run: npm install msw@latest openapi-typescript@latest 28 | - name: Generate Test Fixtures 29 | run: npm run generate 30 | - name: Run Integration Tests 31 | run: npm run test:int 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | permissions: 11 | id-token: write # to publish with provenance 12 | contents: write # to create release (changesets/action) 13 | issues: write # to post issue comments (changesets/action) 14 | pull-requests: write # to create pull request (changesets/action) 15 | 16 | jobs: 17 | release: 18 | if: github.repository == 'christoph-fricke/openapi-msw' 19 | runs-on: macos-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version-file: ".node-version" 27 | cache: "npm" 28 | - name: Install Dependencies 29 | run: npm ci 30 | - name: Build Package 31 | run: npm run build 32 | - name: Version and/or Publish Package 33 | uses: changesets/action@v1 34 | with: 35 | publish: npm run release 36 | version: npm run version 37 | commit: "ci: release package" 38 | title: "ci: release package" 39 | env: 40 | NPM_CONFIG_PROVENANCE: true 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | cjs 3 | dist 4 | node_modules 5 | test/fixtures/*.ts -------------------------------------------------------------------------------- /.lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: false 3 | commands: 4 | lint: 5 | priority: 1 6 | glob: "*.{ts,js,mjs}" 7 | run: npx eslint --fix {staged_files} 8 | stage_fixed: true 9 | format: 10 | priority: 2 11 | glob: "*" 12 | run: npx prettier --ignore-unknown --write {staged_files} 13 | stage_fixed: true 14 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v22.14.0 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "streetsidesoftware.code-spell-checker", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode", 6 | "vitest.explorer" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["openapi", "typecheck", "redoc", "redocly"], 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "typescript.tsdk": "node_modules/typescript/lib" 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # openapi-msw 2 | 3 | ## 1.2.0 4 | 5 | ### Minor Changes 6 | 7 | - [#78](https://github.com/christoph-fricke/openapi-msw/pull/78) [`482c028`](https://github.com/christoph-fricke/openapi-msw/commit/482c0282805a44013ada2ab95a08f75ae0bba479) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Added utility types for creating type-safe functionality around OpenAPI-MSW. Special thanks to [@DrewHoo](https://github.com/DrewHoo) for suggesting and inspiring this change. 8 | 9 | ```typescript 10 | import { 11 | createOpenApiHttp, 12 | type PathsFor, 13 | type RequestBodyFor, 14 | type ResponseBodyFor, 15 | } from "openapi-msw"; 16 | 17 | const http = createOpenApiHttp(); 18 | 19 | // A union of all possible GET paths. 20 | type Paths = PathsFor; 21 | 22 | // The request body for POST /tasks. 23 | type RequestBody = RequestBodyFor; 24 | 25 | // The response body for GET /tasks. 26 | type ResponseBody = ResponseBodyFor; 27 | ``` 28 | 29 | ## 1.1.0 30 | 31 | ### Minor Changes 32 | 33 | - [#73](https://github.com/christoph-fricke/openapi-msw/pull/73) [`f81ae29`](https://github.com/christoph-fricke/openapi-msw/commit/f81ae2928233fb5d3dd22d2bb0d9123da8afc6ca) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Added a `request.clone()` type override to continue returning type-safe `OpenApiRequest`s when called. With this, cloning the `request` in resolvers does not lose its type-safety on body parsing methods. 34 | 35 | ## 1.0.0 36 | 37 | ### Major Changes 38 | 39 | - [#70](https://github.com/christoph-fricke/openapi-msw/pull/70) [`bc9a50f`](https://github.com/christoph-fricke/openapi-msw/commit/bc9a50ff869583a070a08be6e1de0868440adb48) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Updated MSW peer dependency from _v2.0.0_ to _v2.7.0_. This is only a breaking change if you are not already using the latest version of MSW. 40 | 41 | - [#70](https://github.com/christoph-fricke/openapi-msw/pull/70) [`bc9a50f`](https://github.com/christoph-fricke/openapi-msw/commit/bc9a50ff869583a070a08be6e1de0868440adb48) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Renamed `HttpHandlerFactory` type to `OpenApiHttpRequestHandler`. This rename aligns its name with MSW's equivalent `HttpRequestHandler` type. 42 | 43 | ### Minor Changes 44 | 45 | - [#68](https://github.com/christoph-fricke/openapi-msw/pull/68) [`33088be`](https://github.com/christoph-fricke/openapi-msw/commit/33088be804138e98647ffc0e9d85d71f2dfae6e8) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Removed dependency on _openapi-typescript-helpers_. We were depending on an older version without being able to easily update. With this refactoring, your projects should no longer resolve to multiple versions of _openapi-typescript-helpers_. 46 | 47 | ## 0.7.1 48 | 49 | ### Patch Changes 50 | 51 | - [#63](https://github.com/christoph-fricke/openapi-msw/pull/63) [`b9f4bea`](https://github.com/christoph-fricke/openapi-msw/commit/b9f4bead7907eb7cd0d1c7458e6c89520c65414f) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Fixed type inference for extended JSON mime types, such as `application/problem+json`. Previously, APIs like `response(...).json` would be typed as `never` for such mime types. Now, they will be properly typed. 52 | 53 | ## 0.7.0 54 | 55 | ### Minor Changes 56 | 57 | - [#58](https://github.com/christoph-fricke/openapi-msw/pull/58) [`f08acf1`](https://github.com/christoph-fricke/openapi-msw/commit/f08acf19a6e792ab36214bf8c1925447c2489704) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Added "content-length" header for `response(...).empty()`. If no "content-length" header is provided in the response init, the "content-length" header is now set with the value "0". See #56 for more details. 58 | 59 | ## 0.6.1 60 | 61 | ### Patch Changes 62 | 63 | - [#54](https://github.com/christoph-fricke/openapi-msw/pull/54) [`6793dcc`](https://github.com/christoph-fricke/openapi-msw/commit/6793dccff4641dedc266f8096ede373dc95fca8f) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Fixed type-exports for CommonJS refer to a non-existing file. 64 | 65 | ## 0.6.0 66 | 67 | ### Minor Changes 68 | 69 | - [#50](https://github.com/christoph-fricke/openapi-msw/pull/50) [`37da681`](https://github.com/christoph-fricke/openapi-msw/commit/37da6814e65105cfc5c38067bdf32ba1c6208d8f) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Added compilation and exports for CommonJS modules. This makes OpenAPI-MSW usable in projects that still use CommonJS as their module system. 70 | 71 | - [#52](https://github.com/christoph-fricke/openapi-msw/pull/52) [`88ca9da`](https://github.com/christoph-fricke/openapi-msw/commit/88ca9da973ac0a9d25a3185e1cf05b88722c717d) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Added enhanced typing for the `request` object. Now, `request.json()` and `request.text()` infer their return type from the given OpenAPI request-body content schema. Previously, only `request.json()` has been inferred without considering the content-type. 72 | 73 | ## 0.5.0 74 | 75 | ### Minor Changes 76 | 77 | - [#41](https://github.com/christoph-fricke/openapi-msw/pull/41) [`fe70d20`](https://github.com/christoph-fricke/openapi-msw/commit/fe70d20494692df764188c35105cbf6be178d687) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Added `response` helper to the resolver-info argument. It provides an granular type-safety when creating HTTP responses. Instead of being able to return any status code, `response` limits status codes, content types, and their response bodies to the combinations defined by the given OpenAPI spec. 78 | 79 | ```typescript 80 | /* 81 | Imagine this endpoint specification for the following example: 82 | 83 | /response-example: 84 | get: 85 | summary: Get Resource 86 | operationId: getResource 87 | responses: 88 | 200: 89 | description: Success 90 | content: 91 | application/json: 92 | schema: 93 | $ref: "#/components/schemas/Resource" 94 | text/plain: 95 | schema: 96 | type: string 97 | enum: ["Hello", "Goodbye"] 98 | 204: 99 | description: NoContent 100 | "5XX": 101 | description: Error 102 | content: 103 | text/plain: 104 | schema: 105 | type: string 106 | */ 107 | 108 | const handler = http.get("/response-example", ({ response }) => { 109 | // Error: Status Code 204 only allows empty responses 110 | const invalidRes = response(204).text("Hello"); 111 | 112 | // Error: Status Code 200 only allows "Hello" as text 113 | const invalidRes = response(200).text("Some other string"); 114 | 115 | // No Error: This combination is part of the defined OpenAPI spec 116 | const validRes = response(204).empty(); 117 | 118 | // No Error: This combination is part of the defined OpenAPI spec 119 | const validRes = response(200).text("Hello"); 120 | 121 | // Using a wildcard requires you to provide a matching status code for the response 122 | const validRes = response("5XX").text("Fatal Error", { status: 503 }); 123 | }); 124 | ``` 125 | 126 | ## 0.4.0 127 | 128 | ### Minor Changes 129 | 130 | - [#42](https://github.com/christoph-fricke/openapi-msw/pull/42) [`c466bbc`](https://github.com/christoph-fricke/openapi-msw/commit/c466bbcf4c27dea2e4c6928bf92369abf138fb47) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Changed response body types to be a union of all response bodies for all status codes and media types. This makes it possible to return responses for specified error codes without requiring a type cast. Imagine the following endpoint. Its response body is now typed as `StrictResponse<{ id: string, value: number } | string | null>`. 131 | 132 | ```yaml 133 | /resource: 134 | get: 135 | summary: Get Resource 136 | operationId: getResource 137 | responses: 138 | 200: 139 | description: Success 140 | content: 141 | application/json: 142 | schema: 143 | type: object 144 | required: [id, value] 145 | properties: 146 | id: 147 | type: string 148 | value: 149 | type: integer 150 | 202: 151 | description: Accepted 152 | content: 153 | text/plain: 154 | schema: 155 | type: string 156 | 418: 157 | description: NoContent 158 | ``` 159 | 160 | ### Patch Changes 161 | 162 | - [#44](https://github.com/christoph-fricke/openapi-msw/pull/44) [`a9338b5`](https://github.com/christoph-fricke/openapi-msw/commit/a9338b5bcb289ceaab0e5538a4131995c10dd5f0) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Fixed endpoints with no specified query params allow any query key in the `query` helper methods. Now, providing any query key causes a type error. 163 | 164 | ## 0.3.0 165 | 166 | ### Minor Changes 167 | 168 | - [#33](https://github.com/christoph-fricke/openapi-msw/pull/33) [`1f3958d`](https://github.com/christoph-fricke/openapi-msw/commit/1f3958dee1fce818b20c37bf486d6d73a0fcd1ea) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Added `query` helper to resolver-info argument. It provides a type-safe wrapper around `URLSearchParams` for reading search parameters. As usual, the information about available parameters is inferred from your OpenAPI spec. 169 | 170 | ```typescript 171 | /* 172 | Imagine this endpoint specification for the following example: 173 | 174 | /query-example: 175 | get: 176 | summary: Query Example 177 | operationId: getQueryExample 178 | parameters: 179 | - name: filter 180 | in: query 181 | required: true 182 | schema: 183 | type: string 184 | - name: page 185 | in: query 186 | schema: 187 | type: number 188 | - name: sort 189 | in: query 190 | required: false 191 | schema: 192 | type: string 193 | enum: ["asc", "desc"] 194 | - name: sortBy 195 | in: query 196 | schema: 197 | type: array 198 | items: 199 | type: string 200 | */ 201 | 202 | const handler = http.get("/query-example", ({ query }) => { 203 | const filter = query.get("filter"); // Typed as string 204 | const page = query.get("page"); // Typed as string | null since it is not required 205 | const sort = query.get("sort"); // Typed as "asc" | "desc" | null 206 | const sortBy = query.getAll("sortBy"); // Typed as string[] 207 | 208 | // Supported methods from URLSearchParams: get(), getAll(), has(), size 209 | if (query.has("sort", "asc")) { 210 | /* ... */ 211 | } 212 | 213 | return HttpResponse.json({ 214 | /* ... */ 215 | }); 216 | }); 217 | ``` 218 | 219 | - [#35](https://github.com/christoph-fricke/openapi-msw/pull/35) [`07fa9b0`](https://github.com/christoph-fricke/openapi-msw/commit/07fa9b0822c441708c70d3e0698a6dbe7577f58c) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Restructured the library to add support for additional response resolver info. The enhanced `ResponseResolver` type and `ResponseResolverInfo` are available as exports. 220 | 221 | ## 0.2.2 222 | 223 | ### Patch Changes 224 | 225 | - [#31](https://github.com/christoph-fricke/openapi-msw/pull/31) [`556dfca`](https://github.com/christoph-fricke/openapi-msw/commit/556dfca3a2c87eeec6f1f7acd2db63af52df2806) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Fixed a type mismatch between path fragment types and the values provided at runtime, which are always strings. Now all path-fragments are typed as string. If a fragment's schema is a string constrained by an enum, the resulting string literals are preserved. This fixes bug [#22](https://github.com/christoph-fricke/openapi-msw/issues/22). 226 | 227 | ```typescript 228 | const handler = http.get("/resource/{id}", ({ params }) => { 229 | // Previously calling "parseInt(...)" caused a type error 230 | // when the schema type for "id" is defined as number. 231 | const id = parseInt(params.id); 232 | 233 | return HttpResponse.json({ id }); 234 | }); 235 | ``` 236 | 237 | ## 0.2.1 238 | 239 | ### Patch Changes 240 | 241 | - [#27](https://github.com/christoph-fricke/openapi-msw/pull/27) [`232ae11`](https://github.com/christoph-fricke/openapi-msw/commit/232ae11b46bda40ec493b4eed6c270e4a9160a00) Thanks [@luchsamapparat](https://github.com/luchsamapparat)! - Fixed a compilation warning in projects using OpenAPI-MSW, which was caused by missing sources in source maps. 242 | 243 | ## 0.2.0 244 | 245 | ### Minor Changes 246 | 247 | - [#24](https://github.com/christoph-fricke/openapi-msw/pull/24) [`bfd7a99`](https://github.com/christoph-fricke/openapi-msw/commit/bfd7a997c662c29bac8a91ea0952993c20dadee8) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Added JSDoc comments to public API for improved DX. 248 | 249 | ### Patch Changes 250 | 251 | - [#23](https://github.com/christoph-fricke/openapi-msw/pull/23) [`29ecb9c`](https://github.com/christoph-fricke/openapi-msw/commit/29ecb9cbccff09d042fe3e55552c906e22f6054c) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Fixed a small naming mistake in the "Getting Started" code example. 252 | 253 | ## 0.1.2 254 | 255 | ### Patch Changes 256 | 257 | - [#17](https://github.com/christoph-fricke/openapi-msw/pull/17) [`2931f0c`](https://github.com/christoph-fricke/openapi-msw/commit/2931f0c37e5ca66378ec2a9596e07736b417a96b) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Fixed OpenAPI operations with no-content responses cannot return a response. Now they are required to return an empty response, i.e. `null` as response body. 258 | 259 | ```typescript 260 | const http = createOpenApiHttp(); 261 | 262 | // Resolver function is required to return a `StrictResponse` (empty response) 263 | // if the OpenAPI operation specifies `content?: never` for the response. 264 | const noContent = http.delete("/resource", ({ params }) => { 265 | return HttpResponse.json(null, { status: 204 }); 266 | }); 267 | ``` 268 | 269 | ## 0.1.1 270 | 271 | ### Patch Changes 272 | 273 | - [#12](https://github.com/christoph-fricke/openapi-msw/pull/12) [`96ce15c`](https://github.com/christoph-fricke/openapi-msw/commit/96ce15c5f81535fb1091143dab2dce671ba65836) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Add legacy entrypoint definitions (types, module) for tools and bundlers that do not understand package.json#exports fields. 274 | 275 | ## 0.1.0 276 | 277 | ### Minor Changes 278 | 279 | - [#9](https://github.com/christoph-fricke/openapi-msw/pull/9) [`6364870`](https://github.com/christoph-fricke/openapi-msw/commit/636487083c131f582507b096318d114c97131630) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Added installation and complete usage guide to the documentation. 280 | 281 | - [#5](https://github.com/christoph-fricke/openapi-msw/pull/5) [`d15a0c2`](https://github.com/christoph-fricke/openapi-msw/commit/d15a0c2720f4d51415309f432cdc50aefb90f25f) Thanks [@christoph-fricke](https://github.com/christoph-fricke)! - Added `createOpenApiHttp(...)` to create a thin, type-safe wrapper around [MSW](https://mswjs.io/)'s `http` that uses [openapi-ts](https://openapi-ts.pages.dev/introduction/) `paths`: 282 | 283 | ```ts 284 | import type { paths } from "./openapi-ts-definitions"; 285 | 286 | const http = createOpenApiHttp(); 287 | 288 | // Define handlers with fully typed paths, path params, and request/response bodies 289 | const handler = http.get("/pets/{id}", () => { 290 | /* ... */ 291 | }); 292 | 293 | // Fallback to default http implementation 294 | const catchAll = http.untyped.all("*", () => { 295 | /* ... */ 296 | }); 297 | ``` 298 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Christoph Fricke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | OpenAPI-MSW logo 3 |

4 | 5 |

OpenAPI-MSW

6 | 7 | A tiny, type-safe wrapper around [MSW](https://mswjs.io) to add support for full 8 | type inference from OpenAPI schema definitions that are generated with 9 | [OpenAPI-TS](https://openapi-ts.dev/introduction). 10 | 11 | > _Please note that the lack of regular releases does not mean that OpenAPI-MSW 12 | > is abandoned. It works very reliably and includes all the features I intended 13 | > to include. I rely heavily on the package myself and will resolve future 14 | > problems and incompatibilities._ 15 | 16 | ## Installation 17 | 18 | You can install OpenAPI-MSW with this shell command: 19 | 20 | ```bash 21 | npm i -D openapi-msw 22 | ``` 23 | 24 | This package has a peer-dependency to MSW **v2**. There is no plan to provide 25 | backwards compatibility for MSW v1. 26 | 27 | ## Usage Guide 28 | 29 | This guide assumes that you already have OpenAPI-TS set up and configured to 30 | generate `paths` definitions. If you have not set it up, please refer to the 31 | [OpenAPI-TS setup guide](https://openapi-ts.dev/introduction) before continuing 32 | with this usage guide. 33 | 34 | ### Getting Started 35 | 36 | Once you have your OpenAPI schema types ready-to-go, you can use OpenAPI-MSW to 37 | create an enhanced version of MSW's `http` object. The enhanced version is 38 | designed to be almost identical to MSW in usage. To go beyond MSW's typing 39 | capabilities, OpenAPI-MSW provides optional helpers for an even better type-safe 40 | experience. Using the `http` object created with OpenAPI-MSW enables multiple 41 | type-safety and editor suggestion benefits: 42 | 43 | - **Paths:** Only accepts paths that are available for the current HTTP method 44 | - **Params**: Automatically typed with path parameters in the current path 45 | - **Query Params**: Automatically typed with the query parameters schema of the 46 | current path 47 | - **Request Body:** Automatically typed with the request-body schema of the 48 | current path 49 | - **Response:** Automatically forced to match an specified status-code, 50 | content-type, and response-body schema of the current path 51 | 52 | ```typescript 53 | import { HttpResponse } from "msw"; 54 | import { createOpenApiHttp } from "openapi-msw"; 55 | // 1. Import the paths from your OpenAPI schema definitions 56 | import type { paths } from "./your-openapi-schema"; 57 | 58 | // 2. Provide your paths definition to enable the above benefits during usage 59 | const http = createOpenApiHttp(); 60 | 61 | // TS only suggests available GET paths 62 | const getHandler = http.get("/resource/{id}", ({ params, response }) => { 63 | const id = params.id; 64 | return response(200).json({ id /* ... more response data */ }); 65 | }); 66 | 67 | // TS only suggests available POST paths 68 | const postHandler = http.post( 69 | "/resource", 70 | async ({ request, query, response }) => { 71 | const sortDir = query.get("sort"); 72 | 73 | const data = await request.json(); 74 | return response(201).json({ ...data /* ... more response data */ }); 75 | }, 76 | ); 77 | 78 | // TS shows an error when "/unknown" is not defined in the OpenAPI schema paths 79 | const otherHandler = http.get("/unknown", () => { 80 | return new HttpResponse(); 81 | }); 82 | ``` 83 | 84 | ### Provide a Base URL for Paths 85 | 86 | You can provide an optional base URL to `createOpenApiHttp`, which is prepended 87 | to all paths. This is especially useful when your application calls your API on 88 | a subpath or another domain. The value can be any string that is resolvable by 89 | MSW. 90 | 91 | ```typescript 92 | const http = createOpenApiHttp({ baseUrl: "/api/rest" }); 93 | 94 | // Requests will be matched by MSW against "/api/rest/resource" 95 | export const getHandler = http.get("/resource", () => { 96 | return HttpResponse.json(/* ... */); 97 | }); 98 | ``` 99 | 100 | ### Handling Unknown Paths 101 | 102 | MSW handlers can be very flexible with the ability to define wildcards (\*) in a 103 | path. This can be very useful for catch-all handlers but clashes with your 104 | OpenAPI spec, since it probably is not an endpoint of your API. To define 105 | handlers that are unknown to your OpenAPI spec, you can access the original 106 | `http` object through `http.untyped`. 107 | 108 | ```typescript 109 | const http = createOpenApiHttp(); 110 | 111 | // Fallback to MSW's original implementation and typings 112 | const catchAll = http.untyped.all("/resource/*", ({ params }) => { 113 | return HttpResponse.json(/* ... */); 114 | }); 115 | ``` 116 | 117 | Alternatively, you can import the original `http` object from MSW and use that 118 | one for unknown paths instead. 119 | 120 | ### Optional Helpers 121 | 122 | For an even better type-safe experience, OpenAPI-MSW provides optional helpers 123 | that are attached to MSW's resolver-info argument. Currently, the helper `query` 124 | is provided for type-safe access to query parameters. Furthermore, the helper 125 | `response` can be used for enhanced type-safety when creating HTTP responses. 126 | 127 | #### `query` Helper 128 | 129 | Type-safe wrapper around 130 | [`URLSearchParams`](https://developer.mozilla.org/docs/Web/API/URLSearchParams) 131 | that implements methods for reading query parameters. For the following example, 132 | imagine an OpenAPI specification that defines some query parameters: 133 | 134 | - **filter**: required string 135 | - **sort**: optional string enum of "desc" and "asc" 136 | - **sortBy**: optional array of strings 137 | 138 | ```typescript 139 | const http = createOpenApiHttp(); 140 | 141 | const handler = http.get("/query-example", ({ query }) => { 142 | const filter = query.get("filter"); // string 143 | const sort = query.get("sort"); // "asc" | "desc" | null 144 | const sortBy = query.getAll("sortBy"); // string[] 145 | 146 | // Supported methods from URLSearchParams: get(), getAll(), has(), size 147 | if (query.has("sort", "asc")) { 148 | /* ... */ 149 | } 150 | 151 | return HttpResponse.json({ 152 | /* ... */ 153 | }); 154 | }); 155 | ``` 156 | 157 | #### `response` Helper 158 | 159 | Type-safe response constructor that narrows allowed response bodies based on the 160 | chosen status code and content type. This helper enables granular type-safety 161 | for responses. Instead of being able to return any status code, `response` 162 | limits status codes, content types, and their response bodies to the 163 | combinations defined by the given OpenAPI spec. 164 | 165 | For the following example, imagine an OpenAPI specification that defines various 166 | responses for an endpoint: 167 | 168 | | Status Code | Content Type | Content | 169 | | :---------- | :----------------- | :--------------------- | 170 | | `200` | `application/json` | _Some Object Schema_ | 171 | | `200` | `text/plain` | Literal: "Hello World" | 172 | | `204` | Empty | | 173 | 174 | ```typescript 175 | const http = createOpenApiHttp(); 176 | 177 | const handler = http.get("/response-example", ({ response }) => { 178 | // Error: Status Code 204 only allows empty responses 179 | const invalidRes = response(204).text("Hello World"); 180 | 181 | // Error: Status Code 200 does not allow empty responses 182 | const invalidRes = response(200).empty(); 183 | 184 | // Error: Status Code 200 only allows "Hello World" as text 185 | const invalidRes = response(200).text("Some other string"); 186 | 187 | // No Error: This combination is part of the defined OpenAPI spec 188 | const validRes = response(204).empty(); 189 | 190 | // No Error: This combination is part of the defined OpenAPI spec 191 | const validRes = response(200).text("Hello World"); 192 | 193 | // No Error: This combination is part of the defined OpenAPI spec 194 | const validRes = response(200).json({ 195 | /* ... */ 196 | }); 197 | }); 198 | ``` 199 | 200 | ##### Wildcard Status Codes 201 | 202 | The OpenAPI specification allows the 203 | [definition of wildcard status codes](https://spec.openapis.org/oas/v3.1.0#patterned-fields-0), 204 | such as `"default"`, `"3XX"`, and `"5XX"`. OpenAPI-MSW's `response` helper 205 | supports using wildcards as status codes. When a wildcard is used, TypeScript 206 | requires you to provide a matching status code that will be used for the 207 | response. Allowed status codes are inferred by TS and suggested based on 208 | [RFC 9110](https://httpwg.org/specs/rfc9110.html#overview.of.status.codes). 209 | 210 | **Note:** The `"default"` wildcard is categorized as 211 | ["any error status code" in OpenAPI-TS](https://github.com/drwpow/openapi-typescript/blob/a7dbe90905e07921147a2239c0323d778d1a72de/packages/openapi-typescript-helpers/index.d.ts#L8). 212 | To align with this assumption, OpenAPI-MSW only allows matching `"4XX"` and 213 | `"5XX"` status codes for the response if the `"default"` wildcard is used. 214 | 215 | ```typescript 216 | const http = createOpenApiHttp(); 217 | 218 | const handler = http.get("/wildcard-status-code-example", ({ response }) => { 219 | // Error: A wildcards is used but no status code provided 220 | const invalidRes = response("5XX").text("Fatal Error"); 221 | 222 | // Error: Provided status code does not match the used wildcard 223 | const invalidRes = response("5XX").text("Fatal Error", { status: 403 }); 224 | 225 | // Error: Provided status code is not defined in RFC 9110 226 | const invalidRes = response("5XX").text("Fatal Error", { status: 520 }); 227 | 228 | // No Error: Provided status code matches the used wildcard 229 | const validRes = response("5XX").text("Fatal Error", { status: 503 }); 230 | 231 | // No Error: "default" wildcard allows 5XX and 4XX status codes 232 | const validRes = response("default").text("Fatal Error", { status: 507 }); 233 | }); 234 | ``` 235 | 236 | ##### Untyped Response Fallback 237 | 238 | Sometimes an OpenAPI spec might not define all status codes that are actually 239 | returned by the implementing API. This can be quite common for server error 240 | responses (5XX). Nonetheless, still being able to mock those with MSW through 241 | otherwise fully typed http-handlers is really helpful. OpenAPI-MSW supports this 242 | scenario with an `untyped` wrapper on the `response` helper, which type-casts 243 | any response into an allowed response. 244 | 245 | ```typescript 246 | const http = createOpenApiHttp(); 247 | 248 | const handler = http.get("/untyped-response-example", ({ response }) => { 249 | // Any response wrapped with `untyped` can be returned regardless 250 | // of the expected response body. 251 | return response.untyped( 252 | HttpResponse.json({ message: "Teapot" }, { status: 418 }), 253 | ); 254 | }); 255 | ``` 256 | 257 | ### Advanced: Utility Types 258 | 259 | OpenAPI-MSW exports utility types that can help you with creating type-safe 260 | functionality around OpenAPI-MSW. These utility types are centered around the 261 | request handlers exposed through the `http` object. 262 | 263 | ```typescript 264 | import { 265 | createOpenApiHttp, 266 | type PathsFor, 267 | type RequestBodyFor, 268 | type ResponseBodyFor, 269 | } from "openapi-msw"; 270 | 271 | const http = createOpenApiHttp(); 272 | 273 | // A union of all possible GET paths. 274 | type Paths = PathsFor; 275 | 276 | // The request body for POST /tasks. 277 | type RequestBody = RequestBodyFor; 278 | 279 | // The response body for GET /tasks. 280 | type ResponseBody = ResponseBodyFor; 281 | ``` 282 | 283 | ## License 284 | 285 | This package is published under the [MIT license](./LICENSE). 286 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import { defineConfig, globalIgnores } from "eslint/config"; 3 | import ts from "typescript-eslint"; 4 | 5 | export default defineConfig( 6 | globalIgnores(["coverage", "cjs", "dist", "test/fixtures/*.ts"]), 7 | js.configs.recommended, 8 | ts.configs.recommended, 9 | ts.configs.stylistic, 10 | { 11 | files: ["test/**/*.test-d.ts"], 12 | // Type tests commonly create a variable which is only used for type checks. 13 | rules: { "@typescript-eslint/no-unused-vars": "off" }, 14 | }, 15 | { 16 | rules: { 17 | "@typescript-eslint/no-empty-object-type": [ 18 | "error", 19 | // Often types are computed and expanded in editor previews, 20 | // which can lead to verbose and hard-to-understand type signatures. 21 | // Interfaces keep their name in previews, which can be used to clarify 22 | // previews by using interfaces that only extend a type. 23 | { allowInterfaces: "with-single-extends" }, 24 | ], 25 | }, 26 | }, 27 | ); 28 | -------------------------------------------------------------------------------- /exports/main.ts: -------------------------------------------------------------------------------- 1 | export type { AnyApiSpec, HttpMethod } from "../src/api-spec.js"; 2 | 3 | export { 4 | createOpenApiHttp, 5 | type HttpOptions, 6 | type OpenApiHttpHandlers, 7 | type OpenApiHttpRequestHandler, 8 | } from "../src/openapi-http.js"; 9 | 10 | export type { 11 | ResponseResolver, 12 | ResponseResolverInfo, 13 | } from "../src/response-resolver.js"; 14 | 15 | export type { 16 | AnyOpenApiHttpRequestHandler, 17 | PathsFor, 18 | RequestBodyFor, 19 | ResponseBodyFor, 20 | } from "../src/openapi-http-utilities.js"; 21 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/package.json", 3 | "name": "openapi-msw", 4 | "type": "module", 5 | "version": "1.2.0", 6 | "license": "MIT", 7 | "author": "Christoph Fricke ", 8 | "description": "Tiny, type-safe wrapper around MSW for type inference from OpenAPI schemas.", 9 | "repository": "github:christoph-fricke/openapi-msw", 10 | "files": [ 11 | "cjs", 12 | "dist", 13 | "CHANGELOG.md", 14 | "README.md", 15 | "LICENSE" 16 | ], 17 | "sideEffects": false, 18 | "types": "./dist/exports/main.d.ts", 19 | "main": "./cjs/exports/main.js", 20 | "module": "./dist/exports/main.js", 21 | "exports": { 22 | ".": { 23 | "types": { 24 | "import": "./dist/exports/main.d.ts", 25 | "require": "./cjs/exports/main.d.ts" 26 | }, 27 | "import": "./dist/exports/main.js", 28 | "require": "./cjs/exports/main.js" 29 | } 30 | }, 31 | "scripts": { 32 | "prepare": "lefthook install", 33 | "build": "npm run build:esm && npm run build:cjs", 34 | "build:esm": "rimraf ./dist && tsc -p tsconfig.build.json", 35 | "build:cjs": "rimraf ./cjs && tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > ./cjs/package.json", 36 | "format": "prettier --write .", 37 | "lint": "eslint . && prettier -c .", 38 | "generate": "openapi-typescript -c ./test/fixtures/.redocly.yml", 39 | "test:unit": "vitest --project=unit", 40 | "test:int": "vitest --project=integration", 41 | "version": "changeset version && npm i", 42 | "release": "changeset publish" 43 | }, 44 | "peerDependencies": { 45 | "msw": "^2.7.0" 46 | }, 47 | "devDependencies": { 48 | "@changesets/changelog-github": "^0.5.1", 49 | "@changesets/cli": "^2.28.1", 50 | "eslint": "^9.22.0", 51 | "lefthook": "^1.11.3", 52 | "openapi-typescript": "^7.6.1", 53 | "prettier": "^3.5.3", 54 | "prettier-plugin-organize-imports": "^4.1.0", 55 | "rimraf": "^6.0.1", 56 | "typescript": "^5.8.2", 57 | "typescript-eslint": "^8.26.1", 58 | "vitest": "^3.0.8" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | export default { 3 | plugins: ["prettier-plugin-organize-imports"], 4 | proseWrap: "always", 5 | overrides: [ 6 | { 7 | files: [".changeset/*.md", "CHANGELOG.md"], 8 | options: { 9 | proseWrap: "never", 10 | }, 11 | }, 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /src/api-spec.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ConvertToStringified, 3 | MapToValues, 4 | ResolvedObjectUnion, 5 | } from "./type-utils.js"; 6 | 7 | /** Base type that any api spec should extend. */ 8 | export type AnyApiSpec = NonNullable; 9 | 10 | /** Intersection of HTTP methods that are supported by both OpenAPI-TS and MSW. */ 11 | export type HttpMethod = 12 | | "get" 13 | | "put" 14 | | "post" 15 | | "delete" 16 | | "options" 17 | | "head" 18 | | "patch"; 19 | 20 | /** Returns a union of all paths that exists in an api spec for a given method. */ 21 | export type PathsForMethod< 22 | ApiSpec extends AnyApiSpec, 23 | Method extends HttpMethod, 24 | > = { 25 | [Path in keyof ApiSpec]: ApiSpec[Path] extends Record 26 | ? Path 27 | : never; 28 | }[keyof ApiSpec]; 29 | 30 | /** Extract the path params of a given path and method from an api spec. */ 31 | export type PathParams< 32 | ApiSpec extends AnyApiSpec, 33 | Path extends keyof ApiSpec, 34 | Method extends HttpMethod, 35 | > = Method extends keyof ApiSpec[Path] 36 | ? ApiSpec[Path][Method] extends { 37 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 38 | parameters: { path: any }; 39 | } 40 | ? ConvertToStringified 41 | : never 42 | : never; 43 | 44 | /** Extract the query params of a given path and method from an api spec. */ 45 | export type QueryParams< 46 | ApiSpec extends AnyApiSpec, 47 | Path extends keyof ApiSpec, 48 | Method extends HttpMethod, 49 | > = Method extends keyof ApiSpec[Path] 50 | ? ApiSpec[Path][Method] extends { 51 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 52 | parameters: { query?: any }; 53 | } 54 | ? ConvertToStringified< 55 | StrictQueryParams< 56 | Required["query"] 57 | > 58 | > 59 | : never 60 | : never; 61 | 62 | /** Ensures that query params are not usable in case no query params are specified (never). */ 63 | type StrictQueryParams = [Params] extends [never] 64 | ? NonNullable 65 | : Params; 66 | 67 | /** 68 | * Extract a request map for a given path and method from an api spec. 69 | * A request map has the shape of (media-type -> body). 70 | */ 71 | export type RequestMap< 72 | ApiSpec extends AnyApiSpec, 73 | Path extends keyof ApiSpec, 74 | Method extends HttpMethod, 75 | > = Method extends keyof ApiSpec[Path] 76 | ? ApiSpec[Path][Method] extends { 77 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 78 | requestBody?: any; 79 | } 80 | ? undefined extends ApiSpec[Path][Method]["requestBody"] 81 | ? Partial["content"]> 82 | : ApiSpec[Path][Method]["requestBody"]["content"] 83 | : never 84 | : never; 85 | 86 | /** Extract the request body of a given path and method from an api spec. */ 87 | export type RequestBody< 88 | ApiSpec extends AnyApiSpec, 89 | Path extends keyof ApiSpec, 90 | Method extends HttpMethod, 91 | > = MapToValues>; 92 | 93 | /** 94 | * Extract a response map for a given path and method from an api spec. 95 | * A response map has the shape of (status -> media-type -> body). 96 | */ 97 | export type ResponseMap< 98 | ApiSpec extends AnyApiSpec, 99 | Path extends keyof ApiSpec, 100 | Method extends HttpMethod, 101 | > = Method extends keyof ApiSpec[Path] 102 | ? ApiSpec[Path][Method] extends { 103 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 104 | responses: any; 105 | } 106 | ? { 107 | [Status in keyof ApiSpec[Path][Method]["responses"]]: ConvertContent< 108 | ApiSpec[Path][Method]["responses"][Status] 109 | >["content"]; 110 | } 111 | : never 112 | : never; 113 | 114 | /** Extract the response body of a given path and method from an api spec. */ 115 | export type ResponseBody< 116 | ApiSpec extends AnyApiSpec, 117 | Path extends keyof ApiSpec, 118 | Method extends HttpMethod, 119 | > = MapToValues< 120 | ResolvedObjectUnion>> 121 | >; 122 | 123 | /** 124 | * OpenAPI-TS generates "no content" with `content?: never`. 125 | * However, `new Response().body` is `null` and strictly typing no-content in 126 | * MSW requires `null`. Therefore, this helper ensures that "no-content" 127 | * can be mapped to null when typing the response body. 128 | */ 129 | type ConvertContent = 130 | Required extends { content: never } 131 | ? { content: { "no-content": null } } 132 | : // eslint-disable-next-line @typescript-eslint/no-explicit-any 133 | Content extends { content: any } 134 | ? Content 135 | : never; 136 | -------------------------------------------------------------------------------- /src/http-status-wildcard.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Mapping of status wildcards allowed in the OpenAPI specification to defined 3 | * HTTP status codes. 4 | * 5 | * @see Allowed Wildcards: https://spec.openapis.org/oas/v3.1.0#patterned-fields-0 6 | * @see Default Code: https://spec.openapis.org/oas/v3.1.0#fixed-fields-13 7 | * @see Specified Status Codes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status 8 | */ 9 | export interface Wildcard { 10 | "1XX": 100 | 101 | 102 | 103; 11 | "2XX": 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226; 12 | "3XX": 300 | 301 | 302 | 303 | 304 | 307 | 308; 13 | "5XX": 500 | 501 | 502 | 503 | 504 | 507 | 508 | 510 | 511; 14 | "4XX": 15 | | 400 16 | | 401 17 | | 402 18 | | 403 19 | | 404 20 | | 405 21 | | 406 22 | | 407 23 | | 408 24 | | 409 25 | | 410 26 | | 411 27 | | 412 28 | | 413 29 | | 414 30 | | 415 31 | | 416 32 | | 417 33 | | 418 34 | | 421 35 | | 422 36 | | 423 37 | | 424 38 | | 425 39 | | 426 40 | | 428 41 | | 429 42 | | 431 43 | | 451; 44 | // Follows OpenAPI-TS in considering "default" as only being 4XX or 5XX. 45 | default: Wildcard["4XX"] | Wildcard["5XX"]; 46 | } 47 | -------------------------------------------------------------------------------- /src/openapi-http-utilities.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AnyApiSpec, 3 | HttpMethod, 4 | PathsForMethod, 5 | RequestBody, 6 | ResponseBody, 7 | } from "./api-spec.js"; 8 | import type { OpenApiHttpRequestHandler } from "./openapi-http.js"; 9 | 10 | /** 11 | * Base type that generic {@link OpenApiHttpRequestHandler | OpenApiHttpRequestHandlers} 12 | * can extend. 13 | */ 14 | export type AnyOpenApiHttpRequestHandler = OpenApiHttpRequestHandler< 15 | AnyApiSpec, 16 | HttpMethod 17 | >; 18 | 19 | /** 20 | * Extracts a union of all paths that can be provided to the given request handler. 21 | * 22 | * @example 23 | * const http = createOpenApiHttp(); 24 | * 25 | * type Paths = PathsFor; 26 | */ 27 | export type PathsFor = 28 | Handler extends OpenApiHttpRequestHandler 29 | ? PathsForMethod 30 | : never; 31 | 32 | /** 33 | * Extracts the request body of a specific path for the given request handler. 34 | * 35 | * @example 36 | * const http = createOpenApiHttp(); 37 | * 38 | * type RequestBody = RequestBodyFor; 39 | */ 40 | export type RequestBodyFor< 41 | Handler extends AnyOpenApiHttpRequestHandler, 42 | Path extends PathsFor, 43 | > = 44 | Handler extends OpenApiHttpRequestHandler 45 | ? RequestBody 46 | : never; 47 | 48 | /** 49 | * Extracts the response body of a specific path for the given request handler. 50 | * 51 | * @example 52 | * const http = createOpenApiHttp(); 53 | * 54 | * type ResponseBody = ResponseBodyFor; 55 | */ 56 | export type ResponseBodyFor< 57 | Handler extends AnyOpenApiHttpRequestHandler, 58 | Path extends PathsFor, 59 | > = 60 | Handler extends OpenApiHttpRequestHandler 61 | ? ResponseBody 62 | : never; 63 | -------------------------------------------------------------------------------- /src/openapi-http.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { http as mswHttp } from "msw"; 3 | import { describe, expect, it, vi } from "vitest"; 4 | import type { HttpMethod } from "./api-spec.js"; 5 | import { createOpenApiHttp } from "./openapi-http.js"; 6 | 7 | const methods: HttpMethod[] = [ 8 | "get", 9 | "put", 10 | "post", 11 | "delete", 12 | "options", 13 | "head", 14 | "patch", 15 | ]; 16 | 17 | describe(createOpenApiHttp, () => { 18 | it("should create an http handlers object", () => { 19 | const http = createOpenApiHttp(); 20 | 21 | expect(http).toBeTypeOf("object"); 22 | for (const method of methods) { 23 | expect(http[method]).toBeTypeOf("function"); 24 | } 25 | }); 26 | 27 | it("should include the original MSW methods in its return type", () => { 28 | const http = createOpenApiHttp(); 29 | 30 | expect(http.untyped).toBe(mswHttp); 31 | }); 32 | }); 33 | 34 | describe.each(methods)("openapi %s http handlers", (method) => { 35 | it("should forward its arguments to MSW", () => { 36 | const spy = vi.spyOn(mswHttp, method); 37 | const resolver = vi.fn(); 38 | 39 | const http = createOpenApiHttp(); 40 | http[method]("/test", resolver, { once: false }); 41 | 42 | expect(spy).toHaveBeenCalledOnce(); 43 | expect(spy).toHaveBeenCalledWith("/test", expect.any(Function), { 44 | once: false, 45 | }); 46 | }); 47 | 48 | it("should convert openapi paths to MSW compatible paths", () => { 49 | const spy = vi.spyOn(mswHttp, method); 50 | const resolver = vi.fn(); 51 | 52 | const http = createOpenApiHttp(); 53 | http[method]("/test/{id}", resolver); 54 | 55 | expect(spy).toHaveBeenCalledOnce(); 56 | expect(spy).toHaveBeenCalledWith( 57 | "/test/:id", 58 | expect.any(Function), 59 | undefined, 60 | ); 61 | }); 62 | 63 | it("should prepend a configured baseUrl to the path for MSW", () => { 64 | const spy = vi.spyOn(mswHttp, method); 65 | const resolver = vi.fn(); 66 | 67 | const http = createOpenApiHttp({ baseUrl: "*/api/rest" }); 68 | http[method]("/test", resolver); 69 | 70 | expect(spy).toHaveBeenCalledOnce(); 71 | expect(spy).toHaveBeenCalledWith( 72 | "*/api/rest/test", 73 | expect.any(Function), 74 | undefined, 75 | ); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /src/openapi-http.ts: -------------------------------------------------------------------------------- 1 | import { http, type HttpHandler, type RequestHandlerOptions } from "msw"; 2 | import type { AnyApiSpec, HttpMethod, PathsForMethod } from "./api-spec.js"; 3 | import { convertToColonPath } from "./path-mapping.js"; 4 | import { 5 | createResolverWrapper, 6 | type ResponseResolver, 7 | } from "./response-resolver.js"; 8 | 9 | /** HTTP handler factory with type inference for provided api paths. */ 10 | export type OpenApiHttpRequestHandler< 11 | ApiSpec extends AnyApiSpec, 12 | Method extends HttpMethod, 13 | > = >( 14 | path: Path, 15 | resolver: ResponseResolver, 16 | options?: RequestHandlerOptions, 17 | ) => HttpHandler; 18 | 19 | function createHttpWrapper< 20 | ApiSpec extends AnyApiSpec, 21 | Method extends HttpMethod, 22 | >( 23 | method: Method, 24 | httpOptions?: HttpOptions, 25 | ): OpenApiHttpRequestHandler { 26 | return (path, resolver, options) => { 27 | const mswPath = convertToColonPath(path as string, httpOptions?.baseUrl); 28 | const mswResolver = createResolverWrapper(resolver); 29 | 30 | return http[method](mswPath, mswResolver, options); 31 | }; 32 | } 33 | 34 | /** Collection of enhanced HTTP handler factories for each available HTTP Method. */ 35 | export type OpenApiHttpHandlers = { 36 | [Method in HttpMethod]: OpenApiHttpRequestHandler; 37 | } & { untyped: typeof http }; 38 | 39 | export interface HttpOptions { 40 | /** Optional baseUrl that is prepended to the `path` of each HTTP handler. */ 41 | baseUrl?: string; 42 | } 43 | 44 | /** 45 | * Creates a wrapper around MSW's {@link http} object, which is enhanced with 46 | * type inference from the provided OpenAPI-TS `paths` definition. 47 | * 48 | * @param options Additional options that are used by all defined HTTP handlers. 49 | * 50 | * @example 51 | * import { HttpResponse } from "msw"; 52 | * import { createOpenApiHttp } from "openapi-msw"; 53 | * // 1. Import the paths from your OpenAPI schema definitions 54 | * import type { paths } from "./your-openapi-schema"; 55 | * 56 | * // 2. Provide your paths definition to enable type inference in HTTP handlers 57 | * const http = createOpenApiHttp(); 58 | * 59 | * // TS only suggests available GET paths 60 | * const getHandler = http.get("/resource/{id}", ({ params }) => { 61 | * const id = params.id; 62 | * return HttpResponse.json({ id, other: "..." }); 63 | * }); 64 | */ 65 | export function createOpenApiHttp( 66 | options?: HttpOptions, 67 | ): OpenApiHttpHandlers { 68 | return { 69 | get: createHttpWrapper("get", options), 70 | put: createHttpWrapper("put", options), 71 | post: createHttpWrapper("post", options), 72 | delete: createHttpWrapper("delete", options), 73 | options: createHttpWrapper("options", options), 74 | head: createHttpWrapper("head", options), 75 | patch: createHttpWrapper("patch", options), 76 | untyped: http, 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /src/path-mapping.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { convertToColonPath } from "./path-mapping.js"; 3 | 4 | describe(convertToColonPath, () => { 5 | it("should leave paths with no path fragments untouched", () => { 6 | const result = convertToColonPath("/users"); 7 | 8 | expect(result).toBe("/users"); 9 | }); 10 | 11 | it("should convert a path fragment to colon convention", () => { 12 | const result = convertToColonPath("/users/{id}"); 13 | 14 | expect(result).toBe("/users/:id"); 15 | }); 16 | 17 | it("should convert all fragments in a path to colon convention", () => { 18 | const result = convertToColonPath("/users/{userId}/posts/{postIds}"); 19 | 20 | expect(result).toBe("/users/:userId/posts/:postIds"); 21 | }); 22 | 23 | it("should append a baseUrl to the path when provided", () => { 24 | const noBaseUrl = convertToColonPath("/users"); 25 | const withBaseUrl = convertToColonPath("/users", "https://localhost:3000"); 26 | 27 | expect(noBaseUrl).toBe("/users"); 28 | expect(withBaseUrl).toBe("https://localhost:3000/users"); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/path-mapping.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a OpenAPI path fragment convention to the colon convention that is 3 | * commonly used in Node.js and also MSW. 4 | * 5 | * @example /users/{id} --> /users/:id 6 | */ 7 | export function convertToColonPath(path: string, baseUrl?: string): string { 8 | const resolvedPath = path.replaceAll("{", ":").replaceAll("}", ""); 9 | if (!baseUrl) return resolvedPath; 10 | 11 | return baseUrl + resolvedPath; 12 | } 13 | -------------------------------------------------------------------------------- /src/query-params.ts: -------------------------------------------------------------------------------- 1 | import type { OptionalKeys } from "./type-utils.js"; 2 | 3 | /** Return values for getting the first value of a query param. */ 4 | type ParamValuesGet = { 5 | [Name in keyof Params]-?: Name extends OptionalKeys 6 | ? Params[Name] | null 7 | : Params[Name]; 8 | }; 9 | 10 | /** Return values for getting all values of a query param. */ 11 | type ParamValuesGetAll = { 12 | [Name in keyof Params]-?: Required[Name][]; 13 | }; 14 | 15 | /** 16 | * Wrapper around the search params of a request that offers methods for 17 | * querying search params with enhanced type-safety from OpenAPI-TS. 18 | */ 19 | export class QueryParams { 20 | #searchParams: URLSearchParams; 21 | constructor(request: Request) { 22 | this.#searchParams = new URL(request.url).searchParams; 23 | } 24 | 25 | /** 26 | * Wraps around {@link URLSearchParams.size}. 27 | * 28 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/size) 29 | */ 30 | get size(): number { 31 | return this.#searchParams.size; 32 | } 33 | 34 | /** 35 | * Wraps around {@link URLSearchParams.get} with type inference from the 36 | * provided OpenAPI-TS `paths` definition. 37 | * 38 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/get) 39 | */ 40 | get(name: Name): ParamValuesGet[Name] { 41 | const value = this.#searchParams.get(name as string); 42 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 43 | return value as any; 44 | } 45 | 46 | /** 47 | * Wraps around {@link URLSearchParams.getAll} with type inference from the 48 | * provided OpenAPI-TS `paths` definition. 49 | * 50 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/getAll) 51 | */ 52 | getAll( 53 | name: Name, 54 | ): ParamValuesGetAll[Name] { 55 | const values = this.#searchParams.getAll(name as string); 56 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 57 | return values as any; 58 | } 59 | 60 | /** 61 | * Wraps around {@link URLSearchParams.has} with type inference from the 62 | * provided OpenAPI-TS `paths` definition. 63 | * 64 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/has) 65 | */ 66 | has(name: Name, value?: Params[Name]): boolean { 67 | return this.#searchParams.has(name as string, value as string | undefined); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/request.ts: -------------------------------------------------------------------------------- 1 | import type { JSONLike, TextLike } from "./type-utils.js"; 2 | 3 | /** A type-safe request helper that enhances native body methods based on the given OpenAPI spec. */ 4 | export interface OpenApiRequest extends Request { 5 | clone(): OpenApiRequest; 6 | json(): JSONLike extends never 7 | ? never 8 | : Promise>; 9 | text(): TextLike extends never 10 | ? never 11 | : TextLike extends string 12 | ? Promise> 13 | : never; 14 | } 15 | -------------------------------------------------------------------------------- /src/response-resolver.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AsyncResponseResolverReturnType, 3 | HttpResponseResolver, 4 | } from "msw"; 5 | import type { 6 | AnyApiSpec, 7 | HttpMethod, 8 | PathParams, 9 | QueryParams, 10 | RequestBody, 11 | RequestMap, 12 | ResponseBody, 13 | ResponseMap, 14 | } from "./api-spec.js"; 15 | import { QueryParams as QueryParamsUtil } from "./query-params.js"; 16 | import type { OpenApiRequest } from "./request.js"; 17 | import { createResponseHelper, type OpenApiResponse } from "./response.js"; 18 | 19 | /** Response resolver that gets provided to HTTP handler factories. */ 20 | export type ResponseResolver< 21 | ApiSpec extends AnyApiSpec, 22 | Path extends keyof ApiSpec, 23 | Method extends HttpMethod, 24 | > = ( 25 | info: ResponseResolverInfo, 26 | ) => AsyncResponseResolverReturnType>; 27 | 28 | /** Response resolver info that extends MSW's resolver info with additional functionality. */ 29 | export interface ResponseResolverInfo< 30 | ApiSpec extends AnyApiSpec, 31 | Path extends keyof ApiSpec, 32 | Method extends HttpMethod, 33 | > extends MSWResponseResolverInfo { 34 | /** Standard request with enhanced typing for body methods based on the given OpenAPI spec. */ 35 | request: OpenApiRequest>; 36 | 37 | /** 38 | * Type-safe wrapper around {@link URLSearchParams} that implements methods for 39 | * reading query parameters. 40 | * 41 | * @example 42 | * const handler = http.get("/query-example", ({ query }) => { 43 | * const filter = query.get("filter"); 44 | * const sortBy = query.getAll("sortBy"); 45 | * 46 | * if (query.has("sort", "asc")) { ... } 47 | * 48 | * return HttpResponse.json({ ... }); 49 | * }); 50 | */ 51 | query: QueryParamsUtil>; 52 | 53 | /** 54 | * A type-safe response helper that narrows allowed status codes and content types 55 | * based on the given OpenAPI spec. The response body is further narrowed to 56 | * the match the selected status code and content type. 57 | * 58 | * If a wildcard status code is chosen, a specific status code for the response 59 | * must be provided in the {@linkcode ResponseInit} argument. All status codes 60 | * allowed by the wildcard are inferred. 61 | * 62 | * A fallback for returning any response without casting is provided 63 | * through `response.untyped(...)`. 64 | * 65 | * @example 66 | * const handler = http.get("/response-example", ({ response }) => { 67 | * return response(200).json({ id: 123 }); 68 | * }); 69 | * 70 | * const empty = http.get("/response-example", ({ response }) => { 71 | * return response(204).empty(); 72 | * }); 73 | * 74 | * const wildcard = http.get("/response-example", ({ response }) => { 75 | * return response("5XX").text("Unexpected Error", { status: 501 }); 76 | * }); 77 | * 78 | * const fallback = http.get("/response-example", ({ response }) => { 79 | * return response.untyped(new Response("Hello")); 80 | * }); 81 | */ 82 | response: OpenApiResponse< 83 | ResponseMap, 84 | ResponseBody 85 | >; 86 | } 87 | 88 | /** Wraps MSW's resolver function to provide additional info to a given resolver. */ 89 | export function createResolverWrapper< 90 | ApiSpec extends AnyApiSpec, 91 | Path extends keyof ApiSpec, 92 | Method extends HttpMethod, 93 | >( 94 | resolver: ResponseResolver, 95 | ): MSWResponseResolver { 96 | return (info) => { 97 | return resolver({ 98 | ...info, 99 | request: info.request as OpenApiRequest< 100 | RequestMap 101 | >, 102 | query: new QueryParamsUtil(info.request), 103 | response: createResponseHelper(), 104 | }); 105 | }; 106 | } 107 | 108 | /** MSW response resolver info that is made type-safe through an api spec. */ 109 | type MSWResponseResolverInfo< 110 | ApiSpec extends AnyApiSpec, 111 | Path extends keyof ApiSpec, 112 | Method extends HttpMethod, 113 | > = Parameters>[0]; 114 | 115 | /** MSW response resolver function that is made type-safe through an api spec. */ 116 | export type MSWResponseResolver< 117 | ApiSpec extends AnyApiSpec, 118 | Path extends keyof ApiSpec, 119 | Method extends HttpMethod, 120 | > = HttpResponseResolver< 121 | PathParams, 122 | RequestBody, 123 | ResponseBody 124 | >; 125 | -------------------------------------------------------------------------------- /src/response.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpResponse, 3 | type DefaultBodyType, 4 | type HttpResponseInit, 5 | type StrictResponse, 6 | } from "msw"; 7 | import type { Wildcard } from "./http-status-wildcard.js"; 8 | import type { JSONLike, NoContent, TextLike } from "./type-utils.js"; 9 | 10 | /** 11 | * Requires or removes the status code from {@linkcode HttpResponseInit} depending 12 | * on the chosen OpenAPI status code. When the status is a wildcard, a specific 13 | * status code must be provided. 14 | */ 15 | type DynamicResponseInit = Status extends keyof Wildcard 16 | ? ResponseInitForWildcard 17 | : ResponseInitNoStatus | void; 18 | 19 | interface ResponseInitNoStatus extends Omit {} 20 | interface ResponseInitForWildcard 21 | extends ResponseInitNoStatus { 22 | status: Wildcard[Key]; 23 | } 24 | 25 | /** Creates a type-safe text response, which may require an additional status code. */ 26 | type TextResponse = ( 27 | body: ResponseBody extends string ? ResponseBody : never, 28 | init: DynamicResponseInit, 29 | ) => StrictResponse; 30 | 31 | /** Creates a type-safe json response, which may require an additional status code. */ 32 | type JsonResponse = ( 33 | body: ResponseBody extends DefaultBodyType ? ResponseBody : never, 34 | init: DynamicResponseInit, 35 | ) => StrictResponse; 36 | 37 | /** Creates a type-safe empty response, which may require an additional status code. */ 38 | type EmptyResponse = ( 39 | init: DynamicResponseInit, 40 | ) => StrictResponse; 41 | 42 | /** 43 | * A type-safe response helper that narrows available status codes and content types, 44 | * based on the given OpenAPI spec. The response body is specifically narrowed to 45 | * the specified status code and content type. 46 | */ 47 | export interface OpenApiResponse< 48 | ResponseMap, 49 | ExpectedResponseBody extends DefaultBodyType, 50 | > { 51 | ( 52 | status: Status, 53 | ): { 54 | text: TextLike extends never 55 | ? unknown 56 | : TextResponse, Status>; 57 | 58 | json: JSONLike extends never 59 | ? unknown 60 | : JsonResponse, Status>; 61 | 62 | empty: NoContent extends never 63 | ? unknown 64 | : EmptyResponse; 65 | }; 66 | untyped(response: Response): StrictResponse; 67 | } 68 | 69 | export function createResponseHelper< 70 | ResponseMap, 71 | ExpectedResponseBody extends DefaultBodyType, 72 | >(): OpenApiResponse { 73 | const response: OpenApiResponse = ( 74 | status, 75 | ) => { 76 | const text: TextResponse< 77 | TextLike, 78 | typeof status 79 | > = (body, init) => { 80 | return HttpResponse.text(body, { 81 | status: status as number, 82 | ...init, 83 | }); 84 | }; 85 | 86 | const json: JsonResponse< 87 | JSONLike, 88 | typeof status 89 | > = (body, init) => { 90 | return HttpResponse.json(body, { status: status as number, ...init }); 91 | }; 92 | 93 | const empty: EmptyResponse = (init) => { 94 | const headers = new Headers(init?.headers); 95 | if (!headers.has("content-length")) headers.set("content-length", "0"); 96 | 97 | return new HttpResponse(null, { 98 | status: status as number, 99 | ...init, 100 | headers, 101 | }) as StrictResponse; 102 | }; 103 | 104 | return { text, json, empty }; 105 | }; 106 | 107 | response.untyped = (response) => { 108 | return response as StrictResponse; 109 | }; 110 | 111 | return response; 112 | } 113 | -------------------------------------------------------------------------------- /src/type-utils.ts: -------------------------------------------------------------------------------- 1 | export type FilterKeys = Obj[keyof Obj & Matchers]; 2 | 3 | /** Returns any JSON mime type. Works with types like "application/problem+json". */ 4 | export type JSONLike = FilterKeys; 5 | 6 | /** Returns any TEXT mime type. */ 7 | export type TextLike = FilterKeys; 8 | 9 | /** Returns any special "no-content" entry. Special no-content entries are added by `ConvertContent`. */ 10 | export type NoContent = FilterKeys; 11 | 12 | /** 13 | * Converts a type to string while preserving string literal types. 14 | * {@link Array}s are unboxed to their stringified value. 15 | */ 16 | export type Stringify = Value extends (infer Type)[] 17 | ? Type extends string 18 | ? Type 19 | : string 20 | : Value extends string 21 | ? Value 22 | : string; 23 | 24 | /** Converts a object values to their {@link Stringify} value. */ 25 | export type ConvertToStringified = { 26 | [Name in keyof Params]: Stringify[Name]>; 27 | }; 28 | 29 | /** Returns a union of all property keys that are optional in the given object. */ 30 | export type OptionalKeys = { 31 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 32 | [K in keyof O]-?: {} extends Pick ? K : never; 33 | }[keyof O]; 34 | 35 | /** 36 | * Combines a union of objects into a single object. 37 | * This is useful for types like `keyof ({ a: string } | { b: string })`, 38 | * which is `never` without {@link ResolvedObjectUnion}. 39 | * 40 | * A use-case example of such situation is mapping different media-types that 41 | * split across multiple status codes. 42 | * 43 | * @see https://www.steveruiz.me/posts/smooshed-object-union 44 | */ 45 | export type ResolvedObjectUnion = { 46 | [K in T extends infer P ? keyof P : never]: T extends infer P 47 | ? K extends keyof P 48 | ? P[K] 49 | : never 50 | : never; 51 | }; 52 | 53 | /** Maps an object into a union of all its inner values. */ 54 | export type MapToValues = Obj[keyof Obj]; 55 | -------------------------------------------------------------------------------- /test/fixtures/.redocly.yml: -------------------------------------------------------------------------------- 1 | apis: 2 | http-methods: 3 | root: ./http-methods.api.yml 4 | x-openapi-ts: 5 | output: ./http-methods.api.ts 6 | http-utilities: 7 | root: ./http-utilities.api.yml 8 | x-openapi-ts: 9 | output: ./http-utilities.api.ts 10 | no-content: 11 | root: ./no-content.api.yml 12 | x-openapi-ts: 13 | output: ./no-content.api.ts 14 | options: 15 | root: ./options.api.yml 16 | x-openapi-ts: 17 | output: ./options.api.ts 18 | path-fragments: 19 | root: ./path-fragments.api.yml 20 | x-openapi-ts: 21 | output: ./path-fragments.api.ts 22 | query-params: 23 | root: ./query-params.api.yml 24 | x-openapi-ts: 25 | output: ./query-params.api.ts 26 | request-body: 27 | root: ./request-body.api.yml 28 | x-openapi-ts: 29 | output: ./request-body.api.ts 30 | response-content: 31 | root: ./response-content.api.yml 32 | x-openapi-ts: 33 | output: ./response-content.api.ts 34 | -------------------------------------------------------------------------------- /test/fixtures/http-methods.api.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | info: 3 | title: Http Methods API 4 | version: 1.0.0 5 | servers: 6 | - url: http://localhost:3000 7 | paths: 8 | /resource: 9 | get: 10 | summary: Get Resource 11 | operationId: getResource 12 | responses: 13 | 200: 14 | description: Success 15 | content: 16 | application/json: 17 | schema: 18 | $ref: "#/components/schemas/Resource" 19 | post: 20 | summary: Create Resource 21 | operationId: createResource 22 | responses: 23 | 201: 24 | description: Created 25 | put: 26 | summary: Replace Resource 27 | operationId: replaceResource 28 | responses: 29 | 200: 30 | description: Success 31 | content: 32 | application/json: 33 | schema: 34 | $ref: "#/components/schemas/Resource" 35 | patch: 36 | summary: Update Resource 37 | operationId: updateResource 38 | responses: 39 | 200: 40 | description: Success 41 | content: 42 | application/json: 43 | schema: 44 | $ref: "#/components/schemas/Resource" 45 | delete: 46 | summary: Delete Resource 47 | operationId: deleteResource 48 | responses: 49 | 204: 50 | description: No Content 51 | options: 52 | summary: Options Resource 53 | operationId: optionsResource 54 | responses: 55 | 200: 56 | description: Success 57 | head: 58 | summary: Head Resource 59 | operationId: headResource 60 | responses: 61 | 200: 62 | description: Success 63 | /resource/get: 64 | get: 65 | summary: Get Resource 66 | operationId: resourceGetter 67 | responses: 68 | 200: 69 | description: Success 70 | content: 71 | application/json: 72 | schema: 73 | $ref: "#/components/schemas/Resource" 74 | /resource/post: 75 | post: 76 | summary: Create Resource 77 | operationId: resourcePoster 78 | responses: 79 | 201: 80 | description: Created 81 | /resource/put: 82 | put: 83 | summary: Replace Resource 84 | operationId: resourcePutter 85 | responses: 86 | 200: 87 | description: Success 88 | content: 89 | application/json: 90 | schema: 91 | $ref: "#/components/schemas/Resource" 92 | /resource/patch: 93 | patch: 94 | summary: Update Resource 95 | operationId: resourcePatcher 96 | responses: 97 | 200: 98 | description: Success 99 | content: 100 | application/json: 101 | schema: 102 | $ref: "#/components/schemas/Resource" 103 | /resource/delete: 104 | delete: 105 | summary: Delete Resource 106 | operationId: resourceDeleter 107 | responses: 108 | 204: 109 | description: No Content 110 | components: 111 | schemas: 112 | Resource: 113 | type: object 114 | required: 115 | - value 116 | properties: 117 | value: 118 | type: string 119 | -------------------------------------------------------------------------------- /test/fixtures/http-utilities.api.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | info: 3 | title: Http Utilities API 4 | version: 1.0.0 5 | servers: 6 | - url: http://localhost:3000 7 | paths: 8 | /resource/{id}: 9 | parameters: 10 | - name: id 11 | in: path 12 | required: true 13 | schema: 14 | type: string 15 | get: 16 | summary: Get Resource By Id 17 | operationId: getResourceById 18 | responses: 19 | 200: 20 | description: Success 21 | content: 22 | application/json: 23 | schema: 24 | $ref: "#/components/schemas/Resource" 25 | /resource: 26 | post: 27 | summary: Create Resource 28 | operationId: createResource 29 | requestBody: 30 | required: true 31 | content: 32 | application/json: 33 | schema: 34 | $ref: "#/components/schemas/NewResource" 35 | responses: 36 | 201: 37 | description: Created 38 | get: 39 | summary: Get Resource 40 | operationId: getResource 41 | responses: 42 | 200: 43 | description: Success 44 | content: 45 | application/json: 46 | schema: 47 | $ref: "#/components/schemas/Resource" 48 | components: 49 | schemas: 50 | Resource: 51 | type: object 52 | required: 53 | - id 54 | - name 55 | properties: 56 | id: 57 | type: string 58 | name: 59 | type: string 60 | NewResource: 61 | type: object 62 | required: 63 | - name 64 | properties: 65 | name: 66 | type: string 67 | -------------------------------------------------------------------------------- /test/fixtures/no-content.api.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | info: 3 | title: No Content API 4 | version: 1.0.0 5 | servers: 6 | - url: http://localhost:3000 7 | paths: 8 | /resource: 9 | post: 10 | summary: Create Resource 11 | operationId: createResource 12 | responses: 13 | 201: 14 | description: Created 15 | delete: 16 | summary: Delete Resource 17 | operationId: deleteResource 18 | responses: 19 | 204: 20 | description: No Content 21 | /no-content-resource: 22 | get: 23 | summary: Get Resource With No Content 24 | operationId: getResourceNoContent 25 | responses: 26 | 200: 27 | description: Success 28 | content: 29 | application/json: 30 | schema: 31 | $ref: "#/components/schemas/Resource" 32 | 204: 33 | description: NoContent 34 | components: 35 | schemas: 36 | Resource: 37 | type: object 38 | required: 39 | - id 40 | - name 41 | - value 42 | properties: 43 | id: 44 | type: string 45 | name: 46 | type: string 47 | value: 48 | type: integer 49 | -------------------------------------------------------------------------------- /test/fixtures/options.api.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | info: 3 | title: Options API 4 | version: 1.0.0 5 | servers: 6 | - url: http://localhost:3000 7 | paths: 8 | /resource: 9 | get: 10 | summary: Get Resource 11 | operationId: getResource 12 | responses: 13 | 200: 14 | description: Success 15 | content: 16 | application/json: 17 | schema: 18 | $ref: "#/components/schemas/Resource" 19 | components: 20 | schemas: 21 | Resource: 22 | type: object 23 | required: 24 | - id 25 | properties: 26 | id: 27 | type: string 28 | -------------------------------------------------------------------------------- /test/fixtures/path-fragments.api.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | info: 3 | title: Path Fragments API 4 | version: 1.0.0 5 | servers: 6 | - url: http://localhost:3000 7 | paths: 8 | /resource/{id}/{name}: 9 | parameters: 10 | - name: id 11 | in: path 12 | required: true 13 | schema: 14 | type: string 15 | - name: name 16 | in: path 17 | required: true 18 | schema: 19 | type: string 20 | get: 21 | summary: Get Resource By Id And Name 22 | operationId: getResourceByIdAndName 23 | responses: 24 | 200: 25 | description: Success 26 | content: 27 | application/json: 28 | schema: 29 | $ref: "#/components/schemas/Resource" 30 | /resource/{count}: 31 | parameters: 32 | - name: count 33 | in: path 34 | required: true 35 | schema: 36 | type: number 37 | get: 38 | summary: Get Resource Count 39 | operationId: getResourceCount 40 | responses: 41 | 200: 42 | description: Success 43 | content: 44 | application/json: 45 | schema: 46 | $ref: "#/components/schemas/Count" 47 | /resource/{enum}: 48 | parameters: 49 | - name: enum 50 | in: path 51 | required: true 52 | schema: 53 | type: string 54 | enum: [test1, test2] 55 | get: 56 | summary: Get Resource By Enum 57 | operationId: getResourceEnum 58 | responses: 59 | 200: 60 | description: Success 61 | content: 62 | application/json: 63 | schema: 64 | $ref: "#/components/schemas/Resource" 65 | /resource: 66 | get: 67 | summary: Get Resource 68 | operationId: getResource 69 | responses: 70 | 200: 71 | description: Success 72 | content: 73 | application/json: 74 | schema: 75 | $ref: "#/components/schemas/Count" 76 | components: 77 | schemas: 78 | Resource: 79 | type: object 80 | required: 81 | - id 82 | - name 83 | properties: 84 | id: 85 | type: string 86 | name: 87 | type: string 88 | Count: 89 | type: object 90 | required: 91 | - count 92 | properties: 93 | count: 94 | type: number 95 | -------------------------------------------------------------------------------- /test/fixtures/query-params.api.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | info: 3 | title: Options API 4 | version: 1.0.0 5 | servers: 6 | - url: http://localhost:3000 7 | paths: 8 | /no-query: 9 | get: 10 | summary: No Query Params 11 | operationId: getNoQuery 12 | responses: 13 | 200: 14 | description: Success 15 | content: 16 | application/json: 17 | schema: 18 | $ref: "#/components/schemas/Resource" 19 | /single-query: 20 | get: 21 | summary: Single Query Params 22 | operationId: getSingleQuery 23 | parameters: 24 | - name: query 25 | in: query 26 | required: true 27 | schema: 28 | type: string 29 | - name: page 30 | in: query 31 | schema: 32 | type: number 33 | - name: sort 34 | in: query 35 | required: false 36 | schema: 37 | type: string 38 | enum: ["asc", "desc"] 39 | responses: 40 | 200: 41 | description: Success 42 | content: 43 | application/json: 44 | schema: 45 | $ref: "#/components/schemas/Resource" 46 | /multi-query: 47 | get: 48 | summary: Multi Query Params 49 | operationId: getMultiQuery 50 | parameters: 51 | - name: id 52 | in: query 53 | schema: 54 | type: array 55 | items: 56 | type: number 57 | - name: sortBy 58 | in: query 59 | schema: 60 | type: "array" 61 | items: 62 | type: string 63 | enum: ["asc", "desc"] 64 | responses: 65 | 200: 66 | description: Success 67 | content: 68 | application/json: 69 | schema: 70 | $ref: "#/components/schemas/Resource" 71 | components: 72 | schemas: 73 | Resource: 74 | type: object 75 | required: 76 | - id 77 | properties: 78 | id: 79 | type: string 80 | -------------------------------------------------------------------------------- /test/fixtures/request-body.api.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | info: 3 | title: Request Body API 4 | version: 1.0.0 5 | servers: 6 | - url: http://localhost:3000 7 | paths: 8 | /resource: 9 | get: 10 | summary: Get Resource 11 | operationId: getResource 12 | responses: 13 | 200: 14 | description: Success 15 | content: 16 | application/json: 17 | schema: 18 | $ref: "#/components/schemas/Resource" 19 | post: 20 | summary: Create Resource 21 | operationId: createResource 22 | requestBody: 23 | required: true 24 | content: 25 | application/json: 26 | schema: 27 | $ref: "#/components/schemas/NewResource" 28 | responses: 29 | 201: 30 | description: Created 31 | content: 32 | application/json: 33 | schema: 34 | $ref: "#/components/schemas/Resource" 35 | patch: 36 | summary: Patch Resource 37 | operationId: patchResource 38 | requestBody: 39 | required: false 40 | content: 41 | application/json: 42 | schema: 43 | $ref: "#/components/schemas/NewResource" 44 | responses: 45 | 200: 46 | description: Success 47 | content: 48 | application/json: 49 | schema: 50 | $ref: "#/components/schemas/Resource" 51 | /multi-body: 52 | post: 53 | summary: "Create from Text or JSON" 54 | operationId: postMultiBody 55 | requestBody: 56 | required: true 57 | content: 58 | text/plain: 59 | schema: 60 | type: string 61 | enum: ["Hello", "Goodbye"] 62 | application/json: 63 | schema: 64 | $ref: "#/components/schemas/NewResource" 65 | responses: 66 | 204: 67 | description: NoContent 68 | /special-json: 69 | post: 70 | summary: Create for Special JSON 71 | operationId: postSpecialJSON 72 | requestBody: 73 | required: true 74 | content: 75 | application/ld+json: 76 | schema: 77 | $ref: "#/components/schemas/NewResource" 78 | responses: 79 | 204: 80 | description: NoContent 81 | components: 82 | schemas: 83 | Resource: 84 | allOf: 85 | - $ref: "#/components/schemas/NewResource" 86 | - type: object 87 | required: 88 | - id 89 | properties: 90 | id: 91 | type: string 92 | NewResource: 93 | type: object 94 | required: 95 | - name 96 | - value 97 | properties: 98 | name: 99 | type: string 100 | value: 101 | type: integer 102 | -------------------------------------------------------------------------------- /test/fixtures/response-content.api.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | info: 3 | title: Response Content API 4 | version: 1.0.0 5 | servers: 6 | - url: http://localhost:3000 7 | paths: 8 | /resource: 9 | get: 10 | summary: Get Resource 11 | operationId: getResource 12 | responses: 13 | 200: 14 | description: Success 15 | content: 16 | application/json: 17 | schema: 18 | $ref: "#/components/schemas/Resource" 19 | text/plain: 20 | schema: 21 | type: string 22 | enum: ["Hello", "Goodbye"] 23 | 204: 24 | description: NoContent 25 | 418: 26 | description: Error 27 | content: 28 | application/json: 29 | schema: 30 | $ref: "#/components/schemas/Exception" 31 | "5XX": 32 | description: Error 33 | content: 34 | application/json: 35 | schema: 36 | $ref: "#/components/schemas/Exception" 37 | "default": 38 | description: Error 39 | content: 40 | application/json: 41 | schema: 42 | $ref: "#/components/schemas/Exception" 43 | /text-resource: 44 | get: 45 | summary: Get Text Resource 46 | operationId: getTextResource 47 | responses: 48 | 200: 49 | description: Success 50 | content: 51 | text/plain: 52 | schema: 53 | type: string 54 | /special-json: 55 | get: 56 | summary: Get Special JSON Response 57 | operationId: getSpecialJSON 58 | responses: 59 | 200: 60 | description: Success 61 | content: 62 | application/ld+json: 63 | schema: 64 | $ref: "#/components/schemas/Resource" 65 | 66 | 401: 67 | description: Error 68 | content: 69 | application/problem+json: 70 | schema: 71 | $ref: "#/components/schemas/Exception" 72 | components: 73 | schemas: 74 | Resource: 75 | type: object 76 | required: 77 | - id 78 | - name 79 | - value 80 | properties: 81 | id: 82 | type: string 83 | name: 84 | type: string 85 | value: 86 | type: integer 87 | Exception: 88 | type: object 89 | required: [error, code] 90 | properties: 91 | error: 92 | type: string 93 | code: 94 | type: number 95 | -------------------------------------------------------------------------------- /test/http-methods.test-d.ts: -------------------------------------------------------------------------------- 1 | import { createOpenApiHttp } from "openapi-msw"; 2 | import { describe, expectTypeOf, test } from "vitest"; 3 | import type { paths } from "./fixtures/http-methods.api.js"; 4 | 5 | describe("Given an OpenAPI endpoint with multiple HTTP methods", () => { 6 | const http = createOpenApiHttp(); 7 | 8 | test("When the GET method is mocked, Then only paths with GET methods are allowed", () => { 9 | const path = expectTypeOf().parameter(0); 10 | 11 | path.toEqualTypeOf<"/resource" | "/resource/get">(); 12 | }); 13 | 14 | test("When the PUT method is mocked, Then only paths with PUT methods are allowed", () => { 15 | const path = expectTypeOf().parameter(0); 16 | 17 | path.toEqualTypeOf<"/resource" | "/resource/put">(); 18 | }); 19 | 20 | test("When the POST method is mocked, Then only paths with POST methods are allowed", () => { 21 | const path = expectTypeOf().parameter(0); 22 | 23 | path.toEqualTypeOf<"/resource" | "/resource/post">(); 24 | }); 25 | 26 | test("When the PATCH method is mocked, Then only paths with PATCH methods are allowed", () => { 27 | const path = expectTypeOf().parameter(0); 28 | 29 | path.toEqualTypeOf<"/resource" | "/resource/patch">(); 30 | }); 31 | 32 | test("When the DELETE method is mocked, Then only paths with DELETE methods are allowed", () => { 33 | const path = expectTypeOf().parameter(0); 34 | 35 | path.toEqualTypeOf<"/resource" | "/resource/delete">(); 36 | }); 37 | 38 | test("When the OPTIONS method is mocked, Then only paths with OPTIONS methods are allowed", () => { 39 | const path = expectTypeOf().parameter(0); 40 | 41 | path.toEqualTypeOf<"/resource">(); 42 | }); 43 | 44 | test("When the HEAD method is mocked, Then only paths with HEAD methods are allowed", () => { 45 | const path = expectTypeOf().parameter(0); 46 | 47 | path.toEqualTypeOf<"/resource">(); 48 | }); 49 | 50 | test("When the vanilla HTTP fallback is used, Then any path value is allowed", () => { 51 | const extractPath = (method: keyof typeof http.untyped) => 52 | expectTypeOf<(typeof http.untyped)[typeof method]>() 53 | .parameter(0) 54 | .extract(); 55 | 56 | extractPath("all").toEqualTypeOf(); 57 | extractPath("get").toEqualTypeOf(); 58 | extractPath("put").toEqualTypeOf(); 59 | extractPath("post").toEqualTypeOf(); 60 | extractPath("patch").toEqualTypeOf(); 61 | extractPath("delete").toEqualTypeOf(); 62 | extractPath("options").toEqualTypeOf(); 63 | extractPath("head").toEqualTypeOf(); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/http-utilities.test-d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createOpenApiHttp, 3 | type PathsFor, 4 | type RequestBodyFor, 5 | type ResponseBodyFor, 6 | } from "openapi-msw"; 7 | import { describe, expectTypeOf, test } from "vitest"; 8 | import type { paths } from "./fixtures/http-utilities.api.js"; 9 | 10 | describe("Given an OpenApiHttpHandlers namespace", () => { 11 | const http = createOpenApiHttp(); 12 | 13 | test("When paths are extracted, Then a path union is returned", () => { 14 | const result = expectTypeOf>(); 15 | 16 | result.toEqualTypeOf<"/resource" | "/resource/{id}">(); 17 | }); 18 | 19 | test("When the request body is extracted, Then the request body is returned", () => { 20 | const result = 21 | expectTypeOf>(); 22 | const resultNoBody = 23 | expectTypeOf>(); 24 | 25 | result.toEqualTypeOf<{ name: string }>(); 26 | resultNoBody.toEqualTypeOf(); 27 | }); 28 | 29 | test("When the response body is extracted, Then the response body is returned", () => { 30 | const result = 31 | expectTypeOf>(); 32 | const resultNoBody = 33 | expectTypeOf>(); 34 | 35 | result.toEqualTypeOf<{ id: string; name: string }>(); 36 | resultNoBody.toEqualTypeOf(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/no-content.test-d.ts: -------------------------------------------------------------------------------- 1 | import { type StrictResponse } from "msw"; 2 | import { createOpenApiHttp } from "openapi-msw"; 3 | import { describe, expectTypeOf, test } from "vitest"; 4 | import type { paths } from "./fixtures/no-content.api.js"; 5 | 6 | describe("Given an OpenAPI schema endpoint with no-content", () => { 7 | const http = createOpenApiHttp(); 8 | 9 | test("When an endpoint is mocked, Then responses with content cannot be returned", async () => { 10 | type Endpoint = typeof http.delete<"/resource">; 11 | const resolver = expectTypeOf().parameter(1); 12 | const response = resolver.returns.extract(); 13 | 14 | response.not.toEqualTypeOf>(); 15 | }); 16 | 17 | test("When an endpoint is mocked, Then responses must be strict responses", async () => { 18 | type Endpoint = typeof http.delete<"/resource">; 19 | const resolver = expectTypeOf().parameter(1); 20 | const response = resolver.returns.extract(); 21 | 22 | response.not.toEqualTypeOf(); 23 | response.toEqualTypeOf>(); 24 | }); 25 | 26 | test("When a endpoint with a NoContent response is mocked, Then the no-content option is included in the response union", async () => { 27 | type Endpoint = typeof http.get<"/no-content-resource">; 28 | const resolver = expectTypeOf().parameter(1); 29 | const response = resolver.returns.extract(); 30 | 31 | response.not.toEqualTypeOf(); 32 | response.toEqualTypeOf< 33 | StrictResponse<{ id: string; name: string; value: number } | null> 34 | >(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/no-content.test.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse, type StrictResponse, getResponse } from "msw"; 2 | import { createOpenApiHttp } from "openapi-msw"; 3 | import { describe, expect, test } from "vitest"; 4 | import type { paths } from "./fixtures/no-content.api.js"; 5 | 6 | describe("Given an OpenAPI schema endpoint with no-content", () => { 7 | const http = createOpenApiHttp({ baseUrl: "*" }); 8 | 9 | test("When the DELETE method is mocked, Then empty responses can be returned", async () => { 10 | const request = new Request(new URL("/resource", "http://localhost:3000"), { 11 | method: "delete", 12 | }); 13 | 14 | const handler = http.delete("/resource", () => { 15 | return new HttpResponse(null, { status: 204 }) as StrictResponse; 16 | }); 17 | const response = await getResponse([handler], request); 18 | 19 | expect(response?.body).toBeNull(); 20 | expect(response?.status).toBe(204); 21 | }); 22 | 23 | test("When the POST method is mocked, Then empty responses can be returned", async () => { 24 | const request = new Request(new URL("/resource", "http://localhost:3000"), { 25 | method: "post", 26 | }); 27 | 28 | const handler = http.post("/resource", () => { 29 | return new HttpResponse(null, { status: 201 }) as StrictResponse; 30 | }); 31 | const response = await getResponse([handler], request); 32 | 33 | expect(response?.body).toBeNull(); 34 | expect(response?.status).toBe(201); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/options.test.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse } from "msw"; 2 | import { createOpenApiHttp } from "openapi-msw"; 3 | import { describe, expect, test } from "vitest"; 4 | import type { paths } from "./fixtures/options.api.js"; 5 | 6 | describe("Given a created HTTP object with options", () => { 7 | const http = createOpenApiHttp({ baseUrl: "http://localhost:3000" }); 8 | 9 | test("When a OpenAPI handler is created, Then the baseUrl is prepended to the path", async () => { 10 | const handler = http.get("/resource", () => { 11 | return HttpResponse.json({ id: "test-id" }); 12 | }); 13 | 14 | expect(handler.info.path).toBe("http://localhost:3000/resource"); 15 | }); 16 | 17 | test("When a vanilla handler is created, Then the baseUrl is not prepended to the path", async () => { 18 | const handler = http.untyped.get("/some-resource", () => { 19 | return HttpResponse.json(); 20 | }); 21 | 22 | expect(handler.info.path).toBe("/some-resource"); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/path-fragments.test-d.ts: -------------------------------------------------------------------------------- 1 | import { createOpenApiHttp } from "openapi-msw"; 2 | import { describe, expectTypeOf, test } from "vitest"; 3 | import type { paths } from "./fixtures/path-fragments.api.js"; 4 | 5 | describe("Given an OpenAPI schema endpoint that contains path fragments", () => { 6 | const http = createOpenApiHttp(); 7 | 8 | test("When an endpoint is mocked, Then the path fragments are strict-typed", () => { 9 | type Endpoint = typeof http.get<"/resource/{id}/{name}">; 10 | const resolver = expectTypeOf().parameter(1); 11 | const params = resolver.parameter(0).toHaveProperty("params"); 12 | 13 | params.toEqualTypeOf<{ id: string; name: string }>(); 14 | }); 15 | 16 | test("When a endpoint contains no path fragments, Then no params are provided", () => { 17 | type Endpoint = typeof http.get<"/resource">; 18 | const resolver = expectTypeOf().parameter(1); 19 | const params = resolver.parameter(0).toHaveProperty("params"); 20 | 21 | params.toEqualTypeOf(); 22 | }); 23 | 24 | test("When a path fragment is typed with non-string types, Then the type is converted to string", async () => { 25 | // The values passed in by MSW will always be strings at runtime. 26 | // Therefore, we convert to to enable runtime parsing, e.g. parseInt(...). 27 | type Endpoint = typeof http.get<"/resource/{count}">; 28 | const resolver = expectTypeOf().parameter(1); 29 | const params = resolver.parameter(0).toHaveProperty("params"); 30 | 31 | params.toEqualTypeOf<{ count: string }>(); 32 | }); 33 | 34 | test("When a path fragment is typed with string literals, Then the literals are preserved", async () => { 35 | type Endpoint = typeof http.get<"/resource/{enum}">; 36 | const resolver = expectTypeOf().parameter(1); 37 | const params = resolver.parameter(0).toHaveProperty("params"); 38 | 39 | params.toEqualTypeOf<{ enum: "test1" | "test2" }>(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/path-fragments.test.ts: -------------------------------------------------------------------------------- 1 | import { getResponse, HttpResponse } from "msw"; 2 | import { createOpenApiHttp } from "openapi-msw"; 3 | import { describe, expect, test } from "vitest"; 4 | import type { paths } from "./fixtures/path-fragments.api.js"; 5 | 6 | describe("Given an OpenAPI schema endpoint that contains path fragments", () => { 7 | const http = createOpenApiHttp({ baseUrl: "*" }); 8 | 9 | test("When a endpoint is mocked, Then OpenAPI path fragments can be parsed by the handler", async () => { 10 | const request = new Request( 11 | new URL("/resource/test-id/test-name", "http://localhost:3000"), 12 | ); 13 | 14 | const handler = http.get("/resource/{id}/{name}", ({ params }) => { 15 | return HttpResponse.json({ id: params.id, name: params.name }); 16 | }); 17 | const response = await getResponse([handler], request); 18 | 19 | const responseBody = await response?.json(); 20 | expect(responseBody?.id).toBe("test-id"); 21 | expect(responseBody?.name).toBe("test-name"); 22 | }); 23 | 24 | test("When a path fragment is specified as a number, Then it can be parsed to a number", async () => { 25 | const request = new Request( 26 | new URL("/resource/42", "http://localhost:3000"), 27 | ); 28 | 29 | const handler = http.get("/resource/{count}", ({ params }) => { 30 | const count = parseInt(params.count); 31 | 32 | return HttpResponse.json({ count }); 33 | }); 34 | const response = await getResponse([handler], request); 35 | 36 | const responseBody = await response?.json(); 37 | expect(responseBody?.count).toBe(42); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/query-params.test-d.ts: -------------------------------------------------------------------------------- 1 | import { createOpenApiHttp } from "openapi-msw"; 2 | import { describe, expectTypeOf, test } from "vitest"; 3 | import type { paths } from "./fixtures/query-params.api.js"; 4 | 5 | describe("Given an OpenAPI schema endpoint with query parameters fragments", () => { 6 | const http = createOpenApiHttp({ baseUrl: "*" }); 7 | 8 | test("When a endpoint is mocked, Then search parameter keys are strict typed", () => { 9 | http.get("/single-query", ({ query }) => { 10 | expectTypeOf(query.get) 11 | .parameter(0) 12 | .toEqualTypeOf<"sort" | "page" | "query">(); 13 | expectTypeOf(query.getAll) 14 | .parameter(0) 15 | .toEqualTypeOf<"sort" | "page" | "query">(); 16 | expectTypeOf(query.has) 17 | .parameter(0) 18 | .toEqualTypeOf<"sort" | "page" | "query">(); 19 | }); 20 | }); 21 | 22 | test("When a endpoint is mocked, Then search parameters are converted into their stringified version", () => { 23 | http.get("/single-query", ({ query }) => { 24 | const sort = query.getAll("sort"); 25 | const page = query.get("page"); 26 | const queryString = query.get("query"); 27 | 28 | expectTypeOf(sort).toEqualTypeOf<("asc" | "desc")[]>(); 29 | expectTypeOf(page).toEqualTypeOf(); 30 | expectTypeOf(queryString).toEqualTypeOf(); 31 | }); 32 | }); 33 | 34 | test("When a endpoint is mocked, Then multiple search parameters are parsed from arrays", () => { 35 | http.get("/multi-query", ({ query }) => { 36 | const single = query.get("id"); 37 | const multi = query.getAll("id"); 38 | const singleSortBy = query.get("sortBy"); 39 | const multiSortBy = query.getAll("sortBy"); 40 | 41 | expectTypeOf(single).toEqualTypeOf(); 42 | expectTypeOf(multi).toEqualTypeOf(); 43 | expectTypeOf(singleSortBy).toEqualTypeOf<"asc" | "desc" | null>(); 44 | expectTypeOf(multiSortBy).toEqualTypeOf<("asc" | "desc")[]>(); 45 | }); 46 | }); 47 | 48 | test("When a endpoint with no query params is mocked, Then no query keys can be passed to the query helper", () => { 49 | http.get("/no-query", ({ query }) => { 50 | expectTypeOf(query.get).parameter(0).toEqualTypeOf(); 51 | expectTypeOf(query.getAll).parameter(0).toEqualTypeOf(); 52 | expectTypeOf(query.has).parameter(0).toEqualTypeOf(); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/query-params.test.ts: -------------------------------------------------------------------------------- 1 | import { getResponse, HttpResponse } from "msw"; 2 | import { createOpenApiHttp } from "openapi-msw"; 3 | import { describe, expect, test } from "vitest"; 4 | import type { paths } from "./fixtures/query-params.api.js"; 5 | 6 | describe("Given an OpenAPI schema endpoint with query parameters fragments", () => { 7 | const http = createOpenApiHttp({ baseUrl: "*" }); 8 | 9 | test("When a endpoint is mocked, Then query parameters can be accessed through a 'query' helper", async () => { 10 | expect.assertions(4); // Make sure that assertion in handler is executed. 11 | const request = new Request( 12 | new URL( 13 | "/single-query?page=3&sort=desc&query=test", 14 | "http://localhost:3000", 15 | ), 16 | ); 17 | 18 | const handler = http.get("/single-query", ({ query }) => { 19 | const sort = query.getAll("sort"); 20 | const page = query.get("page"); 21 | const queryString = query.get("query"); 22 | 23 | expect(sort).toEqual(["desc"]); 24 | expect(page).toBe("3"); 25 | expect(queryString).toBe("test"); 26 | 27 | return HttpResponse.json({ id: "test-id" }); 28 | }); 29 | const response = await getResponse([handler], request); 30 | 31 | expect(response?.status).toBe(200); 32 | }); 33 | 34 | test("When a endpoint is mocked, Then multiple query parameters are grouped into an array", async () => { 35 | expect.assertions(2); // Make sure that assertion in handler is executed. 36 | const request = new Request( 37 | new URL("/multi-query?id=1&id=2&id=3", "http://localhost:3000"), 38 | ); 39 | 40 | const handler = http.get("/multi-query", ({ query }) => { 41 | const ids = query.getAll("id"); 42 | expect(ids).toStrictEqual(["1", "2", "3"]); 43 | 44 | return HttpResponse.json({ id: "test-id" }); 45 | }); 46 | const response = await getResponse([handler], request); 47 | 48 | expect(response?.status).toBe(200); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/request-body.test-d.ts: -------------------------------------------------------------------------------- 1 | import { type StrictRequest } from "msw"; 2 | import { createOpenApiHttp } from "openapi-msw"; 3 | import { describe, expectTypeOf, test } from "vitest"; 4 | import type { paths } from "./fixtures/request-body.api.js"; 5 | 6 | describe("Given an OpenAPI schema endpoint with request content", () => { 7 | const http = createOpenApiHttp(); 8 | 9 | test("When the request is used, Then it extend MSW's request object", () => { 10 | type Endpoint = typeof http.get<"/resource">; 11 | const resolver = expectTypeOf().parameter(1); 12 | const request = resolver.parameter(0).toHaveProperty("request"); 13 | 14 | request.toMatchTypeOf, "text" | "json">>(); 15 | }); 16 | 17 | test("When a request is not expected to contain content, Then json and text return never", () => { 18 | type Endpoint = typeof http.get<"/resource">; 19 | const resolver = expectTypeOf().parameter(1); 20 | const request = resolver.parameter(0).toHaveProperty("request"); 21 | 22 | request.toHaveProperty("text").returns.toEqualTypeOf(); 23 | request.toHaveProperty("json").returns.toEqualTypeOf(); 24 | }); 25 | 26 | test("When a request is expected to contain content, Then the content is strict-typed", () => { 27 | type Endpoint = typeof http.post<"/resource">; 28 | const resolver = expectTypeOf().parameter(1); 29 | const request = resolver.parameter(0).toHaveProperty("request"); 30 | 31 | request.toHaveProperty("text").returns.toEqualTypeOf(); 32 | request 33 | .toHaveProperty("json") 34 | .returns.resolves.toEqualTypeOf<{ name: string; value: number }>(); 35 | }); 36 | 37 | test("When a request uses a special JSON mime type, Then the content is strict-typed", async () => { 38 | type Endpoint = typeof http.post<"/special-json">; 39 | const resolver = expectTypeOf().parameter(1); 40 | const request = resolver.parameter(0).toHaveProperty("request"); 41 | 42 | request.toHaveProperty("text").returns.toEqualTypeOf(); 43 | request 44 | .toHaveProperty("json") 45 | .returns.resolves.toEqualTypeOf<{ name: string; value: number }>(); 46 | }); 47 | 48 | test("When a request content is optional, Then the content is strict-typed with optional", () => { 49 | type Endpoint = typeof http.patch<"/resource">; 50 | const resolver = expectTypeOf().parameter(1); 51 | const request = resolver.parameter(0).toHaveProperty("request"); 52 | 53 | request 54 | .toHaveProperty("json") 55 | .returns.resolves.toEqualTypeOf< 56 | { name: string; value: number } | undefined 57 | >(); 58 | }); 59 | 60 | test("When a request accepts multiple media types, Then both body parsers are typed for their media type", () => { 61 | type Endpoint = typeof http.post<"/multi-body">; 62 | const resolver = expectTypeOf().parameter(1); 63 | const request = resolver.parameter(0).toHaveProperty("request"); 64 | 65 | request 66 | .toHaveProperty("text") 67 | .returns.resolves.toEqualTypeOf<"Hello" | "Goodbye">(); 68 | request 69 | .toHaveProperty("json") 70 | .returns.resolves.toEqualTypeOf<{ name: string; value: number }>(); 71 | }); 72 | 73 | test("When the request is cloned, Then it remains strict-typed", () => { 74 | type Endpoint = typeof http.post<"/multi-body">; 75 | const resolver = expectTypeOf().parameter(1); 76 | const request = resolver.parameter(0).toHaveProperty("request"); 77 | const cloned = request.toHaveProperty("clone").returns; 78 | 79 | cloned 80 | .toHaveProperty("text") 81 | .returns.resolves.toEqualTypeOf<"Hello" | "Goodbye">(); 82 | cloned 83 | .toHaveProperty("json") 84 | .returns.resolves.toEqualTypeOf<{ name: string; value: number }>(); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/request-body.test.ts: -------------------------------------------------------------------------------- 1 | import { getResponse, HttpResponse } from "msw"; 2 | import { createOpenApiHttp } from "openapi-msw"; 3 | import { describe, expect, test } from "vitest"; 4 | import type { paths } from "./fixtures/request-body.api.js"; 5 | 6 | describe("Given an OpenAPI schema endpoint with request content", () => { 7 | const http = createOpenApiHttp({ baseUrl: "*" }); 8 | 9 | test("When a request contains content, Then the content can be used in the response", async () => { 10 | const request = new Request(new URL("/resource", "http://localhost:3000"), { 11 | method: "post", 12 | body: JSON.stringify({ name: "test-name", value: 16 }), 13 | }); 14 | 15 | const handler = http.post("/resource", async ({ request }) => { 16 | const newResource = await request.json(); 17 | return HttpResponse.json( 18 | { ...newResource, id: "test-id" }, 19 | { status: 201 }, 20 | ); 21 | }); 22 | const response = await getResponse([handler], request); 23 | 24 | const responseBody = await response?.json(); 25 | expect(response?.status).toBe(201); 26 | expect(responseBody).toStrictEqual({ 27 | id: "test-id", 28 | name: "test-name", 29 | value: 16, 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/response-content.test-d.ts: -------------------------------------------------------------------------------- 1 | import { type StrictResponse } from "msw"; 2 | import { createOpenApiHttp } from "openapi-msw"; 3 | import { describe, expectTypeOf, test } from "vitest"; 4 | import type { paths } from "./fixtures/response-content.api.js"; 5 | 6 | describe("Given an OpenAPI schema endpoint with response content", () => { 7 | const http = createOpenApiHttp(); 8 | 9 | test("When a JSON endpoint is mocked, Then responses must be a strict content response", async () => { 10 | type Endpoint = typeof http.get<"/resource">; 11 | const resolver = expectTypeOf().parameter(1); 12 | const response = resolver.returns.extract(); 13 | 14 | response.not.toEqualTypeOf(); 15 | response.toEqualTypeOf< 16 | StrictResponse< 17 | | { id: string; name: string; value: number } 18 | | "Hello" 19 | | "Goodbye" 20 | | { error: string; code: number } 21 | | null 22 | > 23 | >(); 24 | }); 25 | 26 | test("When the response helper is used, Then available status codes are strict typed", async () => { 27 | type Endpoint = typeof http.get<"/resource">; 28 | const resolver = expectTypeOf().parameter(1); 29 | const response = resolver.parameter(0).toHaveProperty("response"); 30 | 31 | response.toBeFunction(); 32 | response.parameters.toEqualTypeOf< 33 | [status: 200 | 204 | 418 | "5XX" | "default"] 34 | >(); 35 | }); 36 | 37 | test("When a status code is given to the response helper, Then available content types get limited", async () => { 38 | http.get("/resource", ({ response }) => { 39 | expectTypeOf(response(200).text).toBeFunction(); 40 | expectTypeOf(response(200).json).toBeFunction(); 41 | expectTypeOf(response(200).empty).toEqualTypeOf(); 42 | 43 | expectTypeOf(response(418).text).toEqualTypeOf(); 44 | expectTypeOf(response(418).json).toBeFunction(); 45 | expectTypeOf(response(418).empty).toEqualTypeOf(); 46 | 47 | expectTypeOf(response(204).text).toEqualTypeOf(); 48 | expectTypeOf(response(204).json).toEqualTypeOf(); 49 | expectTypeOf(response(204).empty).toBeFunction(); 50 | 51 | expectTypeOf(response("5XX").text).toEqualTypeOf(); 52 | expectTypeOf(response("5XX").json).toBeFunction(); 53 | expectTypeOf(response("5XX").empty).toEqualTypeOf(); 54 | 55 | expectTypeOf(response("default").text).toEqualTypeOf(); 56 | expectTypeOf(response("default").json).toBeFunction(); 57 | expectTypeOf(response("default").empty).toEqualTypeOf(); 58 | }); 59 | }); 60 | 61 | test("When a status code and content type are chosen, Then the specific response content is strict typed", async () => { 62 | http.get("/resource", ({ response }) => { 63 | expectTypeOf(response(200).text).parameters.toEqualTypeOf< 64 | [ 65 | body: "Hello" | "Goodbye", 66 | init?: void | { 67 | headers?: HeadersInit; 68 | type?: ResponseType; 69 | statusText?: string; 70 | }, 71 | ] 72 | >(); 73 | 74 | expectTypeOf(response(418).json).parameters.toEqualTypeOf< 75 | [ 76 | body: { error: string; code: number }, 77 | init?: void | { 78 | headers?: HeadersInit; 79 | type?: ResponseType; 80 | statusText?: string; 81 | }, 82 | ] 83 | >(); 84 | 85 | expectTypeOf(response(204).empty).parameters.toEqualTypeOf< 86 | [ 87 | init?: void | { 88 | headers?: HeadersInit; 89 | type?: ResponseType; 90 | statusText?: string; 91 | }, 92 | ] 93 | >(); 94 | 95 | expectTypeOf(response("5XX").json).parameters.branded.toEqualTypeOf< 96 | [ 97 | body: { error: string; code: number }, 98 | init: { 99 | status: 500 | 501 | 502 | 503 | 504 | 507 | 508 | 510 | 511; 100 | headers?: HeadersInit; 101 | type?: ResponseType; 102 | statusText?: string; 103 | }, 104 | ] 105 | >(); 106 | }); 107 | }); 108 | 109 | test("When the response helper is used, Then the matching strict-typed responses are returned", async () => { 110 | http.get("/resource", ({ response }) => { 111 | expectTypeOf(response(200).text).returns.toEqualTypeOf< 112 | StrictResponse<"Hello" | "Goodbye"> 113 | >(); 114 | 115 | expectTypeOf(response(200).json).returns.toEqualTypeOf< 116 | StrictResponse<{ id: string; name: string; value: number }> 117 | >(); 118 | 119 | expectTypeOf(response(204).empty).returns.toEqualTypeOf< 120 | StrictResponse 121 | >(); 122 | 123 | expectTypeOf(response("default").json).returns.toEqualTypeOf< 124 | StrictResponse<{ error: string; code: number }> 125 | >(); 126 | }); 127 | }); 128 | 129 | test("When the response untyped fallback is used, Then any response is casted to the expected response body", async () => { 130 | type Endpoint = typeof http.get<"/resource">; 131 | const resolver = expectTypeOf().parameter(1); 132 | const response = resolver.parameter(0).toHaveProperty("response"); 133 | const untyped = response.toHaveProperty("untyped"); 134 | 135 | untyped.toBeFunction(); 136 | untyped.parameter(0).toEqualTypeOf(); 137 | untyped.returns.toEqualTypeOf< 138 | StrictResponse< 139 | | { id: string; name: string; value: number } 140 | | "Hello" 141 | | "Goodbye" 142 | | { error: string; code: number } 143 | | null 144 | > 145 | >(); 146 | }); 147 | 148 | test("When special JSON mime types are used, Then the response json helper still works", async () => { 149 | http.get("/special-json", ({ response }) => { 150 | expectTypeOf(response(200).json).returns.toEqualTypeOf< 151 | StrictResponse<{ id: string; name: string; value: number }> 152 | >(); 153 | 154 | expectTypeOf(response(401).json).returns.toEqualTypeOf< 155 | StrictResponse<{ error: string; code: number }> 156 | >(); 157 | }); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /test/response-content.test.ts: -------------------------------------------------------------------------------- 1 | import { getResponse, HttpResponse } from "msw"; 2 | import { createOpenApiHttp } from "openapi-msw"; 3 | import { describe, expect, test } from "vitest"; 4 | import type { paths } from "./fixtures/response-content.api.js"; 5 | 6 | describe("Given an OpenAPI schema endpoint with response content", () => { 7 | const http = createOpenApiHttp({ baseUrl: "*" }); 8 | 9 | test("When a JSON endpoint is mocked, Then a fitting response content can be returned", async () => { 10 | const request = new Request(new URL("/resource", "http://localhost:3000"), { 11 | method: "get", 12 | }); 13 | 14 | const handler = http.get("/resource", () => { 15 | return HttpResponse.json({ id: "test-id", name: "Test", value: 16 }); 16 | }); 17 | const response = await getResponse([handler], request); 18 | 19 | const responseBody = await response?.json(); 20 | expect(responseBody).toStrictEqual({ 21 | id: "test-id", 22 | name: "Test", 23 | value: 16, 24 | }); 25 | expect(response?.status).toBe(200); 26 | }); 27 | 28 | test("When a TEXT endpoint is mocked, Then text response content can be returned", async () => { 29 | const request = new Request( 30 | new URL("/text-resource", "http://localhost:3000"), 31 | { method: "get" }, 32 | ); 33 | 34 | const handler = http.get("/text-resource", () => { 35 | return HttpResponse.text("Hello World"); 36 | }); 37 | const response = await getResponse([handler], request); 38 | 39 | const responseBody = await response?.text(); 40 | expect(responseBody).toBe("Hello World"); 41 | expect(response?.status).toBe(200); 42 | }); 43 | 44 | test("When the response helper is used for fix status codes, Then a JSON response can be returned", async () => { 45 | const request = new Request(new URL("/resource", "http://localhost:3000"), { 46 | method: "get", 47 | }); 48 | 49 | const handler = http.get("/resource", ({ response }) => { 50 | return response(418).json({ code: 123, error: "Something wrong." }); 51 | }); 52 | const response = await getResponse([handler], request); 53 | 54 | const responseBody = await response?.json(); 55 | expect(responseBody).toStrictEqual({ 56 | code: 123, 57 | error: "Something wrong.", 58 | }); 59 | expect(response?.status).toBe(418); 60 | }); 61 | 62 | test("When the response helper is used for fix status codes, Then a TEXT response can be returned", async () => { 63 | const request = new Request(new URL("/resource", "http://localhost:3000"), { 64 | method: "get", 65 | }); 66 | 67 | const handler = http.get("/resource", ({ response }) => { 68 | return response(200).text("Hello"); 69 | }); 70 | const response = await getResponse([handler], request); 71 | 72 | const responseBody = await response?.text(); 73 | expect(responseBody).toBe("Hello"); 74 | expect(response?.status).toBe(200); 75 | }); 76 | 77 | test("When the response helper is used for fix status codes, Then a EMPTY response can be returned", async () => { 78 | const request = new Request(new URL("/resource", "http://localhost:3000"), { 79 | method: "get", 80 | }); 81 | 82 | const handler = http.get("/resource", ({ response }) => { 83 | return response(204).empty(); 84 | }); 85 | const response = await getResponse([handler], request); 86 | 87 | expect(response?.body).toBeNull(); 88 | expect(response?.status).toBe(204); 89 | }); 90 | 91 | test("When an empty response is created, Then the response includes a content-length header", async () => { 92 | const request = new Request(new URL("/resource", "http://localhost:3000"), { 93 | method: "get", 94 | }); 95 | 96 | const handler = http.get("/resource", ({ response }) => { 97 | return response(204).empty(); 98 | }); 99 | const response = await getResponse([handler], request); 100 | 101 | expect(response?.status).toBe(204); 102 | expect(response?.headers.has("content-length")).toBeTruthy(); 103 | expect(response?.headers.get("content-length")).toBe("0"); 104 | }); 105 | 106 | test("When an empty response with content-length is created, Then the provided content-length header is used", async () => { 107 | const request = new Request(new URL("/resource", "http://localhost:3000"), { 108 | method: "get", 109 | }); 110 | 111 | const handler = http.get("/resource", ({ response }) => { 112 | return response(204).empty({ headers: { "content-length": "32" } }); 113 | }); 114 | const response = await getResponse([handler], request); 115 | 116 | expect(response?.status).toBe(204); 117 | expect(response?.headers.has("content-length")).toBeTruthy(); 118 | expect(response?.headers.get("content-length")).toBe("32"); 119 | }); 120 | 121 | test("When the response helper is used for wildcard status codes, Then a specific response status must be chosen", async () => { 122 | const request = new Request(new URL("/resource", "http://localhost:3000"), { 123 | method: "get", 124 | }); 125 | 126 | const handler = http.get("/resource", ({ response }) => { 127 | return response("5XX").json( 128 | { code: 123, error: "Something wrong." }, 129 | { status: 503 }, 130 | ); 131 | }); 132 | const response = await getResponse([handler], request); 133 | 134 | const responseBody = await response?.json(); 135 | expect(responseBody).toStrictEqual({ 136 | code: 123, 137 | error: "Something wrong.", 138 | }); 139 | expect(response?.status).toBe(503); 140 | }); 141 | 142 | test("When an endpoint is mocked with the fallback helper, Then any response can be returned", async () => { 143 | const request = new Request(new URL("/resource", "http://localhost:3000"), { 144 | method: "get", 145 | }); 146 | const handler = http.get("/resource", ({ response }) => { 147 | return response.untyped( 148 | HttpResponse.text("Unexpected Error", { status: 500 }), 149 | ); 150 | }); 151 | const response = await getResponse([handler], request); 152 | 153 | const responseBody = await response?.text(); 154 | expect(response?.status).toBe(500); 155 | expect(responseBody).toBe("Unexpected Error"); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig.json", 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "sourceMap": true, 8 | "inlineSources": true 9 | }, 10 | "include": ["src", "exports"], 11 | "exclude": ["**/*.test.*"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig.json", 3 | "extends": "./tsconfig.build.json", 4 | "compilerOptions": { 5 | "module": "CommonJS", 6 | "moduleResolution": "Node", 7 | "verbatimModuleSyntax": false, 8 | "outDir": "./cjs" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig.json", 3 | "compilerOptions": { 4 | /* Language and Environment */ 5 | "target": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "skipLibCheck": true, 8 | 9 | /* Modules */ 10 | "module": "NodeNext", 11 | "moduleResolution": "NodeNext", 12 | "moduleDetection": "force", 13 | "verbatimModuleSyntax": true, 14 | "isolatedModules": true, 15 | "noUncheckedSideEffectImports": true, 16 | "erasableSyntaxOnly": true, 17 | 18 | /* Type Checking */ 19 | "strict": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noImplicitOverride": true, 22 | "noImplicitReturns": true, 23 | "noPropertyAccessFromIndexSignature": true, 24 | "noUncheckedIndexedAccess": true, 25 | "allowUnreachableCode": false, 26 | "exactOptionalPropertyTypes": true 27 | }, 28 | "include": ["src", "exports", "test"] 29 | } 30 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | watch: false, 6 | clearMocks: true, 7 | workspace: [ 8 | { 9 | extends: true, 10 | test: { 11 | name: "unit", 12 | root: "src", 13 | }, 14 | }, 15 | { 16 | extends: true, 17 | test: { 18 | name: "integration", 19 | root: "test", 20 | typecheck: { enabled: true }, 21 | }, 22 | }, 23 | ], 24 | }, 25 | }); 26 | --------------------------------------------------------------------------------