Middleware & Auth
Middleware allows you to modify either the request, response, or both for all fetches. 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()
and onResponse()
callbacks, which can observe and/or mutate requests and responses.
import createClient from "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 } = response;
// 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);
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.
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.
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}`)
}
}
Ejecting middleware
To remove middleware, call client.eject(middleware)
:
const myMiddleware = {
// …
};
// 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-fetch
will NOT arbitrarily clone requests/responses for performance; it’s up to you to create clean copies.
const myMiddleware: Middleware = {
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-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:
const UNPROTECTED_ROUTES = ["/v1/login", "/v1/logout", "/v1/public/"];
const authMiddleware = {
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;
},
};