Skip to content

Commit

Permalink
Middleware 2nd pass
Browse files Browse the repository at this point in the history
  • Loading branch information
drwpow committed Feb 15, 2024
1 parent b174dd6 commit fd44bd2
Show file tree
Hide file tree
Showing 14 changed files with 596 additions and 354 deletions.
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
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
9 changes: 8 additions & 1 deletion docs/.vitepress/theme/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
/**
* Fonts
*/

@font-face {
font-family: "Inter";
font-style: normal;
Expand Down Expand Up @@ -114,6 +113,14 @@ code {
font-variant-ligatures: none;
}

/**
* Base styles
* -------------------------------------------------------------------------- */
pre,
code {
font-variant-ligatures: none;
}

/**
* Component: Button
* -------------------------------------------------------------------------- */
Expand Down
113 changes: 43 additions & 70 deletions docs/openapi-fetch/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +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](#middleware) |
| `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 @@ -154,97 +154,70 @@ openapi-fetch supports path serialization as [outlined in the 3.1 spec](https://

## Middleware

As of `0.9.0` this library supports lightweight middleware. Middleware allows you to modify either the request, response, or both for all fetches.

You can declare middleware as an array of functions on [createClient](#create-client). Each middleware function will be **called twice**—once for the request, then again for the response. On request, they’ll be called in array order. On response, they’ll be called in reverse-array order. That way the first middleware gets the first “dibs” on request, and the final control over responses.

Within your middleware function, you’ll either need to check for `req` (request) or `res` (response) to handle each pass appropriately:
Middleware is an object with `onRequest()` and `onResponse()` callbacks that can observe and modify requests and responses.

```ts
createClient({
middleware: [
async function myMiddleware({
req, // request (undefined for responses)
res, // response (undefined for requests)
options, // all options passed to openapi-fetch
}) {
if (req) {
return new Request(req.url, {
...req,
headers: { ...req.headers, foo: "bar" },
});
} else if (res) {
return new Response({
...res,
status: 200,
});
}
function myMiddleware(): Middleware {
return {
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 });
},
};
}

createClient({
middleware: [myMiddleware()],
});
```

### Request pass

The request pass of each middleware provides `req` that’s a standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) instance, but has 2 additional properties:

| Name | Type | Description |
| :----------- | :------: | :--------------------------------------------------------------- |
| `schemaPath` | `string` | The OpenAPI pathname called (e.g. `/projects/{project_id}`) |
| `params` | `Object` | The [params](#fetch-options) fetch option provided by the client |

### Response pass
::: tip

The response pass returns a standard [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) instance with no modifications.
Middleware can be a simple object. But wrapping in a function like the examples show lets you optionally create multiple instances of the same logic to handle different scenarios if needed.

### Skipping middleware
:::

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

```ts
async function myMiddleware({ req }) {
if (req.schemaPath !== "/projects/{project_id}") {
return;
}

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

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.
`onRequest()` takes 2 params:

### Handling statefulness
| 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` | `MergedOptiosn` | Combination of [createClient](/openapi-fetch/api#create-client) options + [fetch overrides](/openapi-fetch/api#fetch-options) |

When using middleware, it’s important to remember 2 things:
And it expects either:

- **Create new instances** when modifying (e.g. `new Response()`)
- **Clone bodies** before accessing (e.g. `res.clone().json()`)
- **If modifying the request:** A [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request)
- **If not modifying:** `undefined` (void)

This is to account for the fact responses are [stateful](https://developer.mozilla.org/en-US/docs/Web/API/Response/bodyUsed), and if the stream is consumed in middleware [the client will throw an error](https://developer.mozilla.org/en-US/docs/Web/API/Response/clone).
### onResponse

<!-- prettier-ignore -->
```ts
async function myMiddleware({ req, res }) {
// Example 1: modifying request
if (req) {
res.headers.foo = "bar"; // [!code --]
return new Request(req.url, { // [!code ++]
...req, // [!code ++]
headers: { ...req.headers, foo: "bar" }, // [!code ++]
}); // [!code ++]
}

// Example 2: accessing response
if (res) {
const data = await res.json(); // [!code --]
const data = await res.clone().json(); // [!code ++]
}
onResponse(res, options) {
//
}
```

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

And it expects either:

- `querySerializer()` runs _before_ middleware
- This is to save middleware from having to do annoying URL formatting. But remember middleware can access `req.params`
- `bodySerializer()` runs _after_ middleware
- There is some overlap with `bodySerializer()` and middleware. Probably best to use one or the other; not both together.
- **If modifying the response:** A [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)
- **If not modifying:** `undefined` (void)
112 changes: 5 additions & 107 deletions docs/openapi-fetch/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,131 +4,29 @@ title: openapi-fetch Examples

# Examples

## Authentication
Example code of using openapi-fetch with other frameworks and libraries.

Authentication often requires some reactivity dependent on a token. Since this library is so low-level, there are myriad ways to handle it:

### Nano Stores

Here’s how it can be handled using [Nano Stores](https://github.com/nanostores/nanostores), a tiny (334 b), universal signals store:

```ts
// src/lib/api/index.ts
import { atom, computed } from "nanostores";
import createClient from "openapi-fetch";
import type { paths } from "./api/v1";

export const authToken = atom<string | undefined>();
someAuthMethod().then((newToken) => authToken.set(newToken));

export const client = computed(authToken, (currentToken) =>
createClient<paths>({
headers: currentToken ? { Authorization: `Bearer ${currentToken}` } : {},
baseUrl: "https://myapi.dev/v1/",
}),
);
```

```ts
// src/some-other-file.ts
import { client } from "./lib/api";

const { GET, POST } = client.get();

GET("/some-authenticated-url", {
/**/
});
```

### Vanilla JS Proxies

You can also use [proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) which are now supported in all modern browsers:

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

let authToken: string | undefined = undefined;
someAuthMethod().then((newToken) => (authToken = newToken));

const baseClient = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
export default new Proxy(baseClient, {
get(_, key: keyof typeof baseClient) {
const newClient = createClient<paths>({
headers: authToken ? { Authorization: `Bearer ${authToken}` } : {},
baseUrl: "https://myapi.dev/v1/",
});
return newClient[key];
},
});
```

```ts
// src/some-other-file.ts
import client from "./lib/api";

client.GET("/some-authenticated-url", {
/**/
});
```

### Vanilla JS getter

You can also use a [getter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get):

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

let authToken: string | undefined = undefined;
someAuthMethod().then((newToken) => (authToken = newToken));

export default createClient<paths>({
baseUrl: "https://myapi.dev/v1/",
headers: {
get Authorization() {
return authToken ? `Bearer ${authToken}` : undefined;
},
},
});
```

```ts
// src/some-other-file.ts
import client from "./lib/api";

client.GET("/some-authenticated-url", {
/**/
});
```

## Frameworks

openapi-fetch is simple vanilla JS that can be used in any project. But sometimes the implementation in a framework may come with some prior art that helps you get the most out of your usage.

### React + React Query
## React + React Query

[React Query](https://tanstack.com/query/latest) is a perfect wrapper for openapi-fetch in React. At only 13 kB, it provides clientside caching and request deduping across async React components without too much client weight in return. And its type inference preserves openapi-fetch types perfectly with minimal setup.

[View a code example in GitHub](https://github.com/drwpow/openapi-typescript/tree/main/packages/openapi-fetch/examples/react-query)

### Next.js
## Next.js

[Next.js](https://nextjs.org/) is the most popular SSR framework for React. While [React Query](#react--react-query) is recommended for all clientside fetching with openapi-fetch (not SWR), this example shows how to take advantage of Next.js’s [server-side fetching](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#fetching-data-on-the-server-with-fetch) with built-in caching.

[View a code example in GitHub](https://github.com/drwpow/openapi-typescript/tree/main/packages/openapi-fetch/examples/nextjs)

### Svelte / SvelteKit
## Svelte / SvelteKit

[SvelteKit](https://kit.svelte.dev)’s automatic type inference can easily pick up openapi-fetch’s types in both clientside fetching and [Page Data](https://kit.svelte.dev/docs/load#page-data) fetching. And it doesn’t need any additional libraries to work. SvelteKit also advises to use their [custom fetch](https://kit.svelte.dev/docs/load#making-fetch-requests) in load functions. This can be achieved with [fetch options](/openapi-fetch/api#fetch-options).

_Note: if you’re using Svelte without SvelteKit, the root example in `src/routes/+page.svelte` doesn’t use any SvelteKit features and is generally-applicable to any setup._

[View a code example in GitHub](https://github.com/drwpow/openapi-typescript/tree/main/packages/openapi-fetch/examples/sveltekit)

### Vue
## Vue

There isn’t an example app in Vue yet. Are you using it in Vue? Please [open a PR to add it!](https://github.com/drwpow/openapi-typescript/pulls)

Expand Down
4 changes: 2 additions & 2 deletions docs/openapi-fetch/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ And run `npm run test:ts` in your CI to catch type errors.
Use `tsc --noEmit` to check for type errors rather than relying on your linter or your build command. Nothing will typecheck as accurately as the TypeScript compiler itself.
:::

## Usage
## Basic Usage

The best part about using openapi-fetch over oldschool codegen is no documentation needed. openapi-fetch encourages using your existing OpenAPI documentation rather than trying to find what function to import, or what parameters that function wants:

Expand Down Expand Up @@ -128,7 +128,7 @@ const { data, error } = await PUT("/blogposts", {

### Pathname

The pathname of `GET()`, `PUT()`, `POST()`, etc. **must match your schema literally.** Note in the example, the URL is `/blogposts/{post_id}`. This library will replace all `path` params for you (so they can be typechecked)
The pathname of `GET()`, `PUT()`, `POST()`, etc. **must match your schema literally.** Note in the example, the URL is `/blogposts/{post_id}`. This library will quickly replace all `path` params for you (so they can be typechecked).

::: tip

Expand Down

0 comments on commit fd44bd2

Please sign in to comment.