Beta You're reading the docs for Kubb v5, which is currently in beta. View the stable v4 docs
Skip to content

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.

my-plugin.ts
typescript
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:

kubb.config.ts
typescript
import {  } from 'kubb'
import {  } from './my-plugin.ts'
Cannot find module './my-plugin.ts' or its corresponding type declarations.
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:

generator.ts
typescript
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.

resolver.ts
typescript
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:

resolver-plugin.ts
typescript
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:

consumer.ts
typescript
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 definition
typescript
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:

macros.ts
typescript
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:

naming.ts
typescript
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.

inject.ts
typescript
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.

depends.ts
typescript
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.

barrel.ts
typescript
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.