Claude Code for Orval: OpenAPI Code Generator for React Apps — Claude Skills 360 Blog
Blog / Backend / Claude Code for Orval: OpenAPI Code Generator for React Apps
Backend

Claude Code for Orval: OpenAPI Code Generator for React Apps

Published: June 4, 2027
Read time: 6 min read
By: Claude Skills 360

Orval generates typed API clients with React Query hooks from OpenAPI specs — orval.config.ts with defineConfig controls everything. output: { mode: "tags-split", target: "src/api/endpoints/", client: "react-query" } generates one file per tag with useGetUsers, useCreateUser hooks. output.client: "axios" generates Axios clients; output.client: "fetch" generates fetch clients. output.override.mutator injects a custom Axios instance with auth interceptors. output.override.operations.createUser.mutator overrides a specific operation. output: { client: "zod", target: "src/api/validations.ts" } generates Zod schemas for runtime validation. MSW mocks: output.mock: true generates handlers for setupServer. orval --watch or generate: { watch: true } auto-regenerates on spec changes. Multiple outputs from one config: separate files for schemas, hooks, and MSW mocks. Claude Code generates Orval configs, React Query hooks, custom Axios mutators, and MSW mock setups.

CLAUDE.md for Orval

## Orval Stack
- Version: orval >= 7.3
- Config: orval.config.ts at project root using defineConfig
- Generate: npx orval (reads orval.config.ts) — generates hooks + types + optionally mocks
- Client react-query: generates useGetResource, useCreateResource hooks with useQuery/useMutation
- Custom mutator: output.override.mutator with custom axios instance for auth headers
- Zod output: separate output entry with client: "zod" for runtime validation schemas
- MSW mocks: output.mock: true generates handlers for msw/node testing
- Watch: npx orval --watch or generate: { watch: true }

Orval Config

// orval.config.ts — Orval code generation config
import { defineConfig } from "orval"

export default defineConfig({
  // ── Primary: React Query hooks ──────────────────────────────────────────
  api: {
    input: {
      target: process.env.OPENAPI_URL ?? "./docs/openapi.yaml",
      validation: true,
    },
    output: {
      mode: "tags-split",        // One file per OpenAPI tag
      target: "src/api/endpoints",
      schemas: "src/api/model",  // Shared type definitions
      client: "react-query",
      httpClient: "axios",
      override: {
        mutator: {
          // Custom axios instance with auth interceptors
          path: "src/lib/axios.ts",
          name: "customAxios",
        },
        query: {
          useQuery: true,
          useInfiniteQuery: true,
          useMutation: true,
          useSuspenseQuery: true,
          signal: true,  // AbortSignal support
        },
        // Per-operation overrides
        operations: {
          // Mark specific endpoints as needing refresh on mutation
          createPost: {
            query: {
              useInfiniteQuery: false,
            },
          },
        },
      },
      // Clean output directory before regenerating
      clean: true,
      prettier: true,
    },
  },

  // ── Zod validation schemas ──────────────────────────────────────────────
  apiZod: {
    input: {
      target: process.env.OPENAPI_URL ?? "./docs/openapi.yaml",
    },
    output: {
      client: "zod",
      target: "src/api/validations.ts",
      override: {
        coerceTypes: true,
      },
    },
  },

  // ── MSW mock handlers for tests ─────────────────────────────────────────
  apiMocks: {
    input: {
      target: "./docs/openapi.yaml",
    },
    output: {
      mode: "single",
      target: "src/mocks/handlers.ts",
      client: "react-query",
      mock: true,
      override: {
        mock: {
          useExamples: true,  // Use OpenAPI examples as mock values
        },
      },
    },
  },
})

Custom Axios Mutator

// src/lib/axios.ts — Orval custom mutator with auth
import Axios, { type AxiosRequestConfig } from "axios"

const BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? "/api"

export const axiosInstance = Axios.create({
  baseURL: BASE_URL,
  timeout: 30_000,
  withCredentials: true,
})

// Attach auth token on every request
axiosInstance.interceptors.request.use((config) => {
  const token = typeof window !== "undefined"
    ? localStorage.getItem("access_token")
    : null

  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }

  return config
})

// Handle 401 — refresh token or redirect
axiosInstance.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config

    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true

      try {
        const refreshToken = localStorage.getItem("refresh_token")
        const { data } = await Axios.post<{ accessToken: string }>(`${BASE_URL}/auth/refresh`, {
          refreshToken,
        })

        localStorage.setItem("access_token", data.accessToken)
        originalRequest.headers.Authorization = `Bearer ${data.accessToken}`

        return axiosInstance(originalRequest)
      } catch {
        localStorage.removeItem("access_token")
        localStorage.removeItem("refresh_token")
        window.location.href = "/sign-in"
      }
    }

    return Promise.reject(error)
  },
)

// This is the mutator function Orval calls for each request
export const customAxios = <T>(config: AxiosRequestConfig): Promise<T> => {
  const source = Axios.CancelToken.source()
  const promise = axiosInstance({ ...config, cancelToken: source.token }).then(
    ({ data }) => data,
  )

  // Allow Orval to cancel in-flight requests
  ;(promise as any).cancel = () => {
    source.cancel("Query was cancelled by Orval")
  }

  return promise
}

export default customAxios

Using Generated Hooks

// components/posts/PostList.tsx — using Orval-generated React Query hooks
"use client"
// Orval generates these from OpenAPI spec
import {
  useGetApiPosts,
  useCreateApiPost,
  useDeleteApiPostsId,
  type Post,
  type CreatePostBody,
} from "@/api/endpoints/posts"
import { useQueryClient } from "@tanstack/react-query"
import { getGetApiPostsQueryKey } from "@/api/endpoints/posts"
import { useState } from "react"

export function PostList() {
  const [page, setPage] = useState(1)
  const queryClient = useQueryClient()

  // Generated hook — fully typed query params and response
  const { data, isLoading, error } = useGetApiPosts({
    page,
    pageSize: 10,
    published: true,
  })

  const { mutateAsync: createPost, isPending } = useCreateApiPost({
    mutation: {
      onSuccess: () => {
        // Invalidate posts list after creation
        queryClient.invalidateQueries({ queryKey: getGetApiPostsQueryKey() })
      },
    },
  })

  const { mutateAsync: deletePost } = useDeleteApiPostsId({
    mutation: {
      onSuccess: () => {
        queryClient.invalidateQueries({ queryKey: getGetApiPostsQueryKey() })
      },
    },
  })

  const handleCreate = async (input: CreatePostBody) => {
    await createPost({ data: input })
  }

  const handleDelete = async (id: string) => {
    await deletePost({ id })
  }

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {(error as any).message}</div>

  return (
    <div className="space-y-4">
      <div className="flex items-center justify-between">
        <h2 className="font-semibold text-lg">Posts ({data?.total ?? 0})</h2>
      </div>

      <div className="divide-y border rounded-xl">
        {(data?.posts ?? []).map((post: Post) => (
          <div key={post.id} className="flex items-center gap-3 p-4">
            <div className="flex-1 min-w-0">
              <p className="font-medium text-sm truncate">{post.title}</p>
              <p className="text-xs text-muted-foreground">{post.slug}</p>
            </div>
            <button
              onClick={() => handleDelete(post.id)}
              className="text-xs text-destructive hover:underline"
            >
              Delete
            </button>
          </div>
        ))}
      </div>

      {/* Pagination */}
      <div className="flex justify-center gap-2">
        <button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1}
          className="px-3 py-1.5 text-sm rounded border disabled:opacity-50">
          Prev
        </button>
        <button onClick={() => setPage(p => p + 1)} disabled={!data || page * 10 >= data.total}
          className="px-3 py-1.5 text-sm rounded border disabled:opacity-50">
          Next
        </button>
      </div>
    </div>
  )
}

MSW Test Setup

// src/mocks/setup.ts — MSW with Orval-generated handlers
import { setupServer } from "msw/node"
// Generated by Orval from OpenAPI spec
import { handlers } from "./handlers"

export const server = setupServer(...handlers)

// vitest.setup.ts:
// import { server } from "./src/mocks/setup"
// beforeAll(() => server.listen({ onUnhandledRequest: "error" }))
// afterEach(() => server.resetHandlers())
// afterAll(() => server.close())
// scripts/generate-api.ts — regenerate on spec change
// package.json:
// {
//   "scripts": {
//     "generate:api": "orval",
//     "generate:api:watch": "orval --watch",
//     "prebuild": "npm run generate:api"
//   }
// }

// .github/workflows/generate-api.yml (ci auto-regen):
// on:
//   push:
//     paths: ["docs/openapi.yaml"]
// jobs:
//   generate:
//     runs-on: ubuntu-latest
//     steps:
//       - uses: actions/checkout@v4
//       - uses: actions/setup-node@v4
//         with: { node-version: '20' }
//       - run: npm ci
//       - run: npm run generate:api
//       - uses: stefanzweifel/git-auto-commit-action@v5
//         with:
//           commit_message: "chore: regenerate API client"
//           file_pattern: "src/api/**"

For the openapi-ts (@hey-api) alternative when a more flexible plugin system with better tree-shaking, a wider range of output shapes, and fetch-native clients without an Axios dependency are preferred — openapi-ts has a cleaner plugin architecture while Orval has better React Query hook generation with more options per endpoint, see the openapi-ts guide. For the Zodios alternative when hand-authoring a typed contract in TypeScript code (rather than generating from a YAML spec) and runtime Zod validation at the HTTP boundary is preferred over code generation — Zodios is better when there’s no OpenAPI spec or the spec is not the source of truth, see the Zodios guide. The Claude Skills 360 bundle includes Orval skill sets covering React Query generation, Zod schemas, and MSW mock handlers. Start with the free tier to try OpenAPI client generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free