openapi-ts (via @hey-api) generates fully typed TypeScript API clients from OpenAPI 3.x specs — npx @hey-api/openapi-ts -i openapi.yaml -o src/api generates services, models, and types. @hey-api/client-fetch generates a native fetch client; @hey-api/client-axios generates an Axios client. Generated services expose getUsers(), createUser({ body }), deleteUser({ path: { id } }) — all with inferred request/response types. Error types from OpenAPI components/responses are typed too. Configuration via openapi-ts.config.ts controls output directory, service naming, and plugins. openapi-zod-client generates Zod schemas from OpenAPI for runtime validation. @hey-api/sdk generates a modern SDK with tree-shakable functions. The watch flag regenerates on spec changes. --base overrides the base URL. CI/CD runs generation in pipelines with npx @hey-api/openapi-ts. Claude Code generates openapi-ts configs, re-export patterns, mock handlers, and CI workflows.
CLAUDE.md for openapi-ts
## openapi-ts Stack
- Version: @hey-api/openapi-ts >= 0.56, @hey-api/client-fetch >= 0.5
- Generate: npx @hey-api/openapi-ts -i ./openapi.yaml -o src/api/generated
- Config: openapi-ts.config.ts at project root with input/output/plugins
- Plugin fetch: plugins: ["@hey-api/client-fetch"] — native fetch, tree-shakable
- Plugin axios: plugins: ["@hey-api/client-axios"] — axios, interceptors
- Import: import { getUsers, createUser } from "@/api/generated"
- Base URL: client.setConfig({ baseUrl: process.env.NEXT_PUBLIC_API_URL })
Configuration
// openapi-ts.config.ts — openapi-ts configuration
import { defineConfig } from "@hey-api/openapi-ts"
export default defineConfig({
// OpenAPI spec — URL or local file
input: process.env.OPENAPI_URL ?? "./docs/openapi.yaml",
// Output directory
output: {
path: "./src/api/generated",
format: "prettier",
lint: "eslint",
},
// Plugins determine the output shape
plugins: [
"@hey-api/schemas", // JSON schemas from components
"@hey-api/sdk", // Tree-shakable SDK functions
{
name: "@hey-api/client-fetch",
// Throw for non-2xx responses
throwOnError: true,
},
{
name: "@hey-api/typescript",
enums: "typescript", // Generate const enums
},
],
})
Generated Client Usage
// lib/api/client.ts — configure the generated client
import { client } from "@/api/generated"
import { cookies } from "next/headers"
// Initialize once with base URL (server-side)
export function initApiClient(accessToken?: string) {
client.setConfig({
baseUrl: process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000",
headers: accessToken
? { Authorization: `Bearer ${accessToken}` }
: undefined,
})
}
// Browser-side: auto-attach token from storage
if (typeof window !== "undefined") {
client.interceptors.request.use((config) => {
const token = localStorage.getItem("access_token")
if (token) {
config.headers.set("Authorization", `Bearer ${token}`)
}
return config
})
// Handle 401 — refresh or redirect
client.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Try token refresh
const refreshToken = localStorage.getItem("refresh_token")
if (refreshToken) {
try {
const { data } = await fetch("/api/auth/refresh", {
method: "POST",
body: JSON.stringify({ refreshToken }),
}).then(r => r.json())
localStorage.setItem("access_token", data.accessToken)
// Retry original request
return client.request(error.config)
} catch {
localStorage.clear()
window.location.href = "/sign-in"
}
}
}
throw error
},
)
}
Using Generated Services
// lib/api/users.ts — wrapper around generated services
import {
getApiUsers,
postApiUsers,
getApiUsersId,
putApiUsersId,
deleteApiUsersId,
type User,
type CreateUserBody,
type UpdateUserBody,
} from "@/api/generated"
// Typed response wrappers
export async function listUsers(params: { page?: number; per_page?: number } = {}) {
const { data } = await getApiUsers({ query: params })
return data // Inferred as PaginatedUsers type from OpenAPI spec
}
export async function createUser(body: CreateUserBody) {
const { data } = await postApiUsers({ body })
return data // Inferred as User
}
export async function getUserById(id: string) {
const { data } = await getApiUsersId({ path: { id } })
return data // Inferred as User
}
export async function updateUser(id: string, body: UpdateUserBody) {
const { data } = await putApiUsersId({ path: { id }, body })
return data
}
export async function deleteUser(id: string) {
await deleteApiUsersId({ path: { id } })
}
Zod Validator Generation
// scripts/generate-validators.ts — generate Zod from OpenAPI
// Run: npx openapi-zod-client openapi.yaml -o src/api/validators.ts
// Example generated output (openapi-zod-client):
// export const UserSchema = z.object({
// id: z.string().uuid(),
// email: z.string().email(),
// name: z.string().min(1).max(100),
// role: z.enum(["user", "admin"]),
// createdAt: z.string().datetime(),
// })
// Custom config for openapi-zod-client
// package.json script:
// "generate:validators": "openapi-zod-client openapi.yaml -o src/api/validators.ts --export-schemas"
CI/CD Integration
# .github/workflows/generate-api.yml — auto-regenerate on spec change
# name: Regenerate API Client
# 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', cache: 'npm' }
# - run: npm ci
# - run: npx @hey-api/openapi-ts
# - name: Commit generated files
# uses: stefanzweifel/git-auto-commit-action@v5
# with:
# commit_message: "chore: regenerate API client from OpenAPI spec"
# file_pattern: "src/api/generated/**"
// openapi-ts.config.ts — remote spec from running backend
import { defineConfig } from "@hey-api/openapi-ts"
// Optionally fetch spec from running dev server
export default defineConfig({
input: process.env.CI
? "./docs/openapi.yaml" // CI: use committed spec
: "http://localhost:8000/openapi.json", // Local: live spec from API
output: {
path: "./src/api/generated",
format: "prettier",
},
plugins: [
"@hey-api/sdk",
{
name: "@hey-api/client-fetch",
throwOnError: true,
},
{
name: "@hey-api/typescript",
enums: "typescript",
dates: "types+transform", // Auto-transform ISO strings to Date objects
},
],
})
MSW Mocks from OpenAPI
// scripts/generate-mocks.ts — generate MSW handlers from OpenAPI
// package.json: "generate:mocks": "openapi-msw openapi.yaml -o src/mocks/handlers.ts"
// Usage in tests:
// import { handlers } from "@/mocks/handlers"
// import { setupServer } from "msw/node"
// const server = setupServer(...handlers)
// beforeAll(() => server.listen())
// afterEach(() => server.resetHandlers())
// afterAll(() => server.close())
For the Orval alternative when generating React Query hooks, Axios clients, Angular services, or Zod schemas directly from OpenAPI with zero manual wrapping — Orval has more generator plugins and is more opinionated about the output shape, while openapi-ts is more flexible and has better TypeScript quality, see the Orval guide. For the Zodios alternative when a hand-authored, type-safe HTTP client with Zod schema validation at the boundary is preferred over code generation — Zodios lets you define API contracts in TypeScript code rather than generating from a YAML spec, useful when there’s no existing OpenAPI document, see the Zodios guide. The Claude Skills 360 bundle includes openapi-ts skill sets covering codegen, client configuration, and CI integration. Start with the free tier to try API client generation.