← ProgrammingBack to top

Orval: OpenAPI TypeScript Client code generation

Orval is a tool that automatically generates TypeScript API Client based on OpenAPI Schema.

For the Django + DRF + React project, Orval can be automatically generated:

  • TypeScript request and response types.
  • HTTP request function.
  • TanStack Query Hooks。
  • Query Key and Query Options.
  • Mutation Hooks and Mutation Options.
  • Mock data and MSW Handler for front-end testing.

Basic workflow:

Django / DRF
     ↓
drf-spectacular
     ↓
OpenAPI Schema
     ↓
    Orval
     │
     ├── TypeScript Types
     ├── Request Functions
     ├── Query Keys
     ├── Query Options
     ├── Mutation Options
     └── React Query Hooks
               ↓
         React Components

The main value of Orval is to reduce the effort of manually maintaining API types and request code, and to keep the front-end and back-end interfaces consistent.

1. Installation and basic configuration

Installation:

pnpm add -D orval
pnpm add axios @tanstack/react-query

Usually created in the project root directory:

orval.config.ts

Basic configuration

import { defineConfig } from "orval";

export default defineConfig({
  api: {
    input: {
      target: "./openapi.yaml",
    },

    output: {
      target: "./src/api/generated/index.ts",
      schemas: "./src/api/generated/model",
      client: "react-query",
      httpClient: "axios",
      mode: "tags-split",
    },
  },
});

Main configuration:

Configurationfunction
input.targetOpenAPI file path or URL
output.targetGenerated API code output location
output.schemasTypeScript Model output directory
output.clientGenerated client type
output.httpClientHTTP request implementation
output.modeHow generated files are organized
output.mockWhether to generate Mock

The difference between client and httpClient

client: "react-query",
httpClient: "axios",

Meaning:

  • react-query: Generates TanStack Query Hooks and helper functions.
  • axios: The request function uses Axios to send a HTTP request.

The two solve different problems.

2. Generate command

pnpm exec orval

You can join package.json:

{
  "scripts": {
    "api:generate": "orval",
    "build": "tsc -b && vite build"
  }
}

Execution:

pnpm api:generate

Orval will read OpenAPI Schema and generate the corresponding TypeScript code.

It is generally not recommended to edit the makefile directly as modifications will be overwritten when regenerating.

3. operationId

operationId in OpenAPI usually determines the name of the generated function.

For example:

paths:
  /api/v1/user/profile/:
    get:
      operationId: userProfileRetrieve
      responses:
        "200":
          description: Success

May generate:

userProfileRetrieve();

useUserProfileRetrieve();

getUserProfileRetrieveQueryKey();

getUserProfileRetrieveQueryOptions();

The specific function names and available auxiliary functions depend on the Orval version and configuration.

Naming principles

It is recommended to use stable operationId in Django:

@extend_schema(
    operation_id="getUserProfile",
)

This might generate:

getUserProfile();
useGetUserProfile();
getGetUserProfileQueryKey();

Note: Modifying operationId may change the names of all related generated functions, which may lead to front-end TypeScript compilation errors.

Therefore, operationId should be considered part of the public contract of API Client.

4. Normal Request Function

Assuming backend interface:

GET /api/v1/user/profile/

Orval can generate something like:

export const userProfileRetrieve = (
  signal?: AbortSignal,
): Promise<UserProfile> => {
  return axiosInstance<UserProfile>({
    url: "/api/v1/user/profile/",
    method: "GET",
    signal,
  });
};

It is assumed here that axiosInstance<T> is a custom Mutator, and Promise<T> is returned, where T is the response body type.

Request process:

React Component
       ↓
Request Function
       ↓
Axios Instance
       ↓
HTTP Request
       ↓
Django API

Request Function Responsibilities

  • Build API URL.
  • Specify HTTP Method.
  • Pass Query Parameters.
  • Pass Request Body.
  • Specifies the TypeScript return type.
  • Call HTTP Client.

It is generally not responsible for TanStack Query caching by itself.

5. TanStack Query integration

Configuration:

client: "react-query",

Orval generates the TanStack Query code for React.

Its main structure is:

Request Function
       │
       ▼
    QueryFn
       │
       ├── Query Key
       │
       └── Query Options
                │
                ▼
             useQuery
                │
                ▼
          React Component

Note: queryKey, queryFn and queryOptions are configuration relationships that together form Query, not request steps executed sequentially.

6. Query Key

Query Key is the unique key used by TanStack Query to identify the cached data.

Orval usually generates the Query Key factory:

export const getUserProfileRetrieveQueryKey = () => {
  return ["/api/v1/user/profile/"] as const;
};

For interfaces with parameters:

GET /api/v1/pois?city=1&page=2

Query Key Different parameters should be distinguished.

For example:

["/api/v1/pois", { city: 1, page: 2 }];

with:

["/api/v1/pois", { city: 2, page: 2 }];

Should be a different cache entry.

The main purpose of Query Key

  • Identity cache.
  • Read the specified cache.
  • Update the specified cache.
  • Delete cache.
  • Invalidate cache.
  • Controls query refetching.

For example:

queryClient.invalidateQueries({
  queryKey: getUserProfileRetrieveQueryKey(),
});

This will mark the matching Query as stale and re-fetch the data for the active Query in the default configuration.

7. Query Options

Orval may generate:

export const getUserProfileRetrieveQueryOptions = () => {
  return {
    queryKey: getUserProfileRetrieveQueryKey(),

    queryFn: ({ signal }: { signal?: AbortSignal }) =>
      userProfileRetrieve(signal),
  };
};

The actual generated code will also contain complex TypeScript generics, parameters, and overloads.

The role of Query Options

Gather the configuration of TanStack Query in one object:

{
  queryKey,
  queryFn,
  staleTime,
  gcTime,
  enabled,
  retry,
  refetchOnWindowFocus,
}

For example in React:

const options = getUserProfileRetrieveQueryOptions();

const result = useQuery({
  ...options,
  staleTime: 60_000,
});

staleTime represents the time at which the data is considered fresh.

gcTime indicates how long the cache of inactive Query can be retained.

Note: These two parameters are implemented by TanStack Query, Orval just generates the corresponding configuration code.

8. useQuery Hook

Orval can be further generated:

export const useUserProfileRetrieve = () => {
  return useQuery(getUserProfileRetrieveQueryOptions());
};

Real code usually needs to support the additional Query Options and Axios Options.

Use

function UserProfile() {
  const { data, isPending, isError, error } = useUserProfileRetrieve();

  if (isPending) {
    return <div>Loading...</div>;
  }

  if (isError) {
    return <div>{error.message}</div>;
  }

  return <div>{data.name}</div>;
}

TanStack Query is responsible for managing:

  • data: Obtained data.
  • isPending: Whether it is waiting for initial data.
  • isFetching: Whether the request is being executed.
  • isError: Whether an error occurred.
  • error: Error object.
  • refetch: Manually re-request.

Manual Query and Orval Hook

Do not use Orval:

useQuery({
  queryKey: ["user", "profile"],
  queryFn: getUserProfile,
});

Use Orval:

useUserProfileRetrieve();

The latter reduces the effort of repeatedly writing request functions, Query Key, and type definitions.

9. Mutation

Mutation is often used to perform operations with side effects, such as:

  • POST: Created.
  • PUT: Replacement.
  • PATCH: modified.
  • DELETE: Delete.

Assumptions:

PATCH /api/v1/user/profile/

Orval may generate the following code.

Request Function

export const userProfilePartialUpdate = (
  data: PatchedUserProfile,
): Promise<UserProfile> => {
  return axiosInstance<UserProfile>({
    url: "/api/v1/user/profile/",
    method: "PATCH",
    data,
  });
};

Mutation Options

export const getUserProfilePartialUpdateMutationOptions = () => {
  return {
    mutationFn: (variables: { data: PatchedUserProfile }) =>
      userProfilePartialUpdate(variables.data),
  };
};

Mutation Hook

export const useUserProfilePartialUpdate = () => {
  return useMutation(getUserProfilePartialUpdateMutationOptions());
};

Overall relationship:

useMutation
     ↓
Mutation Options
     ↓
mutationFn
     ↓
Request Function
     ↓
Axios
     ↓
Django API

10. Use Mutation

function ProfileEditor() {
  const mutation = useUserProfilePartialUpdate();

  const handleUpdate = () => {
    mutation.mutate({
      data: {
        name: "Alice",
      },
    });
  };

  return (
    <button onClick={handleUpdate} disabled={mutation.isPending}>
      Update
    </button>
  );
}

mutate and mutateAsync

mutation.mutate({
  data: { name: "Alice" },
});

mutate() is used to trigger Mutation, and the result is usually handled through a callback.

And:

await mutation.mutateAsync({
  data: { name: "Alice" },
});

mutateAsync() returns Promise, suitable for scenarios where you need to wait for the operation to complete.

Mutation status

Propertiesmeaning
isIdleNot yet executed
isPendingExecuting
isSuccesssuccess
isErrorfailed
dataData returned successfully
errorerror message
reset()Reset Mutation state

11. Mutation and cache invalidation

After Mutation modifies the server data, the cached Query data may expire.

For example:

GET User Profile
       ↓
Cache User Data
       ↓
PATCH User Profile
       ↓
Server Data Changed
       ↓
Cached Data Outdated

Therefore, it is usually necessary to invalidate the related Query after Mutation succeeds.

invalidateQueries

const queryClient = useQueryClient();

const mutation = useUserProfilePartialUpdate({
  mutation: {
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: getUserProfileRetrieveQueryKey(),
      });
    },
  },
});

Shown here is the common Orval Hook configuration method. The specific parameter structure needs to be based on the generated file.

Why do you need invalidateQueries?

Because the caches of Mutation and Query are managed independently.

Execution:

useUserProfilePartialUpdate();

It does not necessarily mean:

useUserProfileRetrieve();

The cache will be updated automatically.

Orval supports configuration to generate behavior related to cache invalidation, but whether to automatically invalidate it depends on the configuration, and it cannot be assumed by default that all Mutation will automatically update the related Query.

12. setQueryData

In addition to re-requesting, the cache can also be modified directly.

queryClient.setQueryData(getUserProfileRetrieveQueryKey(), updatedProfile);

Suitable for situations where Mutation has returned the latest complete resource.

invalidateQueries and setQueryData

methodfunction
invalidateQueriesMark the cache as invalid and retrieve it conditionally
setQueryDataWrite directly to cache
removeQueriesDelete cache entry
resetQueriesReset Query state

Recommended principles:

  • Mutation only returns success status: usually use invalidateQueries.
  • Mutation returns complete and reliable up-to-date data: consider setQueryData.
  • Multiple queries rely on the same resource: the consistency of related caches needs to be considered at the same time.

13. Axios Mutator

Mutator is a very important extension mechanism of Orval.

It allows using the project's own Axios instance instead of having every generated function call the default Axios directly.

orval.config.ts

export default defineConfig({
  api: {
    input: {
      target: "./openapi.yaml",
    },

    output: {
      target: "./src/api/generated/index.ts",
      client: "react-query",
      httpClient: "axios",

      override: {
        mutator: {
          path: "./src/api/axios-instance.ts",
          name: "axiosInstance",
        },
      },
    },
  },
});

Custom Axios Instance

import Axios, { type AxiosRequestConfig, type AxiosError } from "axios";

const client = Axios.create({
  baseURL: "/",
  withCredentials: true,
  timeout: 30_000,
});

export const axiosInstance = <T>(
  config: AxiosRequestConfig,
  options?: AxiosRequestConfig,
): Promise<T> => {
  return client({
    ...config,
    ...options,
  }).then(({ data }) => data);
};

export type ErrorType<E> = AxiosError<E>;

export type BodyType<T> = T;

withCredentials: true here is useful for cross-origin Cookie requests, but does not automatically implement CSRF protection.

Actual call:

Orval Generated Function
           ↓
      axiosInstance()
           ↓
       Axios Client
           ↓
    Request Interceptor
           ↓
       Django API
           ↓
    Response Interceptor
           ↓
       Response Data

Purpose of Mutator

  • Set Base URL.
  • CARRY Cookie.
  • Add Authorization Header.
  • Process CSRF.
  • Unified timeout settings.
  • Handle API errors.
  • Convert response data.
  • Handle request cancellation.

14. Axios Interceptors

Interceptors can be configured in custom Axios Instance.

Request Interceptor

client.interceptors.request.use((config) => {
  config.headers.set("X-App-Version", "1.0.0");

  return config;
});

Can be used to add public Header.

Response Interceptor

client.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Handle authentication error
    }

    return Promise.reject(error);
  },
);

Note: 401 should not always trigger a jump to the login page. For example, some interfaces may allow anonymous access or are performing login verification.

Global error handling needs to consider business context.

15. AbortSignal: Cancel request

The Query Function generated by Orval can usually pass the AbortSignal provided by TanStack Query to Axios.

For example:

queryFn: ({ signal }) => userProfileRetrieve(signal);

Request function:

axiosInstance({
  url: "/api/v1/user/profile/",
  method: "GET",
  signal,
});

When TanStack Query cancels a request, Axios can use this Signal to abort the corresponding HTTP request.

Common scenarios:

  • The user leaves the page.
  • Query conditions change.
  • Manually cancel Query.
  • The request result is no longer needed.

Note: The browser's abort of the HTTP request does not guarantee that the server has stopped processing the request.

16. TypeScript type generation

Orval generates type TypeScript based on OpenAPI Schema.

For example:

UserProfile:
  type: object
  required:
    - id
    - name
  properties:
    id:
      type: integer
    name:
      type: string
    avatar:
      type: string
      nullable: true

May generate:

export interface UserProfile {
  id: number;
  name: string;
  avatar?: string | null;
}

required and nullable

The two are different:

SchemaTypeScript meaning
requiredProperty must exist
Not requiredAttributes can be omitted
nullableThe attribute value can be null

For example:

name: string;

Indicates required and null is not allowed.

name?: string | null;

Indicates that the attribute can be omitted, null is also allowed.

Limitations of type generation

Orval generates a static type based on Schema and does not automatically validate the runtime data.

For example:

const profile: UserProfile = await userProfileRetrieve();

TypeScript will trust the API function's declared return type, but TypeScript itself cannot discover at runtime if the server actually returns the wrong data structure.

When runtime verification is required, you can use tools such as Zod, or rely on the backend Serializer to ensure the output structure.

17. Output Mode

Orval supports different ways of organizing generated files.

Common patterns:

Modefunction
singleConcentrate generated content into one file
splitSeparate API from Schema
tagsGroup according to Tags
tags-splitGroup and split files based on Tags

For example:

output: {
  mode: "tags-split",
  target: "./src/api/generated/index.ts",
  schemas: "./src/api/generated/model",
}

can generate something like:

src/api/generated/
├── users/
│   └── users.ts
├── pois/
│   └── pois.ts
├── itineraries/
│   └── itineraries.ts
└── model/
    ├── userProfile.ts
    ├── poi.ts
    └── itinerary.ts

The actual directory and file names are determined by the configuration and Schema Tags.

For projects with a large number of interfaces, tags-split is usually more conducive to organizing code by business modules.

18. Mock and MSW

Orval can also generate Mock from OpenAPI Schema.

Configuration:

output: {
  client: "react-query",
  mock: true,
}

Mock can cooperate with MSW (Mock Service Worker) to intercept HTTP requests and return simulated data.

Basic process:

React Component
       ↓
Generated API Function
       ↓
HTTP Request
       ↓
MSW Interceptor
       ↓
Mock Response

Suitable for:

  • The backend interface has not yet been developed.
  • Front-end independent development.
  • UI test.
  • Simulating different API returns results.
  • Test error status and empty data.

Note: Mock can only simulate interface behavior and cannot prove that the real backend is completely consistent with the OpenAPI contract.

19. Boundaries of responsibilities between generated code and handwritten code

This is an important engineering principle when using Orval.

Generated Code

Automatically maintained by Orval:

generated/
├── types
├── request functions
├── query hooks
├── mutation hooks
└── query key factories

Manual modification is not recommended.

Handwritten Code

Maintained by developers:

api/
├── axios-instance.ts
├── csrf.ts
├── query-client.ts
├── generated/
└── services/

Mainly responsible for:

  • Axios Instance and Interceptors.
  • CSRF processing.
  • QueryClient configuration.
  • Business level cache updates.
  • Multi-interface combination.
  • Special error handling.

Orval is responsible for standardized interface code, business processes and cross-interface logic are maintained by developers.

Do not write complex business logic into generated files.

20. Orval and React Hook Form

For form submission, you can combine:

React Hook Form
       ↓
Form Validation
       ↓
handleSubmit()
       ↓
Orval Mutation Hook
       ↓
Axios
       ↓
Django API

For example:

type FormValues = {
  name: string;
};

function ProfileForm() {
  const { register, handleSubmit } = useForm<FormValues>();

  const mutation = useUserProfilePartialUpdate();

  const onSubmit = (values: FormValues) => {
    mutation.mutate({
      data: values,
    });
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} />

      <button type="submit" disabled={mutation.isPending}>
        Save
      </button>
    </form>
  );
}

React Hook Form is responsible for form status and validation, and Mutation Hook generated by Orval is responsible for calling API.

The two have clear responsibilities and can be used directly in combination.

21. Orval and CI/CD

It is recommended that Schema generation and client code generation be included in CI.

Typical process:

Django API Changes
         ↓
      pytest
         ↓
drf-spectacular
         ↓
     openapi.yaml
         ↓
       Orval
         ↓
 Generated TypeScript
         ↓
    TypeScript Check
         ↓
      Vite Build

For example:

# Django Backend
python manage.py spectacular \
  --file openapi.yaml \
  --validate \
  --fail-on-warn

# React Frontend
pnpm api:generate

# TypeScript
pnpm exec tsc -b

# Build
pnpm build

In this way, some interface contract inconsistencies can be discovered during the construction phase.

For example:

Backend changes field name
           ↓
OpenAPI Schema updated
           ↓
Orval regenerates types
           ↓
Existing frontend code fails typecheck
           ↓
CI detects incompatibility

Note: The TypeScript check can only detect problems that can be discovered by the static type system, and does not guarantee that the backend will comply with Schema when running.

22. FAQ

Q1: Does Orval replace Axios?

no.

Orval is the code generation tool, Axios is HTTP Client.

Orval can generate request functions that use Axios, or other HTTP Client can be configured.

Q2: Does Orval replace TanStack Query?

no.

Orval can generate TanStack Query Hooks.

But caching, request status, retry, invalidation and garbage collection are still implemented by TanStack Query.

Q3: Do you need to manually maintain Query Key?

Usually not required.

Orval can generate Query Key according to the interface parameters.

However, cache organization and cross-Query invalidation strategies in complex businesses still need to be designed by developers.

Q4: Will Query be automatically refreshed after Mutation is successful?

It cannot be assumed by default that it will.

Can be used in onSuccess:

queryClient.invalidateQueries(...)

Or enable the generative cache invalidation mechanism according to the Orval version and configuration.

Q5: Why does the generated code contain a lot of generics and overloads?

Mainly to support:

  • Custom response type.
  • Different Query Options.
  • initialData type inference.
  • Wrong type inference.
  • Mutation Variables type.
  • Axios request options.
  • Type system for TanStack Query.

Daily use typically does not require understanding the entire generated code line by line.

The focus is on understanding the relationship between Request Function, Query Key, Query Options, Mutation Options and Hook.

Summary

The core functions of Orval can be divided into four levels:

levelcore content
code generationOpenAPI、operationId、TypeScript Types
HTTP ClientRequest Function、Axios、Mutator
TanStack QueryQuery Key、Query Options、useQuery、useMutation
engineering practiceCache invalidation, Mock, CI/CD, generated code management

The most important overall architecture:

Backend
  Django / DRF
       ↓
  drf-spectacular
       ↓
  OpenAPI Schema
       ↓
      Orval
       ↓
Frontend Generated Code
       │
       ├── Types
       ├── Request Functions
       ├── Query Hooks
       └── Mutation Hooks
                 ↓
          TanStack Query
                 ↓
            React UI

Orval is responsible for generating the code corresponding to the interface contract, Axios is responsible for HTTP communication, TanStack Query is responsible for server state management, and React is responsible for UI.


Reference documentation