Plugins
Plugins 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. Here is 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. |
| Generation | kubb:generation:start | KubbGenerationStartContext | Code generation phase begins. |
| Plugin | kubb:plugin:setup | KubbPluginSetupContext | Once per plugin. Register generators, resolver, macros, 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
Plugins with enforce: 'post' run after all normal plugins for any given event, which suits cross-plugin concerns like barrel generation. See @kubb/plugin-barrel for an example.
The setup context
kubb:plugin:setup receives a KubbPluginSetupContext that wires the plugin into the build:
| Method / Property | Purpose |
|---|---|
addGenerator | Register one or more Generators that walk the AST. Pass them as separate arguments, or spread an existing list. |
setResolver | Set or override the resolver (file naming + paths). |
addMacro | Add a macro that rewrites AST nodes before generators. |
setMacros | Replace this plugin's macros with a new list. |
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
A generator is what walks the AST for a plugin. Register one with addGenerator inside kubb:plugin:setup:
import { , , } from '@kubb/core'
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 (merge with another generator's output), 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.
A handler returns Array<FileNode> built with the create* factories from @kubb/ast, which is the default. It may instead return JSX, in which case the generator sets renderer: jsxRenderer and @kubb/renderer-jsx walks the JSX into the same FileNodes. The creating-plugins guide names the printer, renderer, and serializer roles around that path.
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'() {
.()
},
},
}))Macros
A macro is a named, composable transform that rewrites nodes before they reach this plugin's generators. Whatever a macro returns replaces the original node for that plugin only, so other plugins keep seeing the untransformed AST.
type Plugin<TFactory> = {
// ...
macros?: Array<ast.Macro>
}Use macros to rename, filter, or rewrite nodes before generation, without forking the adapter or mutating shared state. Register them from kubb:plugin:setup with ctx.addMacro or ctx.setMacros:
import { , } from '@kubb/core'
const = .({
: 'rename',
() {
return {
...,
: ..(/^get/, 'fetch'),
}
},
() {
if (. === 'object') {
return { ..., : `${.}Dto` }
}
},
})
export const = (() => ({
: 'plugin-rename',
: {
'kubb:plugin:setup'() {
.()
},
},
}))A few rules apply:
Each macro callback (input, output, operation, schema, property, parameter, response) is optional, and unhandled node types pass through unchanged. Returning undefined keeps the original node, while returning a node of the same type replaces it. Macros run per plugin and in order, so a later macro sees the output of an earlier one. To share a macro across plugins, export it and add it from each plugin's setup. Macros run before resolver options are computed, so renamed operationIds and SchemaNode.names flow into resolveOptions, resolvePath, and resolveFile. Keep macros pure: build a new node and return it rather than mutating the input, since 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 docs 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 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-axios, …) all follow this layout. Match it so users 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-axios | Type-safe axios client functions. |
@kubb/plugin-fetch | Type-safe Fetch 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 kubb:plugin:setup when a plugin emits a fixed asset that does not depend on the input spec, such as 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' }] }],
})
},
},
}))Declare a dependency on another plugin
Use dependencies to guarantee a sibling plugin runs first. Order in the plugins array stops mattering, and a missing dependency fails the build with a clear error.
import { } from '@kubb/core'
export const = (() => ({
: 'plugin-axios-wrapper',
: ['plugin-ts'],
: {
'kubb:plugin:setup'() {
.('Starting plugin-axios-wrapper after plugin-ts')
},
},
}))Read sibling output in kubb:plugin:end
kubb:plugin:end runs after all generators in the plugin finish. 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 opt in or out. Prefix the name with plugin- (or @scope/plugin-) and keep it stable, since other plugins look it up by name. Use dependencies instead of declaration order, which is fragile. Have generators ask ctx.getResolver(name) rather than building paths inline. Use closure state inside the factory or the setup context, and avoid global state, since plugins may run in parallel. Throw early in kubb:plugin:setup when required options are missing, so the build aborts before any file is written.