Skip to content

Fix: Orval Not Working — Code Generation Failing, Types Not Matching API, or Hooks Not Updating

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix Orval API client generation issues — OpenAPI spec configuration, custom output targets, React Query and Axios integration, mock generation, type overrides, and CI/CD automation.

The Problem

Orval fails to generate code from your OpenAPI spec:

npx orval
# Error: Unable to resolve $ref: '#/components/schemas/User'

Or generated types don’t match the actual API responses:

// Generated type says `id: number` but API returns `id: string`

Or React Query hooks have incorrect parameter types:

const { data } = useGetUsers({ limit: 10 });
// Type error: 'limit' does not exist in type 'GetUsersParams'

Or generated code is outdated after an API change:

New endpoint added to the API but no hook was generated for it

Why This Happens

Orval generates type-safe API clients from OpenAPI (Swagger) specifications. The generation quality depends entirely on the spec:

  • The OpenAPI spec must be valid and complete — Orval reads the spec file (JSON or YAML) and generates TypeScript types and HTTP client code. Missing $ref targets, invalid schema definitions, or incomplete paths cause generation errors or incorrect types.
  • Orval generates once, then you must re-run — unlike runtime tools, Orval generates static code. API changes aren’t reflected until you re-run npx orval. Stale generated code is the most common “bug.”
  • Output format depends on configuration — Orval can generate Axios clients, React Query hooks, SWR hooks, or plain fetch functions. The orval.config.ts file controls what gets generated. Misconfigured output targets produce unexpected code.
  • Response types follow the spec, not the runtime — if the OpenAPI spec says id: integer but the API actually returns a string, Orval generates id: number. The fix is in the spec, not in Orval.

The deeper failure mode with Orval is that the OpenAPI spec is treated as truth. Most teams treat the spec as documentation — written once, updated when someone remembers. Orval treats it as a contract: the generated client will compile only against what the spec describes. When the spec drifts from the implementation, you get types that lie. The function signature says id: number. The runtime returns "a8c34". Your code passes type-check and crashes at the first JSON parse.

The $ref resolution problem usually surfaces when the spec is composed across multiple files using $ref: "./schemas/user.yaml#/User". Orval can resolve external refs but needs the spec to be parseable as a whole. If the loader can’t follow a relative path (CI checkout vs local dev with different working directories), generation fails. The fix is either to inline all refs into a single bundle file with swagger-cli bundle, or to host the resolved spec at a stable URL and point input.target at the URL.

The third trap is that Orval generates code that depends on optional peers. If you set client: 'react-query', the generated file imports from @tanstack/react-query. If you set httpClient: 'axios', it imports axios. The Orval CLI does not install these packages for you. After a fresh clone, the first generation succeeds but tsc fails because the peers are missing. List Orval’s expected peers in your package.json so they’re installed automatically.

Production Incident Lens: When the Generated Client Lies

The blast radius of Orval failure is every API call your frontend makes. When backend ships a breaking schema change and the frontend hasn’t regenerated, three failure modes appear in rough order of severity:

  1. Type-level break — the new field is required in the schema but absent in the generated types. TypeScript fails at build. The build fails fast, no users affected. This is the best case.
  2. Runtime mismatch with crash — the response shape changed (e.g., users became data.users). The generated client returns the new shape; downstream code accesses users.map(...) on something that’s now an object. The console fills with TypeError: users.map is not a function. Users see broken pages. RUM error rate spikes.
  3. Silent data corruption — the field name changed but the shape is similar. The generated client returns null where the old field used to be a number. Code that uses total ?? 0 silently shows 0 instead of crashing. Users see wrong numbers. Nobody notices for hours.

The on-call signal for case 2 is straightforward: error budget burn from useGetUsers errors, traceable to a specific deploy. Case 3 is dangerous because it doesn’t burn an error budget — it burns trust. Customers report “the dashboard says I have 0 orders, but I just placed one.” Mitigation requires comparing pre- and post-deploy response samples, not just looking at error rates.

The defensible pattern is to regenerate Orval in CI on every PR and fail the PR if the diff is non-empty without a corresponding spec change. Treat the generated client as build output, not source — store the spec, derive the client, and never commit by hand. Add contract tests that hit a staging backend with the production-shaped spec and validate that the runtime responses match what Orval generated. If the spec lies, contract tests catch it before users do.

The deeper SRE principle: generated code is only safe when the source-of-truth is enforced. If the backend can ship a schema change without updating the spec, your frontend has no defense. The fix is upstream: enforce the spec as part of the backend CI, generate the spec from code rather than hand-writing it (drizzle-typebox, zod-openapi, FastAPI’s automatic spec), and treat any divergence as a production incident.

Fix 1: Configure Orval

npm install -D orval
// orval.config.ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    input: {
      // OpenAPI spec source — URL or local file
      target: './openapi.json',
      // Or from a URL:
      // target: 'https://api.myapp.com/openapi.json',
      // Or from Swagger:
      // target: 'https://api.myapp.com/swagger/v1/swagger.json',

      // Validation
      validation: true,

      // Filter — only generate for specific paths
      filters: {
        tags: ['users', 'posts'],  // Only endpoints tagged with these
      },
    },
    output: {
      // Output directory
      target: './src/api/generated.ts',
      // Or split into multiple files:
      // target: './src/api/',
      // mode: 'tags-split',  // One file per tag

      // Client type
      client: 'react-query',  // 'axios' | 'react-query' | 'swr' | 'fetch' | 'angular'

      // HTTP client
      httpClient: 'fetch',  // 'axios' | 'fetch'

      // Override base URL
      baseUrl: '/api',

      // Generate mock data (MSW)
      mock: true,

      // Prettier formatting
      prettier: true,

      // Override specific types
      override: {
        mutator: {
          path: './src/api/custom-fetch.ts',
          name: 'customFetch',
        },
        query: {
          useQuery: true,
          useInfinite: true,
          useSuspenseQuery: true,
        },
      },
    },
  },
});
# Generate
npx orval

# Watch mode — regenerate on spec changes
npx orval --watch

# Generate for a specific config
npx orval --config ./orval.config.ts

Fix 2: React Query Output

// orval.config.ts — React Query v5 output
export default defineConfig({
  api: {
    input: { target: './openapi.json' },
    output: {
      target: './src/api/index.ts',
      client: 'react-query',
      httpClient: 'fetch',
      baseUrl: 'https://api.myapp.com',
      override: {
        query: {
          useQuery: true,
          useSuspenseQuery: true,
          useMutation: true,
        },
      },
    },
  },
});
// Generated output — src/api/index.ts (example)
// Types
export interface User {
  id: string;
  name: string;
  email: string;
}

export interface GetUsersParams {
  page?: number;
  limit?: number;
  search?: string;
}

// React Query hooks
export const useGetUsers = (params?: GetUsersParams, options?) => {
  return useQuery({
    queryKey: getGetUsersQueryKey(params),
    queryFn: () => getUsers(params),
    ...options,
  });
};

export const useCreateUser = (options?) => {
  return useMutation({
    mutationFn: (data: CreateUserBody) => createUser(data),
    ...options,
  });
};

// Usage in components
'use client';

import { useGetUsers, useCreateUser } from '@/api';

function UserList() {
  const { data, isLoading } = useGetUsers({ page: 1, limit: 20 });
  const createUser = useCreateUser();

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      {data?.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
      <button onClick={() => createUser.mutate({
        name: 'Alice',
        email: '[email protected]',
      })}>
        Add User
      </button>
    </div>
  );
}

Fix 3: Custom Fetch Client (Auth, Error Handling)

// src/api/custom-fetch.ts — custom HTTP client
export const customFetch = async <T>(
  url: string,
  options: RequestInit,
): Promise<T> => {
  const token = getAuthToken();

  const response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Content-Type': 'application/json',
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
    },
  });

  if (response.status === 401) {
    // Handle token refresh
    await refreshToken();
    // Retry the request
    return customFetch(url, options);
  }

  if (!response.ok) {
    const error = await response.json().catch(() => ({ message: 'Request failed' }));
    throw new ApiError(response.status, error.message);
  }

  if (response.status === 204) return undefined as T;

  return response.json();
};

class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message);
  }
}
// orval.config.ts — use custom fetch
export default defineConfig({
  api: {
    input: { target: './openapi.json' },
    output: {
      target: './src/api/index.ts',
      client: 'react-query',
      httpClient: 'fetch',
      override: {
        mutator: {
          path: './src/api/custom-fetch.ts',
          name: 'customFetch',
        },
      },
    },
  },
});

Fix 4: Mock Generation (MSW)

// orval.config.ts — generate MSW mocks
export default defineConfig({
  api: {
    input: { target: './openapi.json' },
    output: {
      target: './src/api/index.ts',
      client: 'react-query',
      mock: true,  // Generate MSW handlers
    },
  },
});
// Generated: src/api/index.msw.ts
import { http, HttpResponse } from 'msw';

export const getGetUsersMock = () =>
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: '1', name: 'Alice', email: '[email protected]' },
      { id: '2', name: 'Bob', email: '[email protected]' },
    ]);
  });

export const handlers = [
  getGetUsersMock(),
  getGetUserByIdMock(),
  getCreateUserMock(),
  // ... all endpoints
];

// Use in tests
import { setupServer } from 'msw/node';
import { handlers } from '@/api/index.msw';

const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterAll(() => server.close());

Fix 5: Type Overrides and Transformations

// orval.config.ts — fine-tune generated types
export default defineConfig({
  api: {
    input: { target: './openapi.json' },
    output: {
      target: './src/api/index.ts',
      client: 'react-query',
      override: {
        // Override specific operation names
        operations: {
          listUsers: {
            query: {
              useQuery: true,
              useInfinite: true,  // Generate infinite query hook
              useInfiniteQueryParam: 'cursor',  // Pagination param
            },
          },
        },
        // Transform response types
        components: {
          schemas: {
            // Override a specific schema
            User: {
              // Add custom properties not in the spec
              properties: {
                fullName: { type: 'string' },
              },
            },
          },
        },
      },
    },
  },
});

Fix 6: CI/CD — Auto-Regenerate on API Changes

// package.json
{
  "scripts": {
    "api:generate": "orval",
    "api:check": "orval --check",
    "prebuild": "npm run api:generate"
  }
}
# .github/workflows/api-check.yml
name: API Client Check
on:
  pull_request:
    paths: ['openapi.json', 'orval.config.ts']
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npx orval
      - name: Check for uncommitted changes
        run: |
          git diff --exit-code src/api/ || \
          (echo "Generated API client is out of date. Run 'npx orval' and commit." && exit 1)

Still Not Working?

“Unable to resolve $ref” — the OpenAPI spec has a broken $ref link. Check that all $ref targets exist in components.schemas. If the spec comes from a URL, download it locally and validate with npx @apidevtools/swagger-cli validate openapi.json.

Generated types have unknown everywhere — the OpenAPI spec is missing response schemas. Endpoints without responses.200.content.application/json.schema produce unknown types. Fix the spec or add response schemas.

Hooks re-run npx orval but code doesn’t change — Orval generates deterministic output. If the spec hasn’t changed, the output is identical. Check that the spec file was actually updated. For URL-based specs, Orval fetches on each run but may get a cached response.

“Cannot find module” after generation — the generated file might import from packages you haven’t installed. If client: 'react-query', you need @tanstack/react-query installed. If httpClient: 'axios', you need axios. Check the generated imports and install missing packages.

Generated client compiles but runtime returns 404baseUrl doesn’t match the deployed API. Set baseUrl per environment using a build-time variable: baseUrl: process.env.NEXT_PUBLIC_API_URL. The default Orval baseUrl is empty, which sends requests to the same origin as the page. That’s usually wrong outside of local dev.

Spec changed but PR doesn’t reflect it — the CI regeneration check passes because the spec didn’t change in the PR — but it changed upstream. If you source the spec from a URL, lock it to a content hash or commit a snapshot to the repo. A URL-based spec that drifts silently is a recurring incident waiting to happen.

Generated hooks miss the latest endpointsfilters.tags in input excludes endpoints not in the allowed tag set. If the new endpoint was added without a tag (or with a tag not in the filter), Orval skips it. Either remove the filter or coordinate tag taxonomy with backend.

For related API client issues, see Fix: tRPC Not Working, Fix: TanStack Query Not Working, Fix: MSW Not Working, and Fix: Axios Network Error.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles