Plugins
Plugins are how you teach Kubb to generate something new. A plugin owns its file naming, its output folder, its lifecycle hooks, and the generators that walk the AST and emit FileNodes. Most of what you see in a generated src/gen/ folder comes from a plugin.
TIP
Need a TanStack Query client, a Zod schema set, or MSW handlers? Check the Plugins registry first. Build a custom plugin only when no existing one fits.
Quick start
A plugin is a factory created with definePlugin. It returns an object with a name and a hooks map.
import { } from '@kubb/core'
export const = (() => ({
: 'plugin-hello',
: {
'kubb:plugin:setup'() {
.({
: 'hello.ts',
: `${..}/hello.ts`,
: [{ : 'Source', : [{ : 'Text', : '// Hello from a plugin\n' }] }],
})
},
},
}))Register it in kubb.config.ts:
import { } from 'kubb'
import { } from './my-plugin.ts'
export default ({
: { : './petstore.yaml' },
: { : './src/gen' },
: [()],
})Anatomy
| Property | Type | Required | Purpose |
|---|---|---|---|
name | string | Yes | Unique plugin identifier (plugin-<thing> by convention). |
hooks | { [K in keyof KubbHooks]?: handler } | Yes | Map of lifecycle event handlers. Keys mirror the KubbHooks event names. |
dependencies? | Array<string> | No | Other plugin names that must be registered. Kubb throws at startup when a dependency is missing. |
enforce? | 'pre' | 'post' | No | Run this plugin before (pre) or after (post) all normal plugins. Dependencies always win. |
options? | TFactory['options'] | No | The user options forwarded by the factory. Typed via the PluginFactoryOptions generic. |
Lifecycle events
The hooks map can subscribe to any event in KubbHooks. The full list, in the order they fire during a build:
| Phase | Event | Context | When it fires |
|---|---|---|---|
| Lifecycle | kubb:lifecycle:start | KubbLifecycleStartContext | Beginning of the Kubb run, before configuration loading. |
| Lifecycle | kubb:lifecycle:end | none | End of the run, after every other event. |
| Config | kubb:config:start | none | Configuration loading starts. |
| Config | kubb:config:end | KubbConfigEndContext | Configuration loaded, before generation. |
| Generation | kubb:generation:start | KubbGenerationStartContext | Code generation phase begins. |
| Plugin | kubb:plugin:setup | KubbPluginSetupContext | Once per plugin. Register generators, resolver, transformer, renderer. |
| Plugin | kubb:build:start | KubbBuildStartContext | After all kubb:plugin:setup handlers, before the plugin loop. |
| Plugin | kubb:plugin:start | KubbPluginStartContext | Just before this plugin's generators run. |
| Plugin | kubb:generate:schema | (node: SchemaNode, ctx: GeneratorContext) | For each schema node during the AST walk. |
| Plugin | kubb:generate:operation | (node: OperationNode, ctx: GeneratorContext) | For each operation node during the AST walk. |
| Plugin | kubb:generate:operations | (nodes: OperationNode[], ctx: GeneratorContext) | Once per plugin after every operation has been walked. |
| Plugin | kubb:plugin:end | KubbPluginEndContext | This plugin finished. files snapshot is available. |
| Plugin | kubb:plugins:end | KubbPluginsEndContext | All plugins finished, before files are written. Inject final files here. |
| Generation | kubb:generation:end | KubbGenerationEndContext | Code generation phase complete. Carries the run's diagnostics, status, and file count. |
| Files | kubb:files:processing:start | KubbFilesProcessingStartContext | File processing starts. |
| Files | kubb:files:processing:update | KubbFilesProcessingUpdateContext | Batched per-flush progress updates. The context exposes a files array of KubbFileProcessingUpdate items. |
| Files | kubb:files:processing:end | KubbFilesProcessingEndContext | All files written. |
| Build | kubb:build:end | KubbBuildEndContext | Build finished, after files are on disk. |
| Format | kubb:format:start | none | Formatter (Biome, Prettier, …) starts. |
| Format | kubb:format:end | none | Formatter completes. |
| Lint | kubb:lint:start | none | Linter starts. |
| Lint | kubb:lint:end | none | Linter completes. |
| Hooks | kubb:hooks:start | none | User-defined hooks.done execution starts. |
| Hooks | kubb:hook:start | KubbHookStartContext | Each individual user hook command starts. |
| Hooks | kubb:hook:end | KubbHookEndContext | Each individual user hook completes. |
| Hooks | kubb:hooks:end | none | All user hooks finished. |
| Diagnostics | kubb:diagnostic | KubbDiagnosticContext | Emitted for each collected diagnostic during the run. |
| Diagnostics | kubb:info / kubb:success / kubb:warn / kubb:error | corresponding KubbInfoContext, KubbSuccessContext, … | Logging events. Subscribe to forward into your own observability stack. |
TIP
Middlewares subscribe to the same KubbHooks events, but most often listen to kubb:plugin:end (per-plugin file post-processing) or kubb:plugins:end (cross-plugin aggregation).
The setup context
kubb:plugin:setup receives a KubbPluginSetupContext that lets you wire the plugin into the build:
| Method / Property | Purpose |
|---|---|
addGenerator | Register a Generator that walks the AST. |
setResolver | Set or override the resolver (file naming + paths). |
setTransformer | Pre-process AST nodes with a Visitor. |
setRenderer | Set the renderer factory that handles JSX-style returns. |
setOptions | Provide resolved options to the build loop. |
injectFile | Inject a raw UserFileNode into the build, bypassing generators. |
updateConfig | Merge a partial config update into the running build. |
config | The resolved Config at setup time. |
options | The user-supplied plugin options. |
Generators
Generators are how a plugin actually walks the AST. Register them with addGenerator inside kubb:plugin:setup:
import { , } from '@kubb/core'
import { , , } from '@kubb/ast'
const = ({
: 'operation-files',
async (, ) {
return [
({
: `${.}.ts`,
: `${.}/${.}.ts`,
: [
({
: [(`// ${.} ${.}\n`)],
}),
],
}),
]
},
})
export const = (() => ({
: 'plugin-operations',
: {
'kubb:plugin:setup'() {
.()
},
},
}))A generator may implement any combination of three handlers:
| Handler | Called for… | Returns |
|---|---|---|
schema | Each SchemaNode in the AST. | Array<FileNode>, void, or JSX. |
operation | Each OperationNode in the AST. | Array<FileNode>, void, or JSX. |
operations | Once with all OperationNodes after the operation walk. | Array<FileNode>, void, or JSX. |
Each handler receives a GeneratorContext with helpers like addFile, upsertFile, getResolver(name), requirePlugin(name), info, warn, error, plus the resolved adapter, the document meta (an InputMeta with title, version, baseURL, circularNames, and enumNames), and per-node options.
Resolvers
Every plugin owns a resolver that decides where files live and how they are named. Other plugins call ctx.getResolver('plugin-name') to refer to those names without hard-coding paths.
import { } from '@kubb/core'
import from 'node:path'
import type { , } from '@kubb/core'
type = <'plugin-hello', object, object, >
export const = <>(() => ({
: 'default',
: 'plugin-hello',
({ }, { , }) {
return .(, ., 'hello', )
},
}))Use it inside the plugin:
import { , } from '@kubb/core'
import type { , } from '@kubb/core'
type = <'plugin-hello', object, object, >
const = <>(() => ({
: 'default',
: 'plugin-hello',
}))
export const = <>(() => ({
: 'plugin-hello',
: {
'kubb:plugin:setup'() {
.()
},
},
}))Other plugins consume it through the generator context:
import { , } from '@kubb/core'
const = ({
: 'consumer',
(, ) {
const = .('plugin-hello')
if ('name' in && typeof . === 'string') {
const = .(., 'function')
.(`hello name: ${}`)
}
},
})
export const = (() => ({
: 'plugin-consumer',
: ['plugin-hello'],
: {
'kubb:plugin:setup'() {
.()
},
},
}))Transformer
A transformer is an ast.Visitor that pre-processes nodes before they reach this plugin's generators. The walker calls the visitor for each SchemaNode, OperationNode, and other node it encounters; whatever the visitor returns replaces the original node for that plugin only. Other plugins keep seeing the untransformed AST.
type Plugin<TFactory> = {
// ...
transformer?: ast.Visitor
}Use a transformer when you need to rename, filter, or rewrite nodes before generation, without forking the adapter or mutating shared state. Set it from kubb:plugin:setup with ctx.setTransformer:
import { } from '@kubb/core'
import type { } from '@kubb/ast'
const : = {
() {
return {
...,
: ..(/^get/, 'fetch'),
}
},
() {
if (. === 'object') {
return { ..., : `${.}Dto` }
}
},
}
export const = (() => ({
: 'plugin-rename',
: {
'kubb:plugin:setup'() {
.()
},
},
}))A few rules apply:
- Each visitor key (
input,output,operation,schema,property,parameter,response) is optional. Unhandled node types pass through unchanged. - Returning
voidkeeps the original node. Returning a node of the same type replaces it. - Transformers run per plugin. To share a transform across plugins, export the visitor and import it from each plugin's setup.
- Transformers run before resolver options are computed, so renamed
operationIds andSchemaNode.names flow intoresolveOptions,resolvePath, andresolveFile.
TIP
Keep transformers pure. Mutating the input node leaks the change into other plugins because the AST is shared by reference.
Naming convention
Kubb expects every plugin to follow the same naming pattern so other plugins, the CLI, and the documentation can find them by inference. The convention applies to four places at once:
| Surface | Pattern | Example |
|---|---|---|
| npm package | @<scope>/plugin-<name> or kubb-plugin-<name> | @kubb/plugin-ts, kubb-plugin-stripe |
| Plugin runtime name | plugin-<name> (kebab-case, lowercase) | 'plugin-ts' |
| Factory export | plugin<Name> (camelCase) | pluginTs, pluginReactQuery |
PluginFactoryOptions alias | Plugin<Name> (PascalCase) | PluginTs, PluginReactQuery |
Export the runtime name as a constant so consumers can reference it without typos when declaring dependencies:
import { } from '@kubb/core'
import type { , } from '@kubb/core'
export type = <'plugin-example', { ?: string }, { : string }, >
export const = 'plugin-example' satisfies ['name']
export const = <>(() => ({
: ,
: { : . ?? 'Hello' },
: {},
}))TIP
Built-in plugins (@kubb/plugin-ts, @kubb/plugin-zod, @kubb/plugin-client, …) all follow this layout. Match it so users can swap your plugin in without rewiring imports.
Built-in plugins
The Kubb monorepo ships official plugins for the most common use cases. Browse them in the Plugins registry or in source:
| Plugin | Generates |
|---|---|
@kubb/plugin-ts | TypeScript types from your spec. |
@kubb/plugin-zod | Zod schemas. |
@kubb/plugin-client | Type-safe HTTP client functions. |
@kubb/plugin-react-query | React Query (TanStack) hooks. |
@kubb/plugin-vue-query | Vue Query (TanStack) hooks. |
@kubb/plugin-msw | MSW request handlers. |
@kubb/plugin-faker | Faker-based mock data. |
@kubb/plugin-cypress | Cypress request helpers. |
@kubb/plugin-mcp | MCP tool definitions. |
@kubb/plugin-redoc | Redoc API documentation. |
Examples
Inject a single file from setup
Use ctx.injectFile from the kubb:plugin:setup hook when a plugin emits a fixed asset that doesn't depend on the input spec. Common cases include a README, a barrel file, or a pre-baked runtime helper.
import { } from '@kubb/core'
export const = (() => ({
: 'plugin-banner',
: {
'kubb:plugin:setup'() {
.({
: 'README.md',
: `${..}/README.md`,
: [{ : 'Source', : [{ : 'Text', : '# Generated by Kubb\n' }] }],
})
},
},
}))Walk operations with a generator
Generators receive each AST node together with a typed context. Return an array of FileNode to emit files, or call ctx.upsertFile to merge with output from another generator.
import { , } from '@kubb/core'
import { , , } from '@kubb/ast'
const = ({
: 'list-operations',
(, ) {
return [
({
: `${.}.ts`,
: `${.}/operations/${.}.ts`,
: [({ : [(`// ${.} ${.}\n`)] })],
}),
]
},
})
export const = (() => ({
: 'plugin-operations',
: {
'kubb:plugin:setup'() {
.()
},
},
}))Declare a dependency on another plugin
Use dependencies to guarantee a sibling plugin runs first. Order in the plugins array becomes irrelevant; missing dependencies fail the build with a clear error.
import { } from '@kubb/core'
export const = (() => ({
: 'plugin-client-wrapper',
: ['plugin-ts'],
: {
'kubb:plugin:setup'() {
.('Starting plugin-client-wrapper after plugin-ts')
},
},
}))Read sibling output in kubb:plugin:end
The kubb:plugin:end hook runs after all generators in the plugin finished. Use it to emit aggregate files (barrels, manifests, type re-exports) from the files the plugin already produced.
import { } from '@kubb/core'
export const = (() => ({
: 'plugin-barrel',
: {
'kubb:plugin:end'({ }) {
const = .(() => `export * from './${.}'`).('\n')
.(`Generated ${.} files\n${}`)
},
},
}))Best practices
- Split unrelated outputs into separate plugins so users can opt in or out.
- Prefix the name with
plugin-(or@scope/plugin-) and keep it stable; other plugins look it up by name. - Use
dependenciesinstead of relying on declaration order. Order is fragile; declared dependencies are explicit. - Generators should ask
ctx.getResolver(name)rather than building paths inline. - Use closure state inside the factory or rely on the setup context. Plugins may run in parallel, so avoid global state.
- Throw early in
kubb:plugin:setupwhen required options are missing. The build aborts before any file is written.