Skip to content

Middleware #1521

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cool-steaks-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-fetch": minor
---

Add middleware support
5 changes: 5 additions & 0 deletions .changeset/moody-bottles-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-fetch": patch
---

Support arrays in headers
5 changes: 5 additions & 0 deletions .changeset/nasty-comics-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-fetch": minor
---

⚠️ Breaking change (internal): fetch() is now called with new Request() to support middleware (which may affect test mocking)
5 changes: 5 additions & 0 deletions .changeset/old-beans-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-fetch": minor
---

⚠️ **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!
14 changes: 12 additions & 2 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ export default defineConfig({
{
text: "openapi-fetch",
items: [
{ text: "Introduction", link: "/openapi-fetch/" },
{ text: "Getting Started", link: "/openapi-fetch/" },
{
text: "Middleware & Auth",
link: "/openapi-fetch/middleware-auth",
},
{ text: "Testing", link: "/openapi-fetch/testing" },
{ text: "Examples", link: "/openapi-fetch/examples" },
{ text: "API", link: "/openapi-fetch/api" },
{ text: "About", link: "/openapi-fetch/about" },
Expand All @@ -68,7 +73,12 @@ export default defineConfig({
{
text: "openapi-fetch",
items: [
{ text: "Introduction", link: "/openapi-fetch/" },
{ text: "Getting Started", link: "/openapi-fetch/" },
{
text: "Middleware & Auth",
link: "/openapi-fetch/middleware-auth",
},
{ text: "Testing", link: "/openapi-fetch/testing" },
{ text: "Examples", link: "/openapi-fetch/examples" },
{ text: "API", link: "/openapi-fetch/api" },
{ text: "About", link: "/openapi-fetch/about" },
Expand Down
16 changes: 7 additions & 9 deletions docs/6.x/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,13 @@ type FilterKeys<Obj, Matchers> = {
type PathResponses<T> = T extends { responses: any } ? T["responses"] : unknown;
type OperationContent<T> = T extends { content: any } ? T["content"] : unknown;
type MediaType = `${string}/${string}`;
type MockedResponse<T, Status extends keyof T = keyof T> = FilterKeys<
OperationContent<T[Status]>,
MediaType
> extends never
? { status: Status; body?: never }
: {
status: Status;
body: FilterKeys<OperationContent<T[Status]>, MediaType>;
};
type MockedResponse<T, Status extends keyof T = keyof T> =
FilterKeys<OperationContent<T[Status]>, MediaType> extends never
? { status: Status; body?: never }
: {
status: Status;
body: FilterKeys<OperationContent<T[Status]>, MediaType>;
};

/**
* Mock fetch() calls and type against OpenAPI schema
Expand Down
2 changes: 1 addition & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export interface paths {
Which means your type lookups also have to match the exact URL:

```ts
import type{ paths } from "./api/v1";
import type { paths } from "./api/v1";

const url = `/user/${id}`;
type UserResponses = paths["/user/{user_id}"]["responses"];
Expand Down
2 changes: 1 addition & 1 deletion docs/data/contributors.json

Large diffs are not rendered by default.

16 changes: 7 additions & 9 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,13 @@ type FilterKeys<Obj, Matchers> = {
type PathResponses<T> = T extends { responses: any } ? T["responses"] : unknown;
type OperationContent<T> = T extends { content: any } ? T["content"] : unknown;
type MediaType = `${string}/${string}`;
type MockedResponse<T, Status extends keyof T = keyof T> = FilterKeys<
OperationContent<T[Status]>,
MediaType
> extends never
? { status: Status; body?: never }
: {
status: Status;
body: FilterKeys<OperationContent<T[Status]>, MediaType>;
};
type MockedResponse<T, Status extends keyof T = keyof T> =
FilterKeys<OperationContent<T[Status]>, MediaType> extends never
? { status: Status; body?: never }
: {
status: Status;
body: FilterKeys<OperationContent<T[Status]>, MediaType>;
};

/**
* Mock fetch() calls and type against OpenAPI schema
Expand Down
17 changes: 14 additions & 3 deletions docs/openapi-fetch/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,28 @@ description: openapi-fetch Project Goals, comparisons, and more

## Differences

### vs. Axios

[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.

### vs. tRPC

[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.

### vs. openapi-typescript-fetch

This library is identical in purpose to [openapi-typescript-fetch](https://github.com/ajaishankar/openapi-typescript-fetch), but has the following differences:
[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):

- 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`)
- This library has a more terse syntax (`get(…)`) wheras openapi-typescript-fetch requires chaining (`.path(…).method(…).create()`)
- openapi-typescript-fetch supports middleware whereas this library doesn’t

### vs. openapi-typescript-codegen

This library is quite different from [openapi-typescript-codegen](https://github.com/ferdikoomen/openapi-typescript-codegen)
[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.

### vs. Swagger Codegen

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.

## Contributors

Expand Down
104 changes: 102 additions & 2 deletions docs/openapi-fetch/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ createClient<paths>(options);
The following options apply to all request methods (`.GET()`, `.POST()`, etc.)

```ts
client.get("/my-url", options);
client.GET("/my-url", options);
```

| Name | Type | Description |
Expand All @@ -37,6 +37,7 @@ client.get("/my-url", options);
| `bodySerializer` | BodySerializer | (optional) Provide a [bodySerializer](#bodyserializer) |
| `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. |
| `fetch` | `fetch` | Fetch instance used for requests (default: fetch from `createClient`) |
| `middleware` | `Middleware[]` | [See docs](/openapi-fetch/middleware-auth) |
| (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal`, …) ([docs](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options)) |

## querySerializer
Expand Down Expand Up @@ -123,7 +124,7 @@ const client = createClient({
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`:

```ts
const { data, error } = await PUT("/submit", {
const { data, error } = await client.PUT("/submit", {
body: {
name: "",
query: { version: 2 },
Expand All @@ -150,3 +151,102 @@ openapi-fetch supports path serialization as [outlined in the 3.1 spec](https://
| `/users/{.id*}` | label (exploded) | `/users/.5` | `/users/.3.4.5` | `/users/.role=admin.firstName=Alex` |
| `/users/{;id}` | matrix | `/users/;id=5` | `/users/;id=3,4,5` | `/users/;id=role,admin,firstName,Alex` |
| `/users/{;id*}` | matrix (exploded) | `/users/;id=5` | `/users/;id=3;id=4;id=5` | `/users/;role=admin;firstName=Alex` |

## Middleware

Middleware is an object with `onRequest()` and `onResponse()` callbacks that can observe and modify requests and responses.

```ts
import createClient from "openapi-fetch";
import type { paths } from "./api/v1";

const myMiddleware: Middleware = {
async onRequest(req, options) {
// set "foo" header
req.headers.set("foo", "bar");
return req;
},
async onResponse(res, options) {
const { body, ...resOptions } = res;
// change status of response
return new Response(body, { ...resOptions, status: 200 });
},
};

const client = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });

// register middleware
client.use(myMiddleware);
```

### onRequest

```ts
onRequest(req, options) {
// …
}
```

`onRequest()` takes 2 params:

| Name | Type | Description |
| :-------- | :-----------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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) |
| `options` | `MergedOptions` | Combination of [createClient](/openapi-fetch/api#create-client) options + [fetch overrides](/openapi-fetch/api#fetch-options) |

And it expects either:

- **If modifying the request:** A [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request)
- **If not modifying:** `undefined` (void)

### onResponse

```ts
onResponse(res, options) {
// …
}
```

`onResponse()` also takes 2 params:
| Name | Type | Description |
| :-------- | :-----------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `req` | `MiddlewareRequest` | A standard [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). |
| `options` | `MergedOptions` | Combination of [createClient](/openapi-fetch/api#create-client) options + [fetch overrides](/openapi-fetch/api#fetch-options) |

And it expects either:

- **If modifying the response:** A [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)
- **If not modifying:** `undefined` (void)

### Skipping

If you want to skip the middleware under certain conditions, just `return` as early as possible:

```ts
onRequest(req) {
if (req.schemaPath !== "/projects/{project_id}") {
return undefined;
}
// …
}
```

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.

### Ejecting middleware

To remove middleware, call `client.eject(middleware)`:

```ts{9}
const myMiddleware = {
// …
};

// register middleware
client.use(myMiddleware);

// remove middleware
client.eject(myMiddleware);
```

For additional guides & examples, see [Middleware & Auth](/openapi-fetch/middleware-auth)
Loading