Skip to content

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.

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. 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:

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

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'() {
      .()
    },
  },
}))

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.

ts
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:

transformer.ts
typescript
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 void keeps 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 and SchemaNode.names flow into resolveOptions, resolvePath, and resolveFile.

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:

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-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.

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

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

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

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 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 dependencies instead 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:setup when required options are missing. The build aborts before any file is written.