Skip to main content

Orval vs openapi-typescript vs Kubb: OpenAPI TypeScript Client Generators 2026

·PkgPulse Team

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.ts files you use with fetch or 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

Featureopenapi-typescriptOrvalKubb
TypeScript types
React Query hooksManual (via openapi-fetch)✅ (plugin)
SWR hooksManual✅ (plugin)
Zod schemas✅ (plugin)
MSW mock handlers✅ (plugin)
Axios integrationManual✅ (custom instance)
fetch integration✅ (openapi-fetch)
Custom HTTP client
Infinite query supportManual
Suspense queriesManual
OpenAPI 3.x support
Swagger 2.0
Plugin architecture
Watch mode
Runtime dependencies0 (types only)axios + react-queryDepends on plugins
GitHub stars11k7k3.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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.