Skip to content

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.

ts
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 });
  },
  async onError({ error }) {
    // wrap errors thrown by fetch
    onError({ error }) {
      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.

Skipping

If you want to skip the middleware under certain conditions, just return as early as possible:

ts
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:

ts
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.

ts
onError({ error }) {
  console.error(error);
  return;
},

Return another instance of Error which is thrown instead of the original error.

ts
onError({ error }) {
  return new Error("Oops", { cause: error });
},

Return a new instance of Response which means that the fetch call will proceed as successful.

ts
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 property or ok() method via the onResponse callback.

Ejecting middleware

To remove middleware, call client.eject(middleware):

ts
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.

ts
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.

ts
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:

ts
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;
  },
};