Middleware & Auth
Middleware allows you to modify either the request, response, or both for all fetches as well as handling errors thrown by fetch. One of the most common usecases is authentication, but can also be used for logging/telemetry, throwing errors, or handling specific edge cases.
Middleware
Each middleware can provide onRequest(), onResponse() and onError() callbacks, which can observe and/or mutate requests, responses and fetch errors.
import createClient, { type Middleware } from "openapi-typesafe-fetch";
import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typesafe
const myMiddleware: Middleware = {
async onRequest({ request, options }) {
// set "foo" header
request.headers.set("foo", "bar");
return request;
},
async onResponse({ request, response, options }) {
// change status of response; Response fields are getters, so copy them by name
return new Response(response.body, {
status: 200,
statusText: response.statusText,
headers: response.headers,
});
},
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);TIP
The order in which middleware are registered matters. For requests, onRequest() will be called in the order registered. For responses, onResponse() will be called in reverse order. That way the first middleware gets the first “dibs” on requests, and the final control over the end response.
Middleware for one request
A request method also takes a middleware array, for that request only. It runs after the middleware registered with client.use(), by the same order rules: its onRequest() is called last and its onResponse() first. The client's middleware is not replaced.
const { data } = await client.GET("/posts/{id}", {
params: { path: { id: 123 } },
middleware: [showErrorModal],
});Skipping
If you want to skip the middleware under certain conditions, just return as early as possible:
onRequest({ schemaPath }) {
if (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.
Early Response
You can return a Response directly from onRequest, which will skip the actual request and remaining middleware chain. This is useful for cases such as deduplicating or caching responses to avoid unnecessary network requests.
const cache = new Map<string, Response>();
const getCacheKey = (request: Request) => `${request.method}:${request.url}`;
const cacheMiddleware: Middleware = {
onRequest({ request }) {
const key = getCacheKey(request);
const cached = cache.get(key);
if (cached) {
// Return cached response, skipping actual request and remaining middleware chain
return cached.clone();
}
},
onResponse({ request, response }) {
if (response.ok) {
const key = getCacheKey(request);
cache.set(key, response.clone());
}
}
};When a middleware returns a Response:
- The request is not sent to the server
- Subsequent
onRequesthandlers are skipped onResponsehandlers are skipped
Throwing
Middleware can also be used to throw an error that fetch() wouldn’t normally, useful in libraries like TanStack Query:
onResponse({ response }) {
if (!response.ok) {
// Will produce error messages like "https://example.org/api/v1/example: 404 Not Found".
throw new Error(`${response.url}: ${response.status} ${response.statusText}`)
}
}Error Handling
The onError callback allows you to handle errors thrown by fetch. Common errors are TypeErrors which can occur when there is a network or CORS error or DOMExceptions when the request is aborted using an AbortController.
Depending on the return value, onError can handle errors in three different ways:
Return nothing which means that the error will still be thrown. This is useful for logging.
onError({ error }) {
console.error(error);
return;
},Return another instance of Error which is thrown instead of the original error.
onError({ error }) {
return new Error("Oops", { cause: error });
},Return a new instance of Response which means that the fetch call will proceed as successful.
onError({ error }) {
return Response.json({ message: 'nothing to see' });
},TIP
onError does not handle error responses with 4xx or 5xx HTTP status codes, since these are considered "successful" responses but with a bad status code. In these cases you need to check the response's status or ok property via the onResponse callback.
Ejecting middleware
To remove middleware, call client.eject(middleware):
import type { Middleware } from "openapi-typesafe-fetch";
const myMiddleware: Middleware = {
onRequest({ request }) {
// …
},
};
// register middleware
client.use(myMiddleware);
// remove middleware
client.eject(myMiddleware);Handling statefulness
Since middleware uses native Request and Response instances, it’s important to remember that bodies are stateful. This means:
- Create new instances when modifying (
new Request()/new Response()) - Clone when NOT modifying (
res.clone().json())
By default, openapi-typesafe-fetch will NOT arbitrarily clone requests/responses for performance; it’s up to you to create clean copies.
const myMiddleware: Middleware = {
async onResponse({ response }) {
const data = await response.json();
const data = await response.clone().json();
return undefined;
},
};Auth
This library is unopinionated and can work with any Authorization setup. But here are a few suggestions that may make working with auth easier.
Basic auth
This basic example uses middleware to retrieve the most up-to-date token at every request. In our example, the access token is kept in JavaScript module state, which is safe to do for client applications but should be avoided for server applications.
import createClient, { type Middleware } from "openapi-typesafe-fetch";
import type { paths } from "./my-openapi-3-schema";
let accessToken: string | undefined = undefined;
const authMiddleware: Middleware = {
async onRequest({ request }) {
// fetch token, if it doesn’t exist
if (!accessToken) {
const authRes = await someAuthFunc();
if (authRes.accessToken) {
accessToken = authRes.accessToken;
} else {
// handle auth error
}
}
// (optional) add logic here to refresh token when it expires
// add Authorization header to every request
request.headers.set("Authorization", `Bearer ${accessToken}`);
return request;
},
};
const client = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
client.use(authMiddleware);
const authRequest = await client.GET("/some/auth/url");Conditional auth
If authorization isn’t needed for certain routes, you could also handle that with middleware:
import type { Middleware } from "openapi-typesafe-fetch";
// `schemaPath` is the path as the schema writes it, without the `baseUrl` (`/v1`)
const UNPROTECTED_ROUTES = ["/login", "/logout", "/public/"];
const authMiddleware: Middleware = {
onRequest({ schemaPath, request }) {
if (UNPROTECTED_ROUTES.some((pathname) => schemaPath.startsWith(pathname))) {
return undefined; // don’t modify request for certain paths
}
// for all other paths, set Authorization header as expected
request.headers.set("Authorization", `Bearer ${accessToken}`);
return request;
},
};Redirects and custom auth headers
fetch follows redirects by default (redirect: "follow"), and so does this client. On a redirect to another origin, the platform drops only Authorization, Cookie and Proxy-Authorization. Every other header goes on to the new origin, so a key in a custom header (X-API-Key, X-Auth-Token) reaches whatever host the API redirects to. This is the rule of the Fetch standard, not of this client; openapi-fetch behaves the same.
When the API should never redirect to another host, or when you send credentials in a custom header, turn automatic redirects off for the client and handle a redirect yourself:
const client = createClient<paths>({
baseUrl: "https://myapi.dev/v1/",
headers: { "X-API-Key": apiKey },
redirect: "error", // or "manual" to read the 3xx response yourself
});Prefer Authorization for credentials where the API allows it: it is the one header the platform removes on a cross-origin redirect.