Migration Guide: v4 → v5
Kubb v5 introduces a layered architecture that splits responsibilities between adapters, plugins, parsers, and middlewares. This guide lists every user-facing breaking change and shows the matching v5 syntax. Each section follows the same pattern: a short rationale, a before/after diff, and a link to the relevant reference.
TIP
Start with the Upgrade prompt to migrate most configurations automatically, then walk through this page to verify the result.
Upgrade prompt
Copy the prompt below, paste it into any LLM (Claude, ChatGPT, Gemini, …), and append your kubb.config.ts at the end.
Expand upgrade prompt
You are migrating a kubb.config.ts from Kubb v4 to v5.
Apply every rule below in order, then output the complete updated file.
## 1. Import source
- Change: import { defineConfig } from '@kubb/core'
+ To: import { defineConfig } from 'kubb'
## 2. Remove @kubb/plugin-oas from plugins[]
- Remove pluginOas() from the plugins array entirely.
- Move its options (validate, serverIndex, serverVariables, discriminator,
contentType) to a top-level `adapter` key using adapterOas() from
'@kubb/adapter-oas'. If no options were passed, omit the adapter key
(it defaults automatically when importing from `kubb`).
## 3. Move per-plugin schema options to adapterOas
Delete these from every plugin and set them once on adapterOas():
- dateType (from plugin-ts, plugin-faker, and plugin-zod)
- integerType (from plugin-ts, plugin-zod, plugin-faker)
- unknownType (from plugin-ts, plugin-zod, plugin-faker)
- emptySchemaType (from plugin-ts, plugin-zod, plugin-faker)
- enumSuffix (from plugin-ts only)
## 4. Rename transformers.name → resolver.<resolveSpecificName>
- plugin-ts: resolver: { resolveTypeName(name) { return … } }
- plugin-zod: resolver: { resolveSchemaName(name) { return … } }
- all others: resolver: { resolveName(name) { return … } }
Inside a method, call `this.default(name, 'function')` to invoke the
built-in logic as a fallback.
## 5. Rename transformers.schema → transformer
- transformer is now an AST visitor object:
transformer: { schema(node) { return … } }
## 6. plugin-ts specific
- Remove `mapper` (use printer or transformer instead).
- Remove `UNSTABLE_NAMING` (v5 always uses the new naming convention).
## 7. plugin-zod specific
- Remove `version` (always Zod v4 in v5).
- Remove `mapper` (use printer or transformer instead).
- Set zod dependency to ^4.
## 8. Rename output.barrelType → output.barrel (object)
Replace every `barrelType` string with the `barrel` object:
- output.barrelType: 'named' → output.barrel: { type: 'named' }
- output.barrelType: 'all' → output.barrel: { type: 'all' }
- output.barrelType: 'propagate' → output.barrel: { type: 'named', nested: true }
(or { type: 'all', nested: true } if the original intent was wildcard exports)
- output.barrelType: false → output.barrel: false
This applies at both the root output level and per-plugin output levels.
## 9. Preserve everything else
All other plugin options (output, group, include, exclude, override,
generators, contentType, client, infinite, suspense, query, mutation,
paramsCasing, paramsType, pathParamsType, parser, dataReturnType,
clientType, bundle, baseURL, urlType, operations, typed, inferred,
coercion, guidType, mini, wrapOutput, dateParser, regexGenerator,
seed, handlers, etc.) are unchanged.
Now migrate the following kubb.config.ts:Performance
v5 generates code faster than v4. Benchmarks compare @kubb/[email protected] with the v5 kubb meta-package, using write: false to focus on the generation pipeline.
NOTE
Measured on a 4-core Intel Xeon @ 2.80 GHz, Linux. Speedup is the headline. Absolute milliseconds are hardware-dependent.
petStore.yaml, 19 operations
| Plugins | v4 mean | v5 mean | Speedup |
|---|---|---|---|
plugin-ts | 130.53 ms | 66.03 ms | +98% |
plugin-ts + plugin-client | 198.64 ms | 76.77 ms | +159% |
plugin-ts + plugin-client + plugin-zod + plugin-faker | 331.90 ms | 99.07 ms | +235% |
twitter.json, 80 operations, 374 KB
| Plugins | v4 mean | v5 mean | Speedup |
|---|---|---|---|
plugin-ts | 1,486 ms | 375 ms | +296% |
plugin-ts + plugin-client | 1,743 ms | 401 ms | +335% |
plugin-ts + plugin-client + plugin-zod + plugin-faker | 2,997 ms | 711 ms | +322% |
openai.yaml, 242 operations, 2.7 MB (openai/openai-openapi)
| Plugins | v4 mean | v5 mean | Speedup |
|---|---|---|---|
plugin-ts | 6,033 ms | 1,450 ms | +316% |
plugin-ts + plugin-client | 7,662 ms | 1,544 ms | +396% |
plugin-ts + plugin-client + plugin-zod + plugin-faker | 14,943 ms | 2,461 ms | +507% |
The gap widens on bigger specs. In v4, every plugin bootstrapped its own pluginOas instance, so OAS parsing ran once per plugin. The adapterOas in v5 parses the spec once and shares the result across all plugins.
System requirements
| Node.js | ≥ 18 | ≥ 22 |
|---|
Update your CI pipelines, the engines field in package.json, and any Dockerfile FROM node lines. See Installation for the full setup.
Packages
Plugins moved to a separate repository
In v4, every plugin lived in kubb-labs/kubb. In v5 the plugins were extracted into kubb-labs/plugins but keep the same npm package names, so no rename is required.
bun add -d @kubb/plugin-ts @kubb/plugin-zod @kubb/plugin-client \
@kubb/plugin-react-query @kubb/plugin-vue-query @kubb/plugin-swr \
@kubb/plugin-faker @kubb/plugin-msw \
@kubb/plugin-mcp @kubb/plugin-cypress @kubb/plugin-redocpnpm add -D @kubb/plugin-ts @kubb/plugin-zod @kubb/plugin-client \
@kubb/plugin-react-query @kubb/plugin-vue-query @kubb/plugin-swr \
@kubb/plugin-faker @kubb/plugin-msw \
@kubb/plugin-mcp @kubb/plugin-cypress @kubb/plugin-redocnpm install -D @kubb/plugin-ts @kubb/plugin-zod @kubb/plugin-client \
@kubb/plugin-react-query @kubb/plugin-vue-query @kubb/plugin-swr \
@kubb/plugin-faker @kubb/plugin-msw \
@kubb/plugin-mcp @kubb/plugin-cypress @kubb/plugin-redocyarn add -D @kubb/plugin-ts @kubb/plugin-zod @kubb/plugin-client \
@kubb/plugin-react-query @kubb/plugin-vue-query @kubb/plugin-swr \
@kubb/plugin-faker @kubb/plugin-msw \
@kubb/plugin-mcp @kubb/plugin-cypress @kubb/plugin-redocRemoved plugins
The following plugins have no v5 equivalent. Remove them from your config and uninstall the packages.
| v4 package | Status |
|---|---|
@kubb/plugin-solid-query | Vote |
@kubb/plugin-svelte-query | Vote |
NOTE
@kubb/plugin-swr was unavailable during the early v5 betas but is supported again in v5. See @kubb/plugin-swr below.
New packages in v5
| Package | Purpose |
|---|---|
@kubb/adapter-oas | Replaces @kubb/plugin-oas. See Adapters. |
@kubb/middleware-barrel | Barrel-file generation, auto-included via kubb. See Middlewares. |
@kubb/parser-ts | TypeScript and TSX printer, auto-included via kubb. See Parsers. |
Core configuration
Import source
Always import defineConfig from the top-level kubb package. The kubb package wires the OpenAPI adapter, TypeScript parsers, and the barrel middleware automatically.
import { defineConfig } from '@kubb/core'import { } from 'kubb'Layered architecture
v5 introduces three top-level keys that replace behaviour previously embedded in each plugin. When you import from kubb, all three defaults are applied automatically.
| Option | Package | Purpose | Default |
|---|---|---|---|
adapter | @kubb/adapter-oas | Parses the input spec into a universal AST. | adapterOas() |
parsers | @kubb/parser-ts | Converts AST nodes to .ts and .tsx files. | [parserTs, parserTsx] |
middleware | @kubb/middleware-barrel | Post-processes output, like barrel files. | [middlewareBarrel()] |
@kubb/plugin-oas removed
pluginOas() no longer belongs in plugins. Its configuration moves to the top-level adapter key.
import { defineConfig } from '@kubb/core'
import { pluginOas } from '@kubb/plugin-oas'
import { pluginTs } from '@kubb/plugin-ts'
export default defineConfig({
input: { path: './petstore.yaml' },
output: { path: './src/gen' },
plugins: [
pluginOas({
validate: true,
serverIndex: 0,
serverVariables: { env: 'prod' },
discriminator: 'inherit',
}),
pluginTs(),
],
})import { } from 'kubb'
import { } from '@kubb/adapter-oas'
import { } from '@kubb/plugin-ts'
export default ({
: { : './petstore.yaml' },
: { : './src/gen' },
: ({
: true,
: 0,
: { : 'prod' },
: 'inherit',
}),
: [()],
})NOTE
Uninstall @kubb/plugin-oas. The adapter defaults to adapterOas() when importing from kubb, so the adapter: line is only required when you pass options.
output.format and output.lint: new auto-detection
Both options gained an 'auto' value that detects available tools, and 'oxfmt' / 'oxlint' joined the formatter and linter lists.
| Option | New v5 values | Detection order |
|---|---|---|
output.format | 'auto', 'oxfmt' | oxfmt → biome → prettier |
output.lint | 'auto', 'oxlint' | oxlint → biome → eslint |
output.barrelType → output.barrel
The string-based barrelType option is replaced by an object-based barrel option with a type field. At the plugin level, a nested flag replaces the old 'propagate' string.
v4 (old) output.barrelType | v5 (new) output.barrel |
|---|---|
'named' | { type: 'named' } |
'all' | { type: 'all' } |
'propagate' (plugin only) | { type: 'named', nested: true } |
false | false |
import { defineConfig } from '@kubb/core'
export default defineConfig({
input: { path: './petstore.yaml' },
output: { path: './src/gen', barrelType: 'named' },
})import { defineConfig } from 'kubb'
export default defineConfig({
input: { path: './petstore.yaml' },
output: { path: './src/gen', barrel: { type: 'named' } },
})import { defineConfig } from '@kubb/core'
import { pluginTs } from '@kubb/plugin-ts'
export default defineConfig({
input: { path: './petstore.yaml' },
output: { path: './src/gen', barrelType: 'propagate' },
plugins: [pluginTs()],
})import { defineConfig } from 'kubb'
import { pluginTs } from '@kubb/plugin-ts'
export default defineConfig({
input: { path: './petstore.yaml' },
output: { path: './src/gen', barrel: { type: 'named', nested: true } },
plugins: [pluginTs()],
})See @kubb/middleware-barrel for the full barrel option reference.
Logging: --debug replaced by reporters
The --debug flag and the debug value of --logLevel are gone. v5 renders a run through reporters, picked on the CLI with --reporter (comma-separated) or in the config with reporters. The CLI flag overrides the config. Three ship built in:
| Reporter | Output |
|---|---|
cli (default) | The end-of-run summary in the terminal. |
json | A stable machine-readable report on stdout, for CI. |
file | A log written to .kubb/kubb-<timestamp>.log. This replaces --debug. |
kubb generate --debugkubb generate --reporter fileThe kubb:debug hook and the createDebugger helper are removed alongside the flag. See kubb generate for the full flag list and Diagnostics for the structured problem model the reporters render.
Options moved to adapterOas
Schema-level options that previously had to be repeated on every plugin now live on adapterOas and apply globally. Remove them from each plugin and set them once on the adapter.
| Option | Removed from | v5 location |
|---|---|---|
dateType | plugin-ts, plugin-faker, plugin-zod | adapterOas({ dateType }) |
integerType | plugin-ts, plugin-zod, plugin-faker | adapterOas({ integerType }) |
unknownType | plugin-ts, plugin-zod, plugin-faker | adapterOas({ unknownType }) |
emptySchemaType | plugin-ts, plugin-zod, plugin-faker | adapterOas({ emptySchemaType }) |
enumSuffix | plugin-ts | adapterOas({ enumSuffix }) |
IMPORTANT
The default value of integerType changed from 'number' to 'bigint'. OpenAPI int64 fields now map to bigint by default. To keep the previous behavior, set integerType: 'number' explicitly on adapterOas.
import { defineConfig } from '@kubb/core'
import { pluginTs } from '@kubb/plugin-ts'
import { pluginZod } from '@kubb/plugin-zod'
import { pluginFaker } from '@kubb/plugin-faker'
export default defineConfig({
input: { path: './petstore.yaml' },
output: { path: './src/gen' },
plugins: [
pluginTs({
dateType: 'date',
integerType: 'number',
unknownType: 'unknown',
emptySchemaType: 'unknown',
enumSuffix: 'enum',
}),
pluginZod({
dateType: 'date',
integerType: 'number',
unknownType: 'unknown',
}),
pluginFaker({
dateType: 'date',
integerType: 'number',
unknownType: 'unknown',
}),
],
})import { } from 'kubb'
import { } from '@kubb/adapter-oas'
import { } from '@kubb/plugin-ts'
import { } from '@kubb/plugin-zod'
import { } from '@kubb/plugin-faker'
export default ({
: { : './petstore.yaml' },
: { : './src/gen' },
: ({
: 'date',
: 'number',
: 'unknown',
: 'unknown',
: 'enum',
}),
: [(), (), ()],
})Shared plugin API
These changes apply to every plugin that defined transformers in v4.
transformers.name → resolver
The single transformers.name(name, type) callback is replaced by typed resolver methods. The exact method depends on the plugin:
| Plugin | Resolver method |
|---|---|
@kubb/plugin-ts | resolveTypeName(name) |
@kubb/plugin-zod | resolveSchemaName(name) |
@kubb/plugin-client, @kubb/plugin-react-query, @kubb/plugin-vue-query, @kubb/plugin-msw, @kubb/plugin-faker, @kubb/plugin-cypress, @kubb/plugin-mcp | resolveName(name) |
Inside a resolver method, this is bound to the full resolver, so this.default(name, 'function') falls back to the preset logic.
pluginTs({
transformers: {
name: (name) => `Api${name}`,
},
})import { } from '@kubb/plugin-ts'
({
: {
() {
return `Api${this.(, 'function')}`
},
},
})transformers.schema → transformer
Schema-level transformations move to a transformer visitor object. Returning null or undefined from a visitor method falls back to the preset transformer.
pluginZod({
transformers: {
schema: (schema) => ({ ...schema, description: undefined }),
},
})import { } from '@kubb/plugin-zod'
({
: {
() {
return { ..., : }
},
},
})New: printer
Code-generating plugins now accept a printer option for overriding individual AST node renderers. Use it instead of the removed mapper option for type-level customizations.
import ts from 'typescript'
import { } from '@kubb/plugin-ts'
({
: {
: {
() {
return ts..('Date', [])
},
},
},
})Multiple content types
When an OpenAPI operation declares multiple content types for its requestBody, v5 generates a separate type per content type and a union alias. In v4, only the first content type was used.
// plugin-ts output for an operation with application/json + multipart/form-data
export type UploadFileJsonData = { url: string }
export type UploadFileFormData = { file: Blob }
export type UploadFileData = UploadFileJsonData | UploadFileFormDataThe generated client exposes contentType as a typed literal union and defaults to the first declared content type:
uploadFile(petId, data, { contentType: 'multipart/form-data' })Single-content-type operations are unchanged.
@kubb/plugin-ts
See the full reference in @kubb/plugin-ts.
Removed: mapper
pluginTs({ mapper: { status: 'string' } })Use printer.nodes to override specific schema-type renderers, or transformer to rewrite AST nodes before printing.
Moved to adapterOas
dateType, integerType, unknownType, emptySchemaType, and enumSuffix moved to adapterOas. See Options moved to adapterOas.
@kubb/plugin-zod
See the full reference in @kubb/plugin-zod.
Zod v3 no longer supported
The version option ('3' | '4') is removed. v5 always generates Zod v4 schemas.
Upgrade your zod dependency:
bun add zod@^4pnpm add zod@^4npm install zod@^4yarn add zod@^4Removed: mapper
Use transformer or printer instead.
Moved to adapterOas
dateType, integerType, unknownType, and emptySchemaType moved to adapterOas. See Options moved to adapterOas.
New: mini
Generate Zod Mini's functional syntax for better tree-shaking. When mini: true, importPath defaults to 'zod/mini'.
import { } from 'kubb'
import { } from '@kubb/plugin-zod'
export default ({
: { : './petstore.yaml' },
: { : './src/gen' },
: [({ : true })],
})@kubb/plugin-faker
See the full reference in @kubb/plugin-faker.
dateType, integerType, unknownType, and emptySchemaType moved to adapterOas. The transformers.name → resolver.resolveName pattern applies. All other options are unchanged.
@kubb/plugin-client
See the full reference in @kubb/plugin-client.
transformers.name is replaced by resolver.resolveName. The wrapper option is renamed to sdk. All other options are unchanged.
@kubb/plugin-react-query and @kubb/plugin-vue-query
See @kubb/plugin-react-query and @kubb/plugin-vue-query.
transformers.name is replaced by resolver.resolveName. The client sub-object for HTTP client configuration is unchanged. All other options are unchanged.
@kubb/plugin-msw
See the full reference in @kubb/plugin-msw.
transformers.name is replaced by resolver.resolveName. The contentType option moved to adapterOas. All other options are unchanged.
@kubb/plugin-swr
See the full reference in @kubb/plugin-swr.
@kubb/plugin-swr is supported again in v5. It now follows the same conventions as the React Query and Vue Query plugins: transformers.name is replaced by resolver.resolveName, and the client sub-object for HTTP client configuration is unchanged. Because SWR has no enabled option, the param-presence guard is folded into the null-key gate (useSWR(shouldFetch && !!(petId) ? queryKey : null, ...)), so passing undefined disables the request.
Removed plugins: Solid Query, Svelte Query
@kubb/plugin-solid-query and @kubb/plugin-svelte-query have no v5 equivalents. Remove them from your config and uninstall the packages.
Complete before/after example
import { defineConfig, memoryStorage } from '@kubb/core'
import { pluginOas } from '@kubb/plugin-oas'
import { pluginTs } from '@kubb/plugin-ts'
import { pluginZod } from '@kubb/plugin-zod'
import { pluginClient } from '@kubb/plugin-client'
import { pluginReactQuery } from '@kubb/plugin-react-query'
import { pluginFaker } from '@kubb/plugin-faker'
export default defineConfig({
input: { path: './petstore.yaml' },
output: {
path: './src/gen',
format: 'prettier',
storage: memoryStorage(), // → top-level `storage`
},
plugins: [
pluginOas({
// → top-level `adapter` with adapterOas()
validate: true,
serverIndex: 0,
discriminator: 'inherit',
}),
pluginTs({
output: { path: 'types' },
dateType: 'date', // → adapterOas
integerType: 'number', // → adapterOas
unknownType: 'unknown', // → adapterOas
enumSuffix: 'enum', // → adapterOas
UNSTABLE_NAMING: true, // removed (no replacement)
mapper: {}, // removed (use printer or transformer)
transformers: {
name: (name) => `Api${name}`,
},
}),
pluginZod({
output: { path: 'zod' },
version: '3', // removed (always Zod v4 in v5)
dateType: 'string', // → adapterOas
integerType: 'number', // → adapterOas
mapper: {}, // removed
}),
pluginClient({
output: { path: 'clients' },
client: 'axios',
}),
pluginReactQuery({
output: { path: 'hooks' },
client: { importPath: './src/client.ts' },
}),
pluginFaker({
output: { path: 'mocks' },
dateType: 'date', // → adapterOas
integerType: 'number', // → adapterOas
}),
],
})import { } from 'kubb'
import { } from '@kubb/core'
import { } from '@kubb/adapter-oas'
import { } from '@kubb/plugin-ts'
import { } from '@kubb/plugin-zod'
import { } from '@kubb/plugin-client'
import { } from '@kubb/plugin-react-query'
import { } from '@kubb/plugin-faker'
export default ({
: { : './petstore.yaml' },
: {
: './src/gen',
: 'prettier',
},
: (),
: ({
: true,
: 0,
: 'inherit',
: 'date',
: 'number',
: 'unknown',
: 'enum',
}),
: [
({
: { : 'types' },
: {
() {
return `Api${this.(, 'function')}`
},
},
}),
({
: { : 'zod' },
}),
({
: { : 'clients' },
: 'axios',
}),
({
: { : 'hooks' },
: { : './src/client.ts' },
}),
({
: { : 'mocks' },
}),
],
})Generated output changes per plugin
Beyond config changes, v5 also changes what the generators emit. Update any code that imports from the generated files accordingly.
@kubb/plugin-ts
Enums: object literal instead of enum
v5 emits a const-asserted object plus a *Key type union. This avoids the runtime cost of TypeScript enum and is tree-shakable.
export enum ParamsStatusEnum {
placed = 'placed',
approved = 'approved',
delivered = 'delivered',
}
status: ParamsStatusEnumexport enum orderParamsStatusEnum {
placed = 'placed',
approved = 'approved',
delivered = 'delivered',
}
status: OrderParamsStatusEnumKey- Enum names are now operation-scoped (
orderParamsStatusEnum,customerParamsStatusEnum, …) instead of suffix-deduplicated (ParamsStatusEnum,ParamsStatusEnum2, …). Numeric collisions are gone. - Configure with
enumTypeonpluginTsif you needenum,asConst,asPascalConst, orliteral.
int64 maps to bigint by default
adapterOas now defaults integerType to 'bigint'. OpenAPI fields with format: int64 generate bigint instead of number.
- petId?: number
+ petId?: bigintSet integerType: 'number' on adapterOas to restore the previous output.
Open string unions use (string & {})
To preserve IntelliSense suggestions, v5 writes the well-known TypeScript trick.
- status?: 'accepted' | string
+ status?: 'accepted' | (string & {})JSDoc
@type integer | undefined, int64→@type integer | undefined(format suffix removed; format is documented through the schema, not the type comment).@exampleis emitted from the OpenAPIexamplefield.- Object schemas now carry an
@type objectJSDoc tag.
Discriminated unions are factored
Common fields shared by every variant of a oneOf/anyOf are factored out:
- export type Pet =
- | { id?: number; name: string; status?: StatusEnum; ... }
- | { id?: number; name: string; status?: StatusEnum; ... }
+ export type Pet = ({ ... } | { ... }) & {
+ id?: number
+ name: string
+ status?: PetStatusEnumKey
+ ...
+ }@kubb/plugin-zod
Chained syntax instead of functional wrappers
v5 prefers the chained Zod 4 syntax. .optional() always sits at the end of the chain, before .describe().
id: z.optional(z.int()),
shipDate: z.optional(z.iso.datetime()),
status: z.optional(z.enum(['placed', 'approved']).describe('Order Status')),id: z.int().optional(),
shipDate: z.iso.datetime().optional(),
status: z.enum(['placed', 'approved']).optional().describe('Order Status'),The functional form (z.optional(...)) is now reserved for mini: true output, which lives in its own configured output.path.
Self-referencing getters only for true cycles
v4 wrapped almost every nested ref in a getter. v5 only does so when the schema is genuinely circular (a schema that references itself or its parent).
- get category() {
- return categorySchema.optional()
- },
- get tags() {
- return z.array(tagSchema).optional()
- },
+ category: categorySchema.optional(),
+ tags: z.array(tagSchema).optional(),
get parent() {
return z.array(petSchema).optional()
},@kubb/plugin-faker
Stricter return type and intermediate variable
The create prefix is kept in v5 (e.g. createPet stays createPet), matching the naming used by plugin-msw. What changes is the return type and the internal structure:
- export function createPet(data?: Partial<Pet>): Pet {
- return {
- ...{
- id: faker.number.int(),
- ...
- },
- ...(data || {}),
- }
- }
+ export function createPet(data?: Partial<Pet>): Required<Pet> {
+ const defaultFakeData = {
+ id: faker.number.int(),
+ ...
+ }
+ return {
+ ...defaultFakeData,
+ ...(data || {}),
+ } as Required<Pet>
+ }Required<Pet> guarantees that downstream consumers see populated fields even when the schema marks them optional.
@kubb/plugin-client
Operation type names
The naming scheme dropped the Mutation infix and unified status responses under Status<code>.
| v4 type | v5 type |
|---|---|
AddPet200 | AddPetStatus200 |
AddPet405 | AddPetStatus405 |
AddPetMutationRequest | AddPetData |
AddPetMutationResponse | AddPetResponse |
AddPetMutation (container) | removed (see below) |
| did not exist | AddPetResponses |
| did not exist | AddPetRequestConfig |
The single AddPetMutation aggregate is replaced by three explicit types:
export type AddPetRequestConfig = {
data?: AddPetData
pathParams?: never
queryParams?: never
headerParams?: never
url: '/pet'
}
export type AddPetResponses = {
'200': AddPetStatus200
'405': AddPetStatus405
}
export type AddPetResponse = AddPetStatus200 | AddPetStatus405GET operation example:
export type GetPetQueryParams = { limit?: number; offset?: number }
export type GetPetRequestConfig = {
data?: never
pathParams?: { petId: string }
queryParams?: GetPetQueryParams
headerParams?: never
url: '/pet/{petId}'
}
export type GetPetResponses = { '200': Pet; '404': ErrorResponse }
export type GetPetResponse = Pet | ErrorResponseThis naming pattern applies consistently across all HTTP methods and is inherited by plugin-react-query, plugin-vue-query, plugin-cypress, plugin-msw, and plugin-mcp.
Client return type narrows to 2xx responses
The generic on the generated client function now references the union of 2xx response status types (AddPetStatus200) instead of the full response alias (AddPetResponse). The returned Promise resolves to the success body only; non-2xx responses surface through the client's error path.
- const res = await request<AddPetResponse, ResponseErrorConfig<AddPetStatus405>, AddPetData>({ ... })
+ const res = await request<AddPetStatus200, ResponseErrorConfig<AddPetStatus405>, AddPetData>({ ... })AddPetResponse, AddPetResponses, and the per-status AddPetStatus<code> aliases are still emitted by plugin-ts; only the generic threaded into the client changes.
This matches the default behavior of axios, ky, and Kubb's bundled fetch client, which all throw on non-2xx. If you pass raw native fetch as the client without a throwing wrapper, narrow with a type guard at the call site or wrap the client to throw on error responses. The previous union type masked the same runtime mismatch.
Bundled client runtime exports client
The bundled HTTP client runtime exports its request function as client for both the axios and fetch adapters. This name is consistent across bundled and non-bundled output (@kubb/plugin-client/clients/fetch, @kubb/plugin-client/clients/axios, and the generated .kubb/client.ts), so the generated root barrel re-exports a valid client symbol. The bundled file is always written to .kubb/client.ts; @kubb/plugin-react-query, @kubb/plugin-vue-query, and @kubb/plugin-mcp previously emitted .kubb/fetch.ts.
Generated code imports the runtime as a default import, so most projects need no changes. If you import the request function as a named export, rename it to client:
- import { fetch } from '@kubb/plugin-client/clients/fetch'
+ import { client } from '@kubb/plugin-client/clients/fetch'The default import can still bind to any local name:
import client from '@kubb/plugin-client/clients/fetch'@kubb/plugin-react-query and @kubb/plugin-vue-query
The exported *MutationKey type alias is gone. Keep using the runtime helper if you need the key:
- export type CreateUserMutationKey = ReturnType<typeof createUserMutationKey>
- export const createUserMutationKey = () => [{ url: '/user' }] as const
+ export const createUserMutationKey = () => [{ url: '/user' }] as constAll other generated APIs only inherit the renames from plugin-client (*Data, *Response, *Status<code>).
Mutation and query TData narrows to 2xx responses
The TData generic on useMutation, useQuery, useInfiniteQuery, useSuspenseQuery, and their *Options helpers now references the union of 2xx response status types instead of the full response alias. This aligns with TanStack Query's contract that TData is the resolved success value and errors flow through TError.
export function useAddPet<TContext>(
options: {
mutation?: MutationObserverOptions<
- AddPetResponse,
+ AddPetStatus200,
ResponseErrorConfig<AddPetStatus405>,
{ data: AddPetData },
TContext
> & { client?: QueryClient }
client?: Partial<RequestConfig<AddPetData>> & { client?: typeof client }
} = {},
) { /* ... */ }Call sites that previously needed as casts or 'id' in res checks compile directly:
const pet = await mutateAsync({ data: { name: 'Rex' } })
pet.id // typed as Pet.id — no narrowing requiredThe change applies to queryFn, queryOptions, and the hook generics in a single pass. No config flag toggles the old behavior. If your client returns non-2xx bodies as resolved data instead of throwing, wrap it to throw on error responses so TanStack Query's error / onError path fires correctly. The previous typing made this silently broken at runtime.
enabled-guarded params are now optional
*QueryOptions and *InfiniteQueryOptions emit an enabled guard derived from the required path and query parameters (enabled: !!petId in React Query, enabled: () => !!toValue(petId) in Vue Query). In v4 those parameters stayed required in the generated type, so a caller could never pass undefined to reach the disabled state the guard already implements. The type contradicted the runtime.
v5 makes those parameters optional in the generated queryKey, queryOptions, and hook signatures, and the queryFn calls the client with a non-null assertion. The enabled guard is unchanged.
- export function getPetByIdQueryOptions({ petId }: { petId: GetPetByIdPathPetId }, config: Partial<RequestConfig> & { client?: Client } = {}) {
+ export function getPetByIdQueryOptions({ petId }: { petId?: GetPetByIdPathPetId } = {}, config: Partial<RequestConfig> & { client?: Client } = {}) {
const queryKey = getPetByIdQueryKey({ petId })
return queryOptions<GetPetByIdStatus200, ResponseErrorConfig<GetPetByIdStatus400 | GetPetByIdStatus404>, GetPetByIdStatus200, typeof queryKey>({
enabled: !!petId,
queryKey,
queryFn: async ({ signal }) => {
- return getPetById({ petId }, { ...config, signal: config.signal ?? signal })
+ return getPetById({ petId: petId! }, { ...config, signal: config.signal ?? signal })
},
})
}You can now pass a not-yet-available value (for example a route param or the result of a dependent query) and rely on the existing guard to keep the query disabled until it resolves:
// type-checks in v5; the query stays disabled until petId is defined
useGetPetById({ petId: route.params.petId })NOTE
This is a type-only change. The ? and ! are erased at compile time, so the emitted JavaScript (including the enabled guard) is identical to v4. Suspense hooks cannot be disabled, so their parameters stay required.
@kubb/plugin-msw
Handlers are now strongly typed against the request body and headers, and accept an HttpResponseResolver callback instead of an inline MSW handler signature.
export function createUserHandler(
data?: string | number | boolean | null | object | ((info: Parameters<Parameters<typeof http.post>[1]>[0]) => Response | Promise<Response>),
) {
return http.post('http://localhost:3000/user', function handler(info) {
...
})
}import type { HttpResponseResolver } from 'msw'
import type { CreateUserData } from '../../../models/CreateUser.ts'
export function createUserHandler(
data?: string | number | boolean | null | object | HttpResponseResolver<Record<string, string>, CreateUserData, any>,
) {
return http.post<Record<string, string>, CreateUserData, any>(`http://localhost:3000/user`, function handler(info) {
...
})
}@kubb/plugin-cypress
- HTTP method constants are uppercased (
'post'→'POST'). - Imports follow the new
*Data/*Responsenaming.
- import type { AddPetMutationRequest, AddPetMutationResponse } from '../../models/AddPet.ts'
- export function addPet(data: AddPetMutationRequest): Cypress.Chainable<AddPetMutationResponse> {
- return cy.request<AddPetMutationResponse>({
- method: 'post',
- url: 'http://localhost:3000/pet',
+ import type { AddPetData, AddPetResponse } from '../../models.ts'
+ export function addPet(data: AddPetData): Cypress.Chainable<AddPetResponse> {
+ return cy.request<AddPetResponse>({
+ method: 'POST',
+ url: `http://localhost:3000/pet`,@kubb/plugin-mcp
Handlers receive the MCP RequestHandlerExtra object as a second argument and forward it to the underlying client. Existing tools must be updated to thread it through.
import type { CallToolResult } from '@modelcontextprotocol/sdk/types'
export async function addPetHandler({ data }: { data: AddPetMutationRequest }): Promise<CallToolResult> {
const res = await fetch<AddPetMutationResponse, ResponseErrorConfig<AddPet405>, AddPetMutationRequest>({
method: 'POST',
url: '/pet',
baseURL: 'https://petstore.swagger.io/v2',
data,
})
...
}import type { CallToolResult, ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types'
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol'
export async function addPetHandler(
{ data }: { data: AddPetData },
request: RequestHandlerExtra<ServerRequest, ServerNotification>,
): Promise<CallToolResult> {
const res = await client<AddPetResponse, ResponseErrorConfig<AddPetStatus405>, AddPetData>(
{ method: 'POST', url: `/pet`, baseURL: `https://petstore.swagger.io/v2`, data },
request,
)
...
}Cross-cutting changes
These apply to every generator unless explicitly disabled:
- The banner (
/* Generated by Kubb */) is controlled byoutput.defaultBanneron the root config (default'simple'). Useoutput.banner(andoutput.footer) on individual plugins to override the text for a specific plugin's files. A string applies to every file. Pass a function to receive per-file context (isBarrel,isAggregation,filePath,baseName) and skip the banner on re-export files, for example to add'use server'to source files but not to barrel or group aggregation files. - All response status types are suffixed with
Status<code>.
Plugin author migration
If you maintain a custom plugin or generator, update the following:
PluginManager → PluginDriver
The internal orchestration class was renamed. Update every import and usage:
- import { PluginManager } from '@kubb/core'
+ import { PluginDriver } from '@kubb/core'
- const manager = new PluginManager(config)
+ const driver = new PluginDriver(config)
- ctx.pluginManager.getPlugin(name)
+ ctx.driver.getPlugin(name)The generator context property follows the same rename: pluginManager → driver.
pluginKey → pluginName
Each plugin now has a single pluginName identifier. The pluginKey array property is removed.
- export const myPlugin = definePlugin(() => ({
- pluginKey: ['my-plugin'],
+ export const myPlugin = definePlugin(() => ({
+ pluginName: 'my-plugin',Duplicate plugins (same pluginName registered twice) now throw at startup.
See also
- Adapters: how the OpenAPI input is parsed into the universal AST.
- Plugins: lifecycle, generators, and resolvers.
- Parsers: how AST nodes become source files.
- Middlewares: barrel files and other post-processors.
- Storage: switching between filesystem and in-memory storage.
@kubb/adapter-oas: every option that moved here from the plugins.- Plugin registry: the full list of v5 plugins.
- Recipes: copy-paste configurations for common scenarios.