Skip to main content

Orval vs openapi-typescript vs Kubb

·PkgPulse Team
0

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

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.