Create and extend a plugin
Build a plugin that writes listPets.ts containing the method and path of one API operation, then add a configurable prefix. You need Node.js 22 or higher, npm, and TypeScript knowledge.
Create a project
mkdir kubb-first-plugin
cd kubb-first-plugin
npm init -y
npm pkg set type=module
npm install -D kubb typescript @types/node vitest
mkdir srcAll authoring APIs come from kubb/kit, included with kubb.
Add the specification
Create petStore.yaml in the project root:
openapi: 3.0.3
info:
title: Pets
version: 1.0.0
paths:
/pets:
get:
operationId: listPets
responses:
'200':
description: A list of petsCreate the plugin
defineExtension with kind: 'plugin' takes the plugin's meta and a setup function. Register a generator and resolver in setup with the setResolver and addGenerator composables, then return the plugin's options:
import { addGenerator, ast, createResolver, defineExtension, defineGenerator, setResolver } from 'kubb/kit'
const resolver = createResolver({ pluginName: 'plugin-example' })
const operationsGenerator = defineGenerator({
name: 'operation-comments',
operation(node, ctx) {
const name = ctx.resolver.name(node.operationId)
return [
ast.factory.createFile({
baseName: `${name}.ts`,
path: `${ctx.root}/${name}.ts`,
sources: [
ast.factory.createSource({
nodes: [ast.factory.createText(`// ${node.method} ${node.path}\n`)],
}),
],
}),
]
},
})
export const pluginExample = defineExtension({
kind: 'plugin',
meta: { name: 'plugin-example', version: '1.0.0' },
setup(options) {
setResolver(resolver)
addGenerator(operationsGenerator)
return options
},
})The operation handler writes a comment containing the operation's HTTP method and path.
Generate
Register the plugin in your config:
import { defineConfig } from 'kubb/config'
import { pluginExample } from './src/plugin'
export default defineConfig({
input: './petStore.yaml',
output: { path: './src/gen' },
plugins: [pluginExample()],
})npx kubb generateRead the generated file:
node -e "console.log(require('node:fs').readFileSync('src/gen/listPets.ts', 'utf8'))"It contains a comment naming GET and /pets.
Test the output
Run an in-process build with memory storage and a small fixture. createKubb from kubb supplies the same adapter and parser defaults as defineConfig.
import { describe, expect, it } from 'vitest'
import { createKubb } from 'kubb'
import { memoryStorage } from 'kubb/kit'
import { pluginExample } from './plugin'
const input = {
openapi: '3.0.3',
info: { title: 'Pets', version: '1.0.0' },
paths: {
'/pets': {
get: {
operationId: 'listPets',
responses: { '200': { description: 'A list of pets' } },
},
},
},
}
describe('pluginExample', () => {
it('emits an operation comment', async () => {
const { files, storage } = await createKubb({
input,
output: { path: './gen' },
storage: memoryStorage(),
plugins: [pluginExample()],
}).build()
const file = files.find((file) => file.baseName === 'listPets.ts')
expect(file).toBeDefined()
expect(await storage.readItem(file!.path)).toContain('/pets')
})
})Run the test:
npx vitest run src/plugin.test.tsVitest reports one passing test. It checks the generated file in memory without writing to disk.
Extend the plugin
Add a prefix option so users can label the generated comment. Type the plugin with PluginType, return the resolved option from setup, and read it back from ctx.options in the generator. Apply these changes to src/plugin.ts:
import { addGenerator, ast, createResolver, defineExtension, defineGenerator, setResolver } from 'kubb/kit'
import type { PluginType } from 'kubb/kit'
const resolver = createResolver({ pluginName: 'plugin-example' })
type ExamplePlugin = PluginType<'plugin-example', { prefix?: string }, { prefix: string }>
const resolver = createResolver<ExamplePlugin>({ pluginName: 'plugin-example' })
const operationsGenerator = defineGenerator({
const operationsGenerator = defineGenerator<ExamplePlugin>({
name: 'operation-comments',
operation(node, ctx) {
const name = ctx.resolver.name(node.operationId)
return [
ast.factory.createFile({
baseName: `${name}.ts`,
path: `${ctx.root}/${name}.ts`,
sources: [
ast.factory.createSource({
nodes: [ast.factory.createText(`// ${node.method} ${node.path}\n`)],
nodes: [ast.factory.createText(`// ${ctx.options.prefix}${node.method} ${node.path}\n`)],
}),
],
}),
]
},
})
export const pluginExample = defineExtension({
export const pluginExample = defineExtension<ExamplePlugin>({
kind: 'plugin',
meta: { name: 'plugin-example', version: '1.0.0' },
setup(options) {
setResolver(resolver)
addGenerator(operationsGenerator)
return options
return { prefix: options.prefix ?? '' }
},
})Change the config to plugins: [pluginExample({ prefix: 'API: ' })], then run npx kubb generate again. The generated comment now reads:
// API: GET /petsAdd a second test inside the existing describe block that passes pluginExample({ prefix: 'API: ' }) and expects // API: GET /pets, then run npx vitest run src/plugin.test.ts again. Both tests pass: the original behavior remains available, and the new option changes the output.
You now have a tested plugin with one option. To ship it, follow Publish a plugin. For further extensions, use the Plugin reference and Generator reference. A dependency on another plugin lets your generator reuse its resolver rather than guessing generated names. See Dependencies and ordering.