OrvalOrval

Fetch

Generate Fetch API clients from OpenAPI

The Fetch API is a native browser API requiring no additional dependencies. It also works in server-side frameworks and edge runtimes like Cloudflare Workers, Vercel Edge, and Deno.

Generated code has no runtime dependencies of its own, and every operation is a standalone function, so bundlers can tree-shake unused operations and split the rest by route. See Bundle size for measured numbers.

Configuration

Set the client option to fetch:

orval.config.ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    output: {
      mode: 'tags-split',
      target: 'app/gen/petstore.ts',
      schemas: 'app/gen/models',
      client: 'fetch',
      baseUrl: 'http://localhost:3000',
      mock: true,
    },
    input: {
      target: './petstore.yaml',
    },
  },
});

Runtime base URL

For a host that is only known at runtime (for example process.env.API_BASE_URL in Node or import.meta.env.VITE_* in Vite), use baseUrl.runtime instead of a fixed string. The built-in fetch client embeds the expression in generated URL template literals, so you can keep using override.fetch options such as runtimeValidation and includeHttpResponseReturnType without a custom mutator.

orval.config.ts
baseUrl: {
  runtime: 'process.env.API_BASE_URL',
},

See the output baseUrl reference for imports and expressions such as env.API_BASE_URL from a shared module (for example import { env } from '../../env'). MSW mock host filtering uses the separate mock.baseUrl option.

Set runtimeValidation: { standardSchema: true } to validate through Standard Schema; see standardSchema.

Generated Output

Orval generates three things for each endpoint:

1. Response Type

export type listPetsResponse = {
  data: Pets;
  status: number;
};

2. URL Generator

export const getListPetsUrl = (params?: ListPetsParams) => {
  const normalizedParams = new URLSearchParams();

  Object.entries(params || {}).forEach(([key, value]) => {
    if (value === null) {
      normalizedParams.append(key, 'null');
    } else if (value !== undefined) {
      normalizedParams.append(key, value.toString());
    }
  });

  return `http://localhost:3000/pets?${normalizedParams.toString()}`;
};

3. Fetch Function

export const listPets = async (
  params?: ListPetsParams,
  options?: RequestInit,
): Promise<listPetsResponse> => {
  const res = await fetch(getListPetsUrl(params), {
    ...options,
    method: 'GET',
  });
  const data = await res.json();

  return { status: res.status, data };
};

Bundle size

The fetch client generates plain functions on top of the native Fetch API, so the client code that ships to the browser is limited to what your application calls:

  • No runtime dependencies. Everything, including the shared runtime, is generated into your project.
  • Tree-shaking. Each operation is a separate export, so operations you never import are removed by the bundler.
  • Route-level code splitting. Bundlers split code along module boundaries. With mode: 'tags' or 'tags-split', each tag is its own module, so a lazily loaded page only pulls in the modules for the tags it imports. With mode: 'single' or 'split', all operations live in one module: unused operations are still tree-shaken, but the operations your application imports end up in the same chunk instead of being split by route.

The following sizes were measured for both runtime styles (see Shared Runtime):

MetricStyle50 operations100 operations200 operations
Total bundle, minified + gzippedinline1.42 kB2.26 kB3.89 kB
shared2.29 kB3.05 kB4.50 kB
Total bundle, minifiedinline18.9 kB38.1 kB76.2 kB
shared9.0 kB14.8 kB26.3 kB
First load, minified + gzippedinline1.40 kB1.45 kB1.55 kB
shared2.16 kB2.21 kB2.31 kB

Total bundle is the whole application without code splitting. First load is what the first page downloads when the application is split by route, with 10 operations per lazily loaded page.

Note: These numbers are a snapshot measured with orval 8.47.0 in November 2026, using a synthetic OpenAPI spec in which every operation is used, mode: 'tags-split', and otherwise default options. Bundles were built and minified with esbuild, and gzip sizes use level 9 applied to each output file. They show general trends; actual sizes depend on your spec and configuration.

httpClientInjection: 'factory' returns every operation from a single factory call, so its operations cannot be tree-shaken or split by route. Use it when a single configured client matters more than bundle size.

Custom Fetch Function

Add a custom fetch implementation via mutator:

orval.config.ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    output: {
      // ...
      override: {
        mutator: {
          path: './custom-fetch.ts',
          name: 'customFetch',
        },
      },
    },
    // ...
  },
});

The generated functions will use your custom implementation:

export const listPets = async (
  params?: ListPetsParams,
  options?: RequestInit,
): Promise<listPetsResponse> => {
  return customFetch<Promise<listPetsResponse>>(getListPetsUrl(params), {
    ...options,
    method: 'GET',
  });
};

Shared Runtime

By default every generated function carries its own request and response handling. With override.fetch.runtimeImplementationStyle: 'shared', that logic is generated once into a shared runtime.ts and each operation becomes a thin wrapper around it. Call signatures and the { data, status, headers } response shape are the same as the inline style, and MSW handlers written with getXxxUrl keep working. The query clients and swr under httpClient: 'fetch' use it the same way.

orval.config.ts
override: {
  fetch: {
    runtimeImplementationStyle: 'shared',
    // optional, defaults to runtime.ts next to the target
    runtimeOutputPath: './src/runtime.ts',
  },
},
import { execute, serializeQuery } from './runtime';
import type { CallOptions, ResponseOf } from './runtime';

export type listPetsResponse = ResponseOf<{ 200: Pets; default: Error }>;

export const getListPetsUrl = (params?: ListPetsParams) => {
  const stringifiedParams = serializeQuery(params);

  return stringifiedParams.length > 0 ? `/pets?${stringifiedParams}` : `/pets`;
};

export const listPets = (params?: ListPetsParams, options?: CallOptions) =>
  execute<listPetsResponse>({
    url: getListPetsUrl(params),
    method: 'GET',
    options,
  });

Several outputs can point at the same runtimeOutputPath to share one runtime. A mutator is wired through a per-output client-config.ts and keeps its existing contract, including the schema option that includeZodSchemaInArguments passes it. Keep a shared runtime outside the target directory of any output that uses clean: true with the inline style, since clean wipes that directory.

The per-status type aliases of the inline style (listPetsResponse200, ...) are not generated; use StatusOf<listPetsResponse, 404>['data'] instead.

Choosing between inline and shared

Both styles generate the same call signatures and response shape, so you can switch between them by changing one option. They differ in how the generated code trades download size for parse size (see Bundle size):

  • inline repeats the request handling in every operation. Gzip compresses the repetition very well, so it has the smallest download size and the smallest first load, but the browser has to parse more code after decompressing it.
  • shared generates the request handling once in runtime.ts. Its minified size is roughly a third of inline's for larger specs, so there is less code to parse and execute, and the generated source is much smaller to review.

As a rule of thumb:

  • Choose inline (the default) when download size and the first page load matter most, for example on public sites where many visitors arrive without a cache.
  • Choose shared when your spec is large, when you target low-end devices where parsing JavaScript is expensive, or when you commit the generated code and review its diffs.
  • If you are unsure, shared is the balanced choice: it stays close to inline on download size and is much smaller after minification.

See the runtimeImplementationStyle reference, and the shared runtime example on GitHub, where two OpenAPI documents share one runtime.

Full Example

See the Next.js with Fetch example on GitHub.

On this page