Skip to content

Commit c61c472

Browse files
authored
Merge pull request #1521 from drwpow/middleware
Middleware
2 parents 21fb33c + fc3a468 commit c61c472

27 files changed

+1379
-594
lines changed

Diff for: .changeset/cool-steaks-lay.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-fetch": minor
3+
---
4+
5+
Add middleware support

Diff for: .changeset/moody-bottles-tie.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-fetch": patch
3+
---
4+
5+
Support arrays in headers

Diff for: .changeset/nasty-comics-taste.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-fetch": minor
3+
---
4+
5+
⚠️ Breaking change (internal): fetch() is now called with new Request() to support middleware (which may affect test mocking)

Diff for: .changeset/old-beans-impress.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-fetch": minor
3+
---
4+
5+
⚠️ **Breaking change**: Responses are no longer automatically `.clone()`’d in certain instances. Be sure to `.clone()` yourself if you need to access the raw body!

Diff for: docs/.vitepress/config.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ export default defineConfig({
4444
{
4545
text: "openapi-fetch",
4646
items: [
47-
{ text: "Introduction", link: "/openapi-fetch/" },
47+
{ text: "Getting Started", link: "/openapi-fetch/" },
48+
{
49+
text: "Middleware & Auth",
50+
link: "/openapi-fetch/middleware-auth",
51+
},
52+
{ text: "Testing", link: "/openapi-fetch/testing" },
4853
{ text: "Examples", link: "/openapi-fetch/examples" },
4954
{ text: "API", link: "/openapi-fetch/api" },
5055
{ text: "About", link: "/openapi-fetch/about" },
@@ -68,7 +73,12 @@ export default defineConfig({
6873
{
6974
text: "openapi-fetch",
7075
items: [
71-
{ text: "Introduction", link: "/openapi-fetch/" },
76+
{ text: "Getting Started", link: "/openapi-fetch/" },
77+
{
78+
text: "Middleware & Auth",
79+
link: "/openapi-fetch/middleware-auth",
80+
},
81+
{ text: "Testing", link: "/openapi-fetch/testing" },
7282
{ text: "Examples", link: "/openapi-fetch/examples" },
7383
{ text: "API", link: "/openapi-fetch/api" },
7484
{ text: "About", link: "/openapi-fetch/about" },

Diff for: docs/6.x/advanced.md

+7-9
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,13 @@ type FilterKeys<Obj, Matchers> = {
102102
type PathResponses<T> = T extends { responses: any } ? T["responses"] : unknown;
103103
type OperationContent<T> = T extends { content: any } ? T["content"] : unknown;
104104
type MediaType = `${string}/${string}`;
105-
type MockedResponse<T, Status extends keyof T = keyof T> = FilterKeys<
106-
OperationContent<T[Status]>,
107-
MediaType
108-
> extends never
109-
? { status: Status; body?: never }
110-
: {
111-
status: Status;
112-
body: FilterKeys<OperationContent<T[Status]>, MediaType>;
113-
};
105+
type MockedResponse<T, Status extends keyof T = keyof T> =
106+
FilterKeys<OperationContent<T[Status]>, MediaType> extends never
107+
? { status: Status; body?: never }
108+
: {
109+
status: Status;
110+
body: FilterKeys<OperationContent<T[Status]>, MediaType>;
111+
};
114112

115113
/**
116114
* Mock fetch() calls and type against OpenAPI schema

Diff for: docs/cli.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export interface paths {
121121
Which means your type lookups also have to match the exact URL:
122122

123123
```ts
124-
import type{ paths } from "./api/v1";
124+
import type { paths } from "./api/v1";
125125
126126
const url = `/user/${id}`;
127127
type UserResponses = paths["/user/{user_id}"]["responses"];

Diff for: docs/data/contributors.json

+1-1
Large diffs are not rendered by default.

Diff for: docs/examples.md

+7-9
Original file line numberDiff line numberDiff line change
@@ -150,15 +150,13 @@ type FilterKeys<Obj, Matchers> = {
150150
type PathResponses<T> = T extends { responses: any } ? T["responses"] : unknown;
151151
type OperationContent<T> = T extends { content: any } ? T["content"] : unknown;
152152
type MediaType = `${string}/${string}`;
153-
type MockedResponse<T, Status extends keyof T = keyof T> = FilterKeys<
154-
OperationContent<T[Status]>,
155-
MediaType
156-
> extends never
157-
? { status: Status; body?: never }
158-
: {
159-
status: Status;
160-
body: FilterKeys<OperationContent<T[Status]>, MediaType>;
161-
};
153+
type MockedResponse<T, Status extends keyof T = keyof T> =
154+
FilterKeys<OperationContent<T[Status]>, MediaType> extends never
155+
? { status: Status; body?: never }
156+
: {
157+
status: Status;
158+
body: FilterKeys<OperationContent<T[Status]>, MediaType>;
159+
};
162160

163161
/**
164162
* Mock fetch() calls and type against OpenAPI schema

Diff for: docs/openapi-fetch/about.md

+14-3
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,28 @@ description: openapi-fetch Project Goals, comparisons, and more
1818

1919
## Differences
2020

21+
### vs. Axios
22+
23+
[Axios](https://axios-http.com) doesn’t automatically typecheck against your OpenAPI schema. Further, there’s no easy way to do that. Axios does have more features than openapi-fetch such as request/responce interception and cancellation.
24+
25+
### vs. tRPC
26+
27+
[tRPC](https://trpc.io/) is meant for projects where both the backend and frontend are written in TypeScript (Node.js). openapi-fetch is universal, and can work with any backend that follows an OpenAPI 3.x schema.
28+
2129
### vs. openapi-typescript-fetch
2230

23-
This library is identical in purpose to [openapi-typescript-fetch](https://github.com/ajaishankar/openapi-typescript-fetch), but has the following differences:
31+
[openapi-typescript-fetch](https://github.com/ajaishankar/openapi-typescript-fetch) predates openapi-fetch, and is nearly identical in purpos, but differs mostly in syntax (so it’s more of an opinionated choice):
2432

2533
- This library has a built-in `error` type for `3xx`/`4xx`/`5xx` errors whereas openapi-typescript-fetch throws exceptions (requiring you to wrap things in `try/catch`)
2634
- This library has a more terse syntax (`get(…)`) wheras openapi-typescript-fetch requires chaining (`.path(…).method(…).create()`)
27-
- openapi-typescript-fetch supports middleware whereas this library doesn’t
2835

2936
### vs. openapi-typescript-codegen
3037

31-
This library is quite different from [openapi-typescript-codegen](https://github.com/ferdikoomen/openapi-typescript-codegen)
38+
[openapi-typescript-codegen](https://github.com/ferdikoomen/openapi-typescript-codegen) is a codegen library, which is fundamentally different from openapi-fetch’s “no codegen” approach. openapi-fetch uses static TypeScript typechecking that all happens at build time with no client weight and no performance hit to runtime. Traditional codegen generates hundreds (if not thousands) of different functions that all take up client weight and slow down runtime.
39+
40+
### vs. Swagger Codegen
41+
42+
Swagger Codegen is the original codegen project for Swagger/OpenAPI, and has the same problems of other codgen approaches of size bloat and runtime performance problems. Further, Swagger Codegen require the Java runtime to work, whereas openapi-typescript/openapi-fetch don’t as native Node.js projects.
3243

3344
## Contributors
3445

Diff for: docs/openapi-fetch/api.md

+102-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ createClient<paths>(options);
2626
The following options apply to all request methods (`.GET()`, `.POST()`, etc.)
2727

2828
```ts
29-
client.get("/my-url", options);
29+
client.GET("/my-url", options);
3030
```
3131

3232
| Name | Type | Description |
@@ -37,6 +37,7 @@ client.get("/my-url", options);
3737
| `bodySerializer` | BodySerializer | (optional) Provide a [bodySerializer](#bodyserializer) |
3838
| `parseAs` | `"json"` \| `"text"` \| `"arrayBuffer"` \| `"blob"` \| `"stream"` | (optional) Parse the response using [a built-in instance method](https://developer.mozilla.org/en-US/docs/Web/API/Response#instance_methods) (default: `"json"`). `"stream"` skips parsing altogether and returns the raw stream. |
3939
| `fetch` | `fetch` | Fetch instance used for requests (default: fetch from `createClient`) |
40+
| `middleware` | `Middleware[]` | [See docs](/openapi-fetch/middleware-auth) |
4041
| (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal`, …) ([docs](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options)) |
4142

4243
## querySerializer
@@ -123,7 +124,7 @@ const client = createClient({
123124
Similar to [querySerializer](#queryserializer), bodySerializer allows you to customize how the requestBody is serialized if you don’t want the default [JSON.stringify()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) behavior. You probably only need this when using `multipart/form-data`:
124125

125126
```ts
126-
const { data, error } = await PUT("/submit", {
127+
const { data, error } = await client.PUT("/submit", {
127128
body: {
128129
name: "",
129130
query: { version: 2 },
@@ -150,3 +151,102 @@ openapi-fetch supports path serialization as [outlined in the 3.1 spec](https://
150151
| `/users/{.id*}` | label (exploded) | `/users/.5` | `/users/.3.4.5` | `/users/.role=admin.firstName=Alex` |
151152
| `/users/{;id}` | matrix | `/users/;id=5` | `/users/;id=3,4,5` | `/users/;id=role,admin,firstName,Alex` |
152153
| `/users/{;id*}` | matrix (exploded) | `/users/;id=5` | `/users/;id=3;id=4;id=5` | `/users/;role=admin;firstName=Alex` |
154+
155+
## Middleware
156+
157+
Middleware is an object with `onRequest()` and `onResponse()` callbacks that can observe and modify requests and responses.
158+
159+
```ts
160+
import createClient from "openapi-fetch";
161+
import type { paths } from "./api/v1";
162+
163+
const myMiddleware: Middleware = {
164+
async onRequest(req, options) {
165+
// set "foo" header
166+
req.headers.set("foo", "bar");
167+
return req;
168+
},
169+
async onResponse(res, options) {
170+
const { body, ...resOptions } = res;
171+
// change status of response
172+
return new Response(body, { ...resOptions, status: 200 });
173+
},
174+
};
175+
176+
const client = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
177+
178+
// register middleware
179+
client.use(myMiddleware);
180+
```
181+
182+
### onRequest
183+
184+
```ts
185+
onRequest(req, options) {
186+
//
187+
}
188+
```
189+
190+
`onRequest()` takes 2 params:
191+
192+
| Name | Type | Description |
193+
| :-------- | :-----------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
194+
| `req` | `MiddlewareRequest` | A standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) with `schemaPath` (OpenAPI pathname) and `params` ([params](/openapi-fetch/api#fetch-options) object) |
195+
| `options` | `MergedOptions` | Combination of [createClient](/openapi-fetch/api#create-client) options + [fetch overrides](/openapi-fetch/api#fetch-options) |
196+
197+
And it expects either:
198+
199+
- **If modifying the request:** A [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request)
200+
- **If not modifying:** `undefined` (void)
201+
202+
### onResponse
203+
204+
```ts
205+
onResponse(res, options) {
206+
//
207+
}
208+
```
209+
210+
`onResponse()` also takes 2 params:
211+
| Name | Type | Description |
212+
| :-------- | :-----------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
213+
| `req` | `MiddlewareRequest` | A standard [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). |
214+
| `options` | `MergedOptions` | Combination of [createClient](/openapi-fetch/api#create-client) options + [fetch overrides](/openapi-fetch/api#fetch-options) |
215+
216+
And it expects either:
217+
218+
- **If modifying the response:** A [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)
219+
- **If not modifying:** `undefined` (void)
220+
221+
### Skipping
222+
223+
If you want to skip the middleware under certain conditions, just `return` as early as possible:
224+
225+
```ts
226+
onRequest(req) {
227+
if (req.schemaPath !== "/projects/{project_id}") {
228+
return undefined;
229+
}
230+
//
231+
}
232+
```
233+
234+
This will leave the request/response unmodified, and pass things off to the next middleware handler (if any). There’s no internal callback or observer library needed.
235+
236+
### Ejecting middleware
237+
238+
To remove middleware, call `client.eject(middleware)`:
239+
240+
```ts{9}
241+
const myMiddleware = {
242+
// …
243+
};
244+
245+
// register middleware
246+
client.use(myMiddleware);
247+
248+
// remove middleware
249+
client.eject(myMiddleware);
250+
```
251+
252+
For additional guides & examples, see [Middleware & Auth](/openapi-fetch/middleware-auth)

0 commit comments

Comments
 (0)