API
createClient
createClient accepts the following options, which set the default settings for all subsequent fetch calls.
createClient<paths>(options);| Name | Type | Description |
|---|---|---|
baseUrl | string | Prefix all fetch URLs with this option (e.g. "https://myapi.dev/v1/") |
fetch | fetch | Fetch instance used for requests (default: globalThis.fetch) |
querySerializer | QuerySerializer | (optional) Provide a querySerializer |
bodySerializer | BodySerializer | (optional) Provide a bodySerializer |
pathSerializer | PathSerializer | (optional) Provide a pathSerializer |
| (Fetch options) | Any valid fetch option (headers, mode, cache, signal …) (docs |
The returned client exposes a readonly baseUrl with its trailing slash removed. A per-request baseUrl override applies only to that request.
Fetch options
The following options apply to all request methods (.GET(), .POST(), etc.)
client.GET("/my-url", options);| Name | Type | Description |
|---|---|---|
params | ParamsObject | path and query params for the endpoint |
body | { [name]:value } | requestBody data for the endpoint |
querySerializer | QuerySerializer | (optional) Provide a querySerializer |
bodySerializer | BodySerializer | (optional) Provide a bodySerializer |
pathSerializer | PathSerializer | (optional) Provide a pathSerializer |
parseAs | "json" | "text" | "arrayBuffer" | "blob" | "stream" | (optional) Parse the response using a built-in instance method (default: "json"). "stream" skips parsing altogether and returns the raw stream. |
baseUrl | string | Prefix the fetch URL with this option (e.g. "https://myapi.dev/v1/") |
fetch | fetch | Fetch instance used for requests (default: fetch from createClient) |
middleware | Middleware[] | See docs |
| (Fetch options) | Any valid fetch option (headers, mode, cache, signal, …) (docs) |
wrapAsPathBasedClient
wrapAsPathBasedClient wraps the result of createClient() to return a Proxy-based client that allows path-indexed calls:
const client = createClient<paths>(clientOptions);
const pathBasedClient = wrapAsPathBasedClient(client);
pathBasedClient["/my-url"].GET(fetchOptions);The fetchOptions are the same than for the base client.
A path based client can lead to better type inference but comes at a runtime cost due to the use of a Proxy.
createPathBasedClient is a convenience method combining createClient and wrapAsPathBasedClient if you only want to use the path based call style:
const client = createPathBasedClient<paths>(clientOptions);
client["/my-url"].GET(fetchOptions);Note that it does not allow you to attach middlewares. If you need middlewares, you need to use the full form:
const client = createClient<paths>(clientOptions);
client.use(...);
const pathBasedClient = wrapAsPathBasedClient(client);
client.use(...); // the client reference is shared, so the middlewares will propagate.
pathBasedClient["/my-url"].GET(fetchOptions);querySerializer
OpenAPI supports different ways of serializing objects and arrays for parameters (strings, numbers, and booleans—primitives—always behave the same way). By default, this library serializes arrays using style: "form", explode: true, and objects using style: "deepObject", explode: true, but you can customize that behavior with the querySerializer option (either on createClient() to control every request, or on individual requests for just one).
Object syntax
openapi-fetch ships the common serialization methods out-of-the-box:
| Option | Type | Description |
|---|---|---|
array | SerializerOptions | Set style and explode for arrays (docs). Default: { style: "form", explode: true }. |
object | SerializerOptions | Set style and explode for objects (docs). Default: { style: "deepObject", explode: true }. |
allowReserved | boolean | Set to true to skip URL encoding (⚠️ may break the request) (docs). Default: false. |
const client = createClient({
querySerializer: {
array: {
style: "pipeDelimited", // "form" (default) | "spaceDelimited" | "pipeDelimited"
explode: true,
},
object: {
style: "form", // "form" | "deepObject" (default)
explode: true,
},
},
});Array styles
| Style | Array id = [3, 4, 5] |
|---|---|
| form | /users?id=3,4,5 |
| form (exploded, default) | /users?id=3&id=4&id=5 |
| spaceDelimited | /users?id=3%204%205 |
| spaceDelimited (exploded) | /users?id=3&id=4&id=5 |
| pipeDelimited | /users?id=3|4|5 |
| pipeDelimited (exploded) | /users?id=3&id=4&id=5 |
Object styles
| Style | Object id = {"role": "admin", "firstName": "Alex"} |
|---|---|
| form | /users?id=role,admin,firstName,Alex |
| form (exploded) | /users?role=admin&firstName=Alex |
| deepObject (default) | /users?id[role]=admin&id[firstName]=Alex |
tip
deepObject is always exploded, so it doesn’t matter if you set
explode: trueorexplode: false—it’ll generate the same output.
Alternate function syntax
Sometimes your backend doesn’t use one of the standard serialization methods, in which case you can pass a function to querySerializer to serialize the entire string yourself. You’ll also need to use this if you’re handling deeply-nested objects and arrays in your params:
const client = createClient({
querySerializer(queryParams) {
const search = [];
for (const name in queryParams) {
const value = queryParams[name];
if (Array.isArray(value)) {
for (const item of value) {
search.push(`${name}[]=${encodeURIComponent(item)}`);
}
} else {
search.push(`${name}=${encodeURIComponent(value)}`);
}
}
return search.join(","); // ?tags[]=food,tags[]=california,tags[]=healthy
},
});warning
When serializing yourself, the string will be kept exactly as-authored, so you’ll have to call encodeURI or encodeURIComponent to escape special characters.
bodySerializer
The default serializer preserves FormData, URL-encodes bodies with application/x-www-form-urlencoded, and sends string bodies unchanged for text/*. Other bodies use JSON.stringify, including scalar strings sent as JSON. Content-Type matching ignores case and parameters such as charset.
A custom bodySerializer(body, headers) receives the merged user headers before the default Content-Type is added. For example, use FormData for a multipart request:
const { data, error } = await client.PUT("/submit", {
body: {
name: "",
query: { version: 2 },
},
bodySerializer(body) {
const fd = new FormData();
for (const name in body) {
fd.append(name, body[name]);
}
return fd;
},
});tip
For convenience,
openapi-fetchsetsContent-Typetoapplication/jsonautomatically for any request that provides value for thebodyparameter. When thebodySerializerreturns an instance ofFormData,Content-Typeis omitted, allowing the browser to set it automatically with the correct message part boundary.You can also set
Content-Typemanually throughheadersobject either in the fetch options, or when instantiating the client.
URL-encoded body
To send a body request in application/x-www-form-urlencoded format, which is commonly used to transmit key-value pairs in APIs like OAuth 2.0, pass the appropriate header and body as an object. openapi-fetch will automatically encode the body to the correct format.
const { data, error } = await client.POST("/tokens", {
body: {
clientId: "someClientId",
clientSecret: "someClientSecret",
},
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});pathSerializer
Similar to querySerializer and bodySerializer, pathSerializer allows you to customize how path parameters are serialized. This is useful when your API uses a non-standard path serialization format, or you want to change the default behavior.
Custom Path Serializer
You can provide a custom path serializer when creating the client:
const client = createClient({
pathSerializer(pathname, pathParams) {
let result = pathname;
for (const [key, value] of Object.entries(pathParams)) {
result = result.replace(`{${key}}`, `[${value}]`);
}
return result;
},
});
const { data, error } = await client.GET("/users/{id}", {
params: { path: { id: 5 } },
});
// URL: `/users/[5]`The default path serializer rejects complete . and .. path segments before sending a request because the URL parser would change the route. Values such as file.txt and .well-known remain valid.
Default Path Serializer
openapi-fetch supports path serialization as outlined in the 3.1 spec. This happens automatically, based on the specific format in your OpenAPI schema:
| Template | Style | Primitive id = 5 | Array id = [3, 4, 5] | Object id = {"role": "admin", "firstName": "Alex"} |
|---|---|---|---|---|
/users/{id} | simple (default) | /users/5 | /users/3,4,5 | /users/role,admin,firstName,Alex |
/users/{id*} | simple (exploded) | /users/5 | /users/3,4,5 | /users/role=admin,firstName=Alex |
/users/{.id} | label | /users/.5 | /users/.3,4,5 | /users/.role,admin,firstName,Alex |
/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(), onResponse() and onError() callbacks that can observe and modify requests, responses and errors.
import createClient from "@ternaus/openapi-fetch";
import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript
const myMiddleware: Middleware = {
async onRequest({ request, options }) {
// set "foo" header
request.headers.set("foo", "bar");
return request;
},
async onResponse({ request, response, options }) {
const { body, ...resOptions } = res;
// change status of response
return new Response(body, { ...resOptions, status: 200 });
},
async onError({ error }) {
// wrap errors thrown by fetch
return new Error("Oops, fetch failed", { cause: error });
},
};
const client = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
// register middleware
client.use(myMiddleware);API
Parameters
Each middleware callback receives the following options object with the following:
| Name | Type | Description |
|---|---|---|
request | Request | The current Request to be sent to the endpoint. |
schemaPath | string | The original OpenAPI path called (e.g. /users/{user_id}) |
params | Object | The original params object passed to GET() / POST() / etc. |
id | string | A random, unique ID for this request. |
options | ClientOptions | The readonly options passed to createClient(). |
In addition to these, the onResponse callback receives an additional response property:
| Name | Type | Description |
|---|---|---|
response | Response | The Response returned from the endpoint. |
And the onError callback receives an additional error property:
| Name | Type | Description |
|---|---|---|
error | unknown | The error thrown by fetch, probably a TypeError or a DOMException. |
Response
Each middleware callback can return:
- onRequest: A
Requestto modify the request, aResponseto short-circuit the middleware chain, orundefinedto leave request untouched (skip) - onResponse: Either a
Responseto modify the response, orundefinedto leave it untouched (skip) - onError: Either an
Errorto modify the error that is thrown, aResponsewhich means that thefetchcall will proceed as successful, orundefinedto leave the error untouched (skip)
Ejecting middleware
To remove middleware, call client.eject(middleware):
const myMiddleware = {
// …
};
// register middleware
client.use(myMiddleware);
// remove middleware
client.eject(myMiddleware);For additional guides & examples, see Middleware & Auth