Skip to content

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.

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

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' }, : [()], })

Run the CLI to see it in action:

bash
kubb generate

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

TIP

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:

bash
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 mocks

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

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

typescript
// @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).

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

resolvers.ts
typescript
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.
setup-context.ts
typescript
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.

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

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

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

json
{
  "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:

bash
npm login
npm publish --access public

tsconfig.json

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:

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

plugin-with-dep.ts
typescript
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`)] })],
              }),
            ]
          },
        }),
      )
    },
  },
}))