title |
---|
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.
Each middleware can provide onRequest()
and onResponse()
callbacks, which can observe and/or mutate requests and responses.
::: code-group
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.
:::
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.
Middleware can also be used to throw an error that fetch()
wouldn’t normally, useful in libraries like TanStack Query:
onResponse({ response }) {
if (response.error) {
throw new Error(response.error.message);
}
}
To remove middleware, call client.eject(middleware)
:
const myMiddleware = {
// …
};
// register middleware
client.use(myMiddleware);
// remove middleware
client.eject(myMiddleware);
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(); // [!code --]
const data = await response.clone().json(); // [!code ++]
return undefined;
},
};
This library is unopinionated and can work with any Authorization setup. But here are a few suggestions that may make working with auth easier.
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.
::: code-group
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");
:::
If authorization isn’t needed for certain routes, you could also handle that with middleware:
::: code-group
const UNPROTECTED_ROUTES = ["/v1/login", "/v1/logout", "/v1/public/"];
const authMiddleware = {
onRequest({ url, request }) {
if (UNPROTECTED_ROUTES.some((pathname) => url.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;
},
};
:::