Skip to content

Add multipart requestBody support, update docs #1189

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 2 commits into from
Jun 28, 2023
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/long-moose-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-fetch": minor
---

Add multipart/form-data request body support
5 changes: 5 additions & 0 deletions .changeset/wise-adults-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-fetch": minor
---

Breaking: openapi-fetch now just takes the first media type it finds rather than preferring JSON. This is because in the case of `multipart/form-data` vs `application/json`, it’s not inherently clear which you’d want. Or if there were multiple JSON-like media types.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x, 20.x]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping Node 16 testing cuz it’ll be EOL in 3 months

node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
Expand Down
Binary file modified docs/public/assets/openapi-schema.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/scripts/update-contributors.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const OPENAPI_TS_CONTRIBUTORS = [
]),
];

export const OPENAPI_FETCH_CONTRIBUTORS = [...new Set(["drwpow", "fergusean", "shinzui", "ezpuzz", "KotoriK", "fletchertyler914", "nholik", "roj1512", "nickcaballero", "hd-o", "kecrily"])];
export const OPENAPI_FETCH_CONTRIBUTORS = [...new Set(["drwpow", "fergusean", "shinzui", "ezpuzz", "KotoriK", "fletchertyler914", "nholik", "roj1512", "nickcaballero", "hd-o", "kecrily", "psychedelicious"])];

async function main() {
const openapiTS = Promise.all(OPENAPI_TS_CONTRIBUTORS.map(async (username) => ({ username, avatar: await fetchAvatar(username) })));
Expand Down
77 changes: 50 additions & 27 deletions docs/src/content/docs/openapi-fetch/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ description: openapi-fetch API
createClient<paths>(options);
```

| Name | Type | Description |
| :-------------- | :------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `baseUrl` | `string` | Prefix all fetch URLs with this option (e.g. `"https://myapi.dev/v1/"`). |
| `fetch` | `fetch` | Fetch function used for requests (defaults to `globalThis.fetch`) |
| (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal` …) (<a href="https://developer.mozilla.org/en-US/docs/Web/API/fetch#options" target="_blank" rel="noopener noreferrer">docs</a>) |
| Name | Type | Description |
| :---------------- | :-------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `baseUrl` | `string` | Prefix all fetch URLs with this option (e.g. `"https://myapi.dev/v1/"`). |
| `fetch` | `fetch` | Fetch function used for requests (defaults to `globalThis.fetch`) |
| `querySerializer` | QuerySerializer | (optional) Serialize query params for all requests (default: `new URLSearchParams()`) |
| `bodySerializer` | BodySerializer | (optional) Serialize request body object for all requests (default: `JSON.stringify()`) |
| (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal` …) (<a href="https://developer.mozilla.org/en-US/docs/Web/API/fetch#options" target="_blank" rel="noopener noreferrer">docs</a>) |

## Fetch options

Expand All @@ -25,35 +27,56 @@ The following options apply to all request methods (`.get()`, `.post()`, etc.)
client.get("/my-url", options);
```

| Name | Type | Description |
| :---------------- | :---------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `params` | ParamsObject | Provide `path` and `query` params from the OpenAPI schema |
| `params.path` | `{ [name]: value }` | Provide all `path` params (params that are part of the URL) |
| `params.query` | `{ [name]: value }` | Provide all `query params (params that are part of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/searchParams" target="_blank" rel="noopener noreferrer">searchParams</a> |
| `body` | `{ [name]:value }` | The <a href="https://spec.openapis.org/oas/latest.html#request-body-object" target="_blank" rel="noopener noreferrer">requestBody</a> data, if needed (PUT/POST/PATCH/DEL only) |
| `querySerializer` | QuerySerializer | (optional) Override default param serialization (see [Parameter Serialization](#parameter-serialization)) |
| `parseAs` | `"json"` \| `"text"` \| `"arrayBuffer"` \| `"blob"` \| `"stream"` | Decide how to parse the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Response/body" target="_blank" rel="noopener noreferrer">response body</a>, with `"stream"` skipping processing altogether (default: `"json"`) |
| (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal` …) (<a href="https://developer.mozilla.org/en-US/docs/Web/API/fetch#options" target="_blank" rel="noopener noreferrer">docs</a>) |
| Name | Type | Description |
| :---------------- | :---------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `params` | ParamsObject | Provide `path` and `query` params from the OpenAPI schema |
| `params.path` | `{ [name]: value }` | Provide all `path` params (params that are part of the URL) |
| `params.query` | `{ [name]: value }` | Provide all `query params (params that are part of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/searchParams" target="_blank" rel="noopener noreferrer">searchParams</a> |
| `body` | `{ [name]:value }` | The <a href="https://spec.openapis.org/oas/latest.html#request-body-object" target="_blank" rel="noopener noreferrer">requestBody</a> data, if needed (PUT/POST/PATCH/DEL only) |
| `querySerializer` | QuerySerializer | (optional) Serialize query params for this request only (default: `new URLSearchParams()`) |
| `bodySerializer` | BodySerializer | (optional) Serialize request body for this request only (default: `JSON.stringify()`) |
| `parseAs` | `"json"` \| `"text"` \| `"arrayBuffer"` \| `"blob"` \| `"stream"` | Parse the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Response/body" target="_blank" rel="noopener noreferrer">response body</a>, with `"stream"` skipping processing altogether (default: `"json"`) |
| (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal` …) (<a href="https://developer.mozilla.org/en-US/docs/Web/API/fetch#options" target="_blank" rel="noopener noreferrer">docs</a>) |

### Parameter Serialization
### querySerializer

In the spirit of being lightweight, this library only uses <a href="https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams" target="_blank" rel="noopener noreferrer">URLSearchParams</a> to <a href="https://swagger.io/docs/specification/serialization/" target="_blank" rel="noopener noreferrer">serialize parameters</a>. So for complex query param types (e.g. arrays) you’ll need to provide your own `querySerializer()` method that transforms query params into a URL-safe string:
This library uses <a href="https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams" target="_blank" rel="noopener noreferrer">URLSearchParams</a> to <a href="https://swagger.io/docs/specification/serialization/" target="_blank" rel="noopener noreferrer">serialize query parameters</a>. For complex query param types (e.g. arrays) you’ll need to provide your own `querySerializer()` method that transforms query params into a URL-safe string:

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

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

const { data, error } = await get("/blogposts/{post_id}", {
const { data, error } = await get("/search", {
params: {
path: { post_id: "my-post" },
query: { version: 2 },
query: { tags: ["food", "california", "healthy"] },
},
querySerializer(q) {
let s = "";
for (const [k, v] of Object.entries(q)) {
if (Array.isArray(v)) {
s += `${k}[]=${v.join(",")}`;
} else {
s += `${k}=${v}`;
}
}
return s; // ?tags[]=food&tags[]=california&tags[]=healthy
},
querySerializer: (q) => `v=${q.version}`, // ✅ Still typechecked based on the URL!
});
```

Note that this happens **at the request level** so that you still get correct type inference for that URL’s specific query params.
### bodySerializer

Similar to [querySerializer](#querySerializer), bodySerializer works for requestBody. You probably only need this when using `multipart/form-data`:

_Thanks, [@ezpuzz](https://github.com/ezpuzz)!_
```ts
const { data, error } = await put("/submit", {
body: {
name: "",
query: { version: 2 },
},
bodySerializer(body) {
const fd = new FormData();
for (const [k, v] of Object.entries(body)) {
fd.append(k, v);
}
return fd;
},
});
```
23 changes: 15 additions & 8 deletions docs/src/content/docs/openapi-fetch/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Get Started with openapi-fetch

<img src="/assets/openapi-fetch.svg" alt="openapi-fetch" width="216" height="40" />

openapi-fetch is an ultra-fast fetch client for TypeScript using your OpenAPI schema. Weighs in at **1 kb** and has virtually zero runtime. Works with React, Vue, Svelte, or vanilla JS.
openapi-fetch applies your OpenAPI types to the native fetch API via TypeScript. Weighs in at **1 kb** and has virtually zero runtime. Works with React, Vue, Svelte, or vanilla JS.

| Library | Size (min) |
| :----------------------------- | ---------: |
Expand All @@ -19,7 +19,7 @@ The syntax is inspired by popular libraries like react-query or Apollo client, b
import createClient from "openapi-fetch";
import { paths } from "./v1"; // generated from openapi-typescript

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

// Type-checked request
await put("/blogposts", {
Expand All @@ -30,8 +30,7 @@ await put("/blogposts", {
});

// Type-checked response
const { data, error } = await get("/blogposts/my-blog-post");

const { data, error } = await get("/blogposts/{post_id}", { params: { path: { post_id: "123" } } });
console.log(data.title); // ❌ 'data' is possibly 'undefined'
console.log(error.message); // ❌ 'error' is possibly 'undefined'
console.log(data?.foo); // ❌ Property 'foo' does not exist on type …
Expand Down Expand Up @@ -81,17 +80,17 @@ And run `npm run test:ts` in your CI to catch type errors.

## Usage

Using **openapi-fetch** is as easy as reading your schema! For example, given the following schema:
Using **openapi-fetch** is as easy as reading your schema:

![OpenAPI schema example](/assets/openapi-schema.png)

Here’s how you’d fetch GET `/blogposts/{post_id}` and POST `/blogposts`:
Here’s how you’d fetch GET `/blogposts/{post_id}` and PUT `/blogposts`:

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

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

const { data, error } = await get("/blogposts/{post_id}", {
params: {
Expand All @@ -100,7 +99,7 @@ const { data, error } = await get("/blogposts/{post_id}", {
},
});

const { data, error } = await post("/blogposts", {
const { data, error } = await put("/blogposts", {
body: {
title: "New Post",
body: "<p>New post body</p>",
Expand Down Expand Up @@ -134,3 +133,11 @@ All methods return an object with **data**, **error**, and **response**.
- **error** likewise contains that endpoint’s `4xx`/`5xx` response if the server returned either; otherwise it will be `undefined`
- _Note: `default` will also be interpreted as `error`, since its intent is handling unexpected HTTP codes_
- **response** has response info like `status`, `headers`, etc. It is not typechecked.

## Version Support

openapi-fetch implements the [native fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) which is available in all major browsers.

If using in a Node.js environment, version 18 or greater is recommended (newer is better).

TypeScript support is pretty far-reaching as this library doesn’t use any cutting-edge features, but using the latest version of TypeScript is always recommended for accuracy.
Loading