Orval vs openapi-typescript vs Kubb: OpenAPI TypeScript Client Generators 2026
Orval vs openapi-typescript vs Kubb: OpenAPI TypeScript Client Generators 2026
TL;DR
openapi-typescript is the gold standard for type generation — it converts your OpenAPI spec to TypeScript types with zero runtime dependencies, giving you complete control over the HTTP client. Orval is the batteries-included option — it generates React Query/SWR/Axios code with full CRUD operations, mock data, and Zod validation in one config file. Kubb is the new modular powerhouse — it generates multiple output types (TypeScript types, Zod schemas, React Query hooks, MSW mocks) from the same spec via composable plugins. Use openapi-typescript when you want types only; Orval when you want everything generated for you; Kubb when you need maximum flexibility and control over outputs.
Key Takeaways
- openapi-typescript GitHub stars: ~11k — the most popular types-only generator
- Orval GitHub stars: ~7k — the dominant full-stack code generator with React Query integration
- Kubb GitHub stars: ~3.5k — youngest but fastest-growing, especially in enterprise TypeScript teams
- All three consume OpenAPI 3.x specs — works with Swagger 2.0 via upgrade tools
- Orval generates ready-to-use React Query hooks — including mutations, infinite queries, and key factories
- openapi-typescript generates zero-runtime types — just
.d.tsfiles you use withfetchor any HTTP client - Kubb's plugin architecture lets you pick exactly what to generate — types only, or types + schemas + hooks + mocks
The OpenAPI Code Generation Problem
Writing TypeScript types for every API endpoint by hand is tedious and error-prone. OpenAPI specs (formerly Swagger) contain all the information needed to auto-generate:
- TypeScript interfaces for request/response shapes
- Type-safe HTTP client functions
- React Query / SWR hooks with correct type signatures
- Zod schemas for runtime validation
- MSW mock handlers for testing
The three tools here approach this from different angles: types-first (openapi-typescript), full-code-gen (Orval), and modular (Kubb).
openapi-typescript: Types Without the Runtime
openapi-typescript converts an OpenAPI 3.x spec into TypeScript types. It generates zero runtime code — just type definitions. You use these types with openapi-fetch (the companion fetch wrapper) or any HTTP client.
Installation
npm install -D openapi-typescript
npm install openapi-fetch # Optional: typed fetch client
Generate Types
# From a URL
npx openapi-typescript https://api.example.com/openapi.json -o src/api/schema.d.ts
# From a local file
npx openapi-typescript openapi.yaml -o src/api/schema.d.ts
# With npm script
{
"scripts": {
"generate:types": "openapi-typescript openapi.yaml -o src/api/schema.d.ts"
}
}
Using Generated Types with openapi-fetch
// Generated schema.d.ts (excerpt)
interface paths {
"/users": {
get: {
parameters: {
query?: {
page?: number;
limit?: number;
search?: string;
};
};
responses: {
200: {
content: {
"application/json": {
users: components["schemas"]["User"][];
total: number;
page: number;
};
};
};
401: { content: { "application/json": components["schemas"]["Error"] } };
};
};
post: {
requestBody: {
content: {
"application/json": components["schemas"]["CreateUserInput"];
};
};
responses: {
201: { content: { "application/json": components["schemas"]["User"] } };
422: { content: { "application/json": components["schemas"]["ValidationError"] } };
};
};
};
"/users/{id}": {
get: {
parameters: { path: { id: string } };
responses: {
200: { content: { "application/json": components["schemas"]["User"] } };
404: { content: { "application/json": components["schemas"]["Error"] } };
};
};
};
}
// Use with openapi-fetch
import createClient from "openapi-fetch";
import type { paths } from "./api/schema.d.ts";
const client = createClient<paths>({
baseUrl: "https://api.example.com",
headers: {
Authorization: `Bearer ${getToken()}`,
},
});
// Fully typed GET
const { data, error } = await client.GET("/users", {
params: {
query: { page: 1, limit: 20, search: "alice" },
},
});
if (data) {
console.log(data.users[0].email); // TypeScript knows the shape
console.log(data.total);
}
// Typed POST
const { data: newUser, error: createError } = await client.POST("/users", {
body: {
name: "Bob Smith",
email: "bob@example.com",
role: "admin",
},
});
// Path parameters
const { data: user } = await client.GET("/users/{id}", {
params: { path: { id: "user_123" } },
});
React Query with openapi-fetch
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import createClient from "openapi-fetch";
import type { paths } from "./api/schema.d.ts";
const client = createClient<paths>({ baseUrl: "/api" });
// Custom hooks wrapping typed client + React Query
export function useUsers(params?: { page?: number; limit?: number }) {
return useQuery({
queryKey: ["users", params],
queryFn: async () => {
const { data, error } = await client.GET("/users", {
params: { query: params },
});
if (error) throw new Error(error.message);
return data;
},
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (input: paths["/users"]["post"]["requestBody"]["content"]["application/json"]) => {
const { data, error } = await client.POST("/users", { body: input });
if (error) throw error;
return data!;
},
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["users"] }),
});
}
Orval: Full-Stack Code Generation
Orval generates everything from your OpenAPI spec: TypeScript types, Axios/fetch client functions, React Query hooks (including mutations, infinite queries, and suspense queries), Zod schemas, and MSW mock handlers. One config file generates your entire API layer.
Installation
npm install -D orval
npm install axios @tanstack/react-query zod # Runtime deps
Configuration
// orval.config.ts
import { defineConfig } from "orval";
export default defineConfig({
// First output: React Query hooks
userApiHooks: {
input: "./openapi.yaml",
output: {
mode: "tags-split", // Split by OpenAPI tags
target: "src/api/hooks",
schemas: "src/api/model", // Types here
client: "react-query",
override: {
mutator: {
path: "./src/lib/axios-instance.ts",
name: "customInstance", // Use custom Axios instance
},
query: {
useQuery: true,
useInfinite: true, // Generate infinite scroll hooks
useSuspenseQuery: true, // Generate suspense variants
},
},
},
},
// Second output: Zod validation schemas
userApiZod: {
input: "./openapi.yaml",
output: {
target: "src/api/zod-schemas",
client: "zod",
},
},
// Third output: MSW mock handlers for tests
userApiMocks: {
input: "./openapi.yaml",
output: {
target: "src/mocks/handlers",
client: "msw",
},
},
});
Generated React Query Hooks
// Generated by Orval (src/api/hooks/users.ts)
import { useMutation, useQuery, useInfiniteQuery } from "@tanstack/react-query";
import type { MutationFunction, QueryFunction } from "@tanstack/react-query";
import { customInstance } from "../../lib/axios-instance";
import type {
User,
CreateUserInput,
UsersListParams,
UsersListResponse,
} from "../model";
// GET /users — list with filtering and pagination
export const getUsersList = (params?: UsersListParams) =>
customInstance<UsersListResponse>({ url: `/users`, method: "GET", params });
export const getGetUsersListQueryKey = (params?: UsersListParams) =>
[`/users`, ...(params ? [params] : [])] as const;
export const useGetUsersList = <TError = unknown>(
params?: UsersListParams,
options?: { query?: UseQueryOptions<UsersListResponse, TError> }
) => {
return useQuery({
queryKey: getGetUsersListQueryKey(params),
queryFn: () => getUsersList(params),
...options?.query,
});
};
// Infinite query for pagination
export const useGetUsersListInfinite = <TError = unknown>(
params?: Omit<UsersListParams, "page">,
options?: { query?: UseInfiniteQueryOptions<UsersListResponse, TError> }
) => {
return useInfiniteQuery({
queryKey: getGetUsersListQueryKey(params),
queryFn: ({ pageParam = 1 }) => getUsersList({ ...params, page: pageParam }),
getNextPageParam: (lastPage) =>
lastPage.page < Math.ceil(lastPage.total / 20) ? lastPage.page + 1 : undefined,
...options?.query,
});
};
// POST /users — create
export const createUser = (createUserInput: CreateUserInput) =>
customInstance<User>({ url: `/users`, method: "POST", data: createUserInput });
export const useCreateUser = (options?: {
mutation?: UseMutationOptions<User, unknown, CreateUserInput>;
}) => {
return useMutation({
mutationFn: createUser,
...options?.mutation,
});
};
Using Generated Hooks
// Using Orval-generated hooks in a component
import { useGetUsersList, useCreateUser } from "../api/hooks/users";
import { getGetUsersListQueryKey } from "../api/hooks/users";
import { useQueryClient } from "@tanstack/react-query";
function UserList() {
const queryClient = useQueryClient();
const { data, isLoading, error } = useGetUsersList({
page: 1,
limit: 20,
search: "",
});
const { mutate: createUser, isPending } = useCreateUser({
mutation: {
onSuccess: () => {
// Invalidate using generated query key
queryClient.invalidateQueries({
queryKey: getGetUsersListQueryKey(),
});
},
},
});
if (isLoading) return <Skeleton />;
if (error) return <ErrorBoundary error={error} />;
return (
<div>
{data?.users.map((user) => <UserCard key={user.id} user={user} />)}
<button onClick={() => createUser({ name: "New User", email: "new@example.com" })}>
{isPending ? "Creating..." : "Add User"}
</button>
</div>
);
}
Custom Axios Instance
// src/lib/axios-instance.ts — referenced in orval.config.ts
import axios, { AxiosRequestConfig } from "axios";
import { getAuthToken, refreshToken } from "./auth";
const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 10000,
});
axiosInstance.interceptors.request.use((config) => {
const token = getAuthToken();
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
const newToken = await refreshToken();
if (newToken) {
error.config.headers.Authorization = `Bearer ${newToken}`;
return axiosInstance(error.config);
}
}
return Promise.reject(error);
}
);
// Orval calls this function with each request config
export const customInstance = <T>(config: AxiosRequestConfig): Promise<T> =>
axiosInstance(config).then((response) => response.data);
Kubb: Modular Plugin-Based Generation
Kubb is the most flexible of the three — it uses a plugin architecture where each output format is a separate plugin. You compose exactly the outputs you need: TypeScript types, Zod schemas, React Query hooks, SWR hooks, MSW mocks, or all of the above.
Installation
npm install -D @kubb/core @kubb/cli
npm install -D @kubb/plugin-oas # OpenAPI spec parsing
npm install -D @kubb/plugin-ts # TypeScript types
npm install -D @kubb/plugin-zod # Zod schemas
npm install -D @kubb/plugin-react-query # React Query hooks
npm install -D @kubb/plugin-msw # MSW mock handlers
Configuration
// kubb.config.ts
import { defineConfig } from "@kubb/core";
import { pluginOas } from "@kubb/plugin-oas";
import { pluginTs } from "@kubb/plugin-ts";
import { pluginZod } from "@kubb/plugin-zod";
import { pluginReactQuery } from "@kubb/plugin-react-query";
import { pluginMsw } from "@kubb/plugin-msw";
export default defineConfig({
root: ".",
input: { path: "openapi.yaml" },
output: { path: "src/gen", clean: true },
plugins: [
// 1. Parse the OpenAPI spec
pluginOas({ validate: true }),
// 2. Generate TypeScript types
pluginTs({
output: { path: "types" },
enumType: "asConst",
unknownType: "unknown",
}),
// 3. Generate Zod schemas for runtime validation
pluginZod({
output: { path: "zod" },
typed: true,
dateType: "date",
}),
// 4. Generate React Query hooks
pluginReactQuery({
output: { path: "hooks" },
client: {
importPath: "../../lib/api-client",
},
mutation: {
methods: ["post", "put", "patch", "delete"],
},
query: {
methods: ["get"],
infinite: {
queryParam: "page",
initialPageParam: 1,
cursorParam: undefined,
},
},
}),
// 5. Generate MSW handlers for testing
pluginMsw({
output: { path: "mocks" },
handlers: true,
}),
],
});
Generated Output Structure
src/gen/
types/
users.ts ← TypeScript interfaces
models/
User.ts
CreateUserInput.ts
zod/
users.ts ← Zod schemas matching the types
hooks/
users/
useGetUsersList.ts
useGetUser.ts
useCreateUser.ts
useUpdateUser.ts
useDeleteUser.ts
mocks/
users/
getUsersListHandler.ts
getUserHandler.ts
Usage
// Generated types
import type { User, CreateUserInput } from "../gen/types/users";
// Generated Zod schema for runtime validation
import { userSchema, createUserInputSchema } from "../gen/zod/users";
// Parse and validate API response at runtime
const result = userSchema.safeParse(apiResponse);
if (!result.success) {
console.error("Invalid API response:", result.error.format());
}
// Generated React Query hooks
import {
useGetUsersList,
useGetUser,
useCreateUser,
} from "../gen/hooks/users/useGetUsersList";
function UserManager() {
const { data, isLoading } = useGetUsersList({ query: { page: 1 } });
const { mutate: create } = useCreateUser();
return (
<div>
{data?.data.map((user) => <UserRow key={user.id} user={user} />)}
<button onClick={() => create({ requestBody: { name: "New", email: "new@example.com" } })}>
Add User
</button>
</div>
);
}
// Generated MSW handlers for tests
import { http, HttpResponse } from "msw";
import { getUsersListHandler } from "../gen/mocks/users/getUsersListHandler";
const server = setupServer(...getUsersListHandler);
test("shows users", async () => {
render(<UserManager />);
expect(await screen.findByText("Alice")).toBeInTheDocument();
});
Feature Comparison
| Feature | openapi-typescript | Orval | Kubb |
|---|---|---|---|
| TypeScript types | ✅ | ✅ | ✅ |
| React Query hooks | Manual (via openapi-fetch) | ✅ | ✅ (plugin) |
| SWR hooks | Manual | ✅ | ✅ (plugin) |
| Zod schemas | ❌ | ✅ | ✅ (plugin) |
| MSW mock handlers | ❌ | ✅ | ✅ (plugin) |
| Axios integration | Manual | ✅ (custom instance) | ✅ |
| fetch integration | ✅ (openapi-fetch) | ✅ | ✅ |
| Custom HTTP client | ✅ | ✅ | ✅ |
| Infinite query support | Manual | ✅ | ✅ |
| Suspense queries | Manual | ✅ | ✅ |
| OpenAPI 3.x support | ✅ | ✅ | ✅ |
| Swagger 2.0 | ❌ | ✅ | ✅ |
| Plugin architecture | ❌ | ❌ | ✅ |
| Watch mode | ✅ | ✅ | ✅ |
| Runtime dependencies | 0 (types only) | axios + react-query | Depends on plugins |
| GitHub stars | 11k | 7k | 3.5k |
When to Use Each
Choose openapi-typescript if:
- You want maximum control over the HTTP client layer — you choose fetch, ky, axios, or anything else
- You want zero runtime overhead from the generator — only types
- You need a lightweight setup for a project that doesn't use React Query
- Your team prefers writing the hooks manually with strong TypeScript safety
Choose Orval if:
- You want everything generated with minimal configuration — one config file, complete API client
- React Query is your data fetching library and you want hooks auto-generated
- You need MSW mock handlers and Zod validation alongside your hooks
- You're building a standard CRUD-heavy React application
Choose Kubb if:
- You need maximum flexibility — pick exactly which outputs to generate via plugins
- Your project uses an unusual combination (e.g., SWR + Zod, but no React Query)
- You're building a large enterprise codebase and need consistent, predictable output format
- You want to extend generation with custom plugins for your specific patterns
Methodology
Data sourced from GitHub repositories (star counts as of February 2026), npm weekly download statistics (January 2026: openapi-typescript ~500k/week, Orval ~350k/week, Kubb ~150k/week), and official documentation. Generated code examples verified against current versions: openapi-typescript 7.x, Orval 7.x, Kubb 2.x.
Related: tRPC vs REST vs GraphQL for type-safe API design patterns, or Zod v4 vs ArkType vs TypeBox for runtime validation comparison.