Creating your first plugin
A plugin teaches Kubb to generate something new. It owns its output folder, file naming, generators that walk the AST, and lifecycle hooks that react to the build.
This guide walks you through building a complete kubb-plugin-example package from scratch and publishing it to npm.
TIP
Before writing a plugin, check the Plugins registry to see if an existing plugin already covers your use case.
Prerequisites
This guide assumes you have:
- Node.js 22 or higher and pnpm (or npm/yarn)
- Working TypeScript knowledge
- A Kubb project with a valid configuration
- Familiarity with the plugin concepts page
Quick start
A plugin is a factory function created with definePlugin from @kubb/core. It returns an object with a name string and a hooks map.
The kubb:plugin:setup hook is where you wire generators and resolvers into the build.
import { , } from '@kubb/core'
import { , , } from '@kubb/ast'
export const = (() => ({
: 'plugin-hello',
: {
'kubb:plugin:setup'() {
.(
({
: 'hello-generator',
(, ) {
return [
({
: `${.}.ts`,
: `${.}/${.}.ts`,
: [
({
: [(`// ${.} ${.}\n`)],
}),
],
}),
]
},
}),
)
},
},
}))Wire it into your kubb.config.ts:
import { } from 'kubb'
import { } from './my-plugin.ts'
export default ({
: { : './petStore.yaml' },
: { : './src/gen' },
: [()],
})Run the CLI to see it in action:
kubb generateProject layout
Every official Kubb plugin follows the same package layout. The reference implementation at @kubb/plugin-client uses dedicated folders per concern: generators/, resolvers/, components/, and templates/.
Mirror this layout in your own plugin so contributors can navigate it without surprises:
kubb-plugin-example/
├── src/
│ ├── index.ts # Public exports (factory, generators, resolvers, types)
│ ├── plugin.ts # definePlugin factory + plugin<Name>Name constant
│ ├── types.ts # PluginExample = PluginFactoryOptions<...>
│ ├── generators/ # One file per generator (e.g. operationsGenerator.ts)
│ │ └── exampleGenerator.ts
│ ├── resolvers/ # One file per resolver
│ │ └── resolverExample.ts
│ ├── components/ # Optional: JSX components when using @kubb/renderer-jsx
│ └── templates/ # Optional: source templates exposed at runtime
├── mocks/ # OpenAPI fixtures consumed by tests
│ └── petStore.yaml
├── package.json
├── tsconfig.json
└── README.mdTIP
Use @kubb/plugin-client as the canonical example. Its src/index.ts re-exports each generator, resolver, and the plugin factory by name, and its src/plugin.ts declares a pluginClientName satisfies PluginClient['name'] constant that other plugins consume.
Scaffold the directories:
mkdir kubb-plugin-example && cd kubb-plugin-example
npm init -y
npm install --save-peer @kubb/core @kubb/ast
npm install --save-dev typescript @types/node vitest
mkdir -p src/generators src/resolvers mocksNaming conventions
Choose the package name and internal identifiers to match Kubb conventions so the registry and other tooling can discover them.
| Surface | Pattern | Example |
|---|---|---|
| npm package (official) | @kubb/plugin-<name> | @kubb/plugin-ts |
| npm package (community) | kubb-plugin-<name> | kubb-plugin-example |
| Runtime plugin name | plugin-<name> (kebab-case, lowercase) | 'plugin-example' |
| Factory export | plugin<Name> (camelCase) | pluginExample |
| Name constant | plugin<Name>Name | pluginExampleName |
Use satisfies to export a typed name constant so other plugins can reference it without typos:
import type { } from '@kubb/core'
export const = 'plugin-example' satisfies ['name']IMPORTANT
Use kubb-plugin-<name> for community packages. The @kubb/plugin-* namespace is reserved for official Kubb Labs packages.
Plugin anatomy
The following four files form the skeleton of a plugin package. Each file is shown in the order it is typically read: types first, then the implementation files, then the public entry point.
// @filename: src/types.ts
import type { } from '@kubb/core'
/** User-facing options for kubb-plugin-example. */
export interface PluginExampleOptions {
/** Output filename for the generated operations index. Defaults to `'operations.ts'`. */
?: string
/** Whether to emit the operations index file. Defaults to `true`. */
?: boolean
}
/**
* `PluginFactoryOptions` binds the plugin name, the user-facing option type,
* and the resolved option type together so generators, resolvers, and the
* build loop share a consistent interface.
*/
export type = <'plugin-example', PluginExampleOptions, <PluginExampleOptions>>
// @filename: src/generators/exampleGenerator.ts
import { } from '@kubb/core'
import { , , } from '@kubb/ast'
import type { } from '../types'
/**
* Creates a generator that emits one file per operation and, optionally,
* an index file listing every operation ID.
*
* `defineGenerator` returns a `TElement | Array<FileNode> | void` union so
* handlers may return a single element, an array, or nothing.
*/
export function (: `${string}.${string}`, : boolean) {
const : string[] = []
return <>({
: 'example-generator',
(, ) {
// OperationNode.operationId is a required string, so no nullability guard is needed.
.(.)
return [
({
: `${.}.ts`,
: `${.}/${.}.ts`,
: [
({
: [(`// ${.} ${.}\n`), (`export const operationId = '${.}'\n`)],
}),
],
}),
]
},
async (, ) {
if (!) return
return [
({
: ,
: `${.}/${}`,
: [
({
: [(`export const operations = ${.()}\n`)],
}),
],
}),
]
},
})
}
// @filename: src/resolvers/resolverExample.ts
import { } from '@kubb/core'
import type { } from '../types'
/**
* `defineResolver` automatically injects defaults for `default`, `resolveOptions`,
* `resolvePath`, `resolveFile`, `resolveBanner`, and `resolveFooter`.
* Only `name` and `pluginName` are required in the builder object.
* Override any of the injected methods when you need custom naming or path logic.
*/
export const = <>(() => ({
: 'default',
: 'plugin-example',
}))
// @filename: src/plugin.ts
import { } from '@kubb/core'
import type { } from '@kubb/core'
import type { } from './types'
import { } from './generators/exampleGenerator'
import { } from './resolvers/resolverExample'
export const = 'plugin-example' satisfies ['name']
export const = <>(() => {
const = (?. ?? 'operations.ts') as `${string}.${string}`
const = ?. ?? true
return {
: ,
: {
'kubb:plugin:setup'() {
.()
.((, ))
.({ , })
},
},
}
})
// @filename: src/index.ts
export { } from './generators/exampleGenerator'
export { } from './resolvers/resolverExample'
export { , } from './plugin'
export type { , } from './types'Generators
A generator walks the AST produced by the adapter and emits FileNodes. Register generators in the kubb:plugin:setup hook using ctx.addGenerator. Each generator may implement any combination of three handlers:
| Handler | Called for | Return type |
|---|---|---|
schema | Each SchemaNode in the AST | Array<FileNode>, element, or void |
operation | Each OperationNode in the AST | Array<FileNode>, element, or void |
operations | Once with all OperationNodes after the operation walk | Array<FileNode>, element, or void |
src/generators/exampleGenerator.ts
The ctx argument inside a handler is a GeneratorContext with helpers such as addFile, upsertFile, getResolver, requirePlugin, warn, error, info, and the resolved config, root, adapter, and document meta (an InputMeta with title, version, baseURL, circularNames, and enumNames).
import { } from '@kubb/core'
import { , , } from '@kubb/ast'
const = ({
: 'operation-files',
(, ) {
// node.operationId is a required string on OperationNode.
return [
({
: `${.}.ts`,
: `${.}/${.}.ts`,
: [
({
: [(`// Generated from ${.} ${.}\n`), (`export const operationId = '${.}'\n`)],
}),
],
}),
]
},
(, ) {
// Runs for each SchemaNode. Return void to skip emitting a file.
.(`Visiting schema: ${.}`)
return []
},
})Resolvers
A resolver controls how file names and output paths are computed for a plugin's files. Other plugins call ctx.getResolver('plugin-example') to reference those names without hard-coding paths.
src/resolvers/resolverExample.ts
defineResolver injects sensible defaults for every resolver method. Provide only name and pluginName in the builder and override specific methods when you need custom behavior. Returning null from resolveOptions excludes the node from generation, so only return null when you explicitly want to filter a node out.
import { } from '@kubb/core'
import from 'node:path'
import type { , } from '@kubb/core'
type = <'plugin-example', object, object, >
export const = <>(() => ({
: 'default',
: 'plugin-example',
// Override resolvePath to place files in a custom sub-folder.
({ }, { , }) {
return .(, ., 'example', )
},
}))The setup context
kubb:plugin:setup receives a KubbPluginSetupContext that wires the plugin into the build. The full interface from @kubb/core:
| Method / Property | Purpose |
|---|---|
addGenerator | Register a Generator for the AST walk. |
setResolver | Set or override the resolver (file naming and paths). |
setTransformer | Pre-process AST nodes with a Visitor. |
setRenderer | Set the renderer factory for JSX-style generator 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. |
import { , } from '@kubb/core'
import { , , } from '@kubb/ast'
export const = (() => ({
: 'plugin-example',
: {
'kubb:plugin:setup'() {
// ctx.config gives access to the full Kubb configuration.
const = ...
// Register a generator that emits one file per operation.
.(
({
: 'example-generator',
(, ) {
return [
({
: `${.}.ts`,
: `${.}/${.}.ts`,
: [({ : [(`// output: ${}\n`)] })],
}),
]
},
}),
)
// Inject a static file directly, bypassing generators entirely.
.({
: 'README.md',
: `${}/README.md`,
: [{ : 'Source', : [{ : 'Text', : '# Generated\n' }] }],
})
},
},
}))Options
Use PluginFactoryOptions to bind the plugin name, user-facing options, and resolved options together. This type flows through definePlugin, defineGenerator, and the resolver, keeping all three in sync.
import { } from '@kubb/core'
import type { } from '@kubb/core'
interface PluginExampleOptions {
/** Output filename for the index. Defaults to `'operations.ts'`. */
?: string
/** Whether to emit the index file. Defaults to `true`. */
?: boolean
}
type = <'plugin-example', PluginExampleOptions, <PluginExampleOptions>>
export const = <>(() => {
// Apply defaults in the factory closure so each build invocation
// gets its own resolved copy.
const = ?. ?? 'operations.ts'
const = ?. ?? true
return {
: 'plugin-example',
: {
'kubb:plugin:setup'() {
// Store the resolved options so generators can read them from ctx.plugin.options.
.({ , })
},
},
}
})Testing
Use createKubb from @kubb/core to create an in-process build and verify that your generator emits the expected files. Pair it with a small OpenAPI fixture to keep tests fast and deterministic.
import { , , } from 'vitest'
import { , , } from '@kubb/core'
import { , , } from '@kubb/ast'
const = (() => ({
: 'plugin-example',
: {
'kubb:plugin:setup'() {
.(
({
: 'example-generator',
(, ) {
return [
({
: `${.}.ts`,
: `${.}/${.}.ts`,
: [({ : [(`// ${.}\n`)] })],
}),
]
},
}),
)
},
},
}))
('pluginExample', () => {
('emits one file per operation', async () => {
const = ({
: { : './test/fixtures/petStore.yaml' },
: { : './dist/test' },
: [()],
})
const { } = await .()
(.).(0)
})
})Observing lifecycle events
Subscribe to kubb.hooks before calling build() to trace plugin activity or collect metrics. Each hook receives a single typed context object.
import { , } from '@kubb/core'
const = ({
: { : './petStore.yaml' },
: { : './gen' },
: [(() => ({ : 'plugin-example', : {} }))()],
})
// kubb:plugin:end receives a single KubbPluginEndContext, not two separate arguments.
..('kubb:plugin:end', ({ , , }) => {
.(`[${.}] finished in ${}ms (ok=${})`)
})
// kubb:files:processing:update fires once per flush batch with an array of per-file updates.
..('kubb:files:processing:update', ({ }) => {
for (const { , , , } of ) {
.(`[${}/${}] (${.(0)}%) ${.}`)
}
})
await .()Publishing your plugin
Configure package.json
Set up package.json for dual-format publishing. Peer-depend on @kubb/core and @kubb/ast at v5 to keep the runtime out of your bundle, and mark them as devDependencies for local builds.
{
"name": "kubb-plugin-example",
"version": "1.0.0",
"description": "A Kubb plugin that generates example files from OpenAPI specs.",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"test": "vitest",
"prepublishOnly": "npm run build && npm test"
},
"peerDependencies": {
"@kubb/core": "^5.0.0",
"@kubb/ast": "^5.0.0"
},
"devDependencies": {
"@kubb/ast": "^5.0.0",
"@kubb/core": "^5.0.0",
"@types/node": "^22.0.0",
"typescript": "^5.0.0",
"vitest": "^3.0.0"
},
"keywords": ["kubb", "plugin", "openapi", "codegen"],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/yourname/kubb-plugin-example"
}
}Publish to npm
Before publishing, verify the checklist:
- Exported TypeScript types compile without errors
- JSDoc comments on public APIs
- README with installation and usage examples
- All tests pass
- Version follows Semantic Versioning
See the npm publishing docs for the full publish workflow:
npm login
npm publish --access publictsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"skipLibCheck": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "test"]
}Examples
The kubb-labs/plugins repository contains official community plugins that follow all conventions described in this guide. Browse the source to see how generators, resolvers, and options are wired together in production packages.
Schema generator
Generate code for each schema definition in the spec:
import { } from '@kubb/core'
import { , , } from '@kubb/ast'
export const = ({
: 'schema-generator',
(, ) {
return [
({
: `${.}.ts`,
: `${.}/${.}.ts`,
: [
({
: [(`// Schema: ${.}\nexport type ${.} = unknown\n`)],
}),
],
}),
]
},
})Extending an existing plugin
Declare dependencies when your plugin must run after another plugin so Kubb verifies the dependency at startup:
import { , } from '@kubb/core'
import { , , } from '@kubb/ast'
export const = (() => ({
: 'plugin-custom',
// plugin-ts must be registered before plugin-custom starts.
: ['plugin-ts'],
: {
'kubb:plugin:setup'() {
.(
({
: 'custom-generator',
(, ) {
// Use the plugin-ts resolver for consistent naming.
const = .('plugin-ts')
const = .(., 'function')
return [
({
: `${}.custom.ts`,
: `${.}/${}.custom.ts`,
: [({ : [(`// extends ${}\n`)] })],
}),
]
},
}),
)
},
},
}))