Skip to content
Host your specs. Generate from anywhere.

Custom Plugin

You may need to write your own plugin if the available plugins do not suit your needs or you’re working on a proprietary use case. This can be easily achieved using the Plugin API. But don’t take our word for it – all Hey API plugins are written this way!

We recommend following the design pattern of the native plugins. You can browse the code on GitHub as you follow this tutorial.

  • Directorymy-plugin/
    • config.ts
    • index.ts
    • plugin.ts
    • symbols.ts
    • types.ts

First, create a my-plugin folder for your plugin files. Inside, create a barrel file index.ts exporting the plugin.

index.ts
export { defaultConfig, defineConfig } from './config';
export type { MyPlugin } from './types';

index.ts references two files, so we need to create them. types.ts contains the TypeScript interface for your plugin options.

types.ts
import type { DefinePlugin, Plugin } from '@hey-api/openapi-ts';
export type UserConfig = Plugin.Name<'my-plugin'> &
Plugin.Hooks &
Plugin.UserExports & {
/**
* User-configurable option for your plugin.
*/
user?: number | string | {
age?: number;
name?: string;
};
};
export type Config = Plugin.Name<'my-plugin'> &
Plugin.Hooks &
Plugin.Exports & {
/** Resolved option for your plugin. */
user: {
age: number;
name: string;
};
};
export type MyPlugin = DefinePlugin<UserConfig, Config>;
  • Plugin.Name sets the plugin’s unique identifier.
  • Plugin.Hooks allows users to override plugin behavior through event hooks. For example, users can control how schemas are extracted or how symbols are routed to files.
  • Plugin.Exports add the includeInEntry option, which controls whether your plugin’s exports are re-exported from the output entry file.
  • UserConfig is what users write in their config file. Config is the fully resolved shape your handler receives.

config.ts defines the runtime configuration for your plugin, including how user input is resolved into Config and the handler function that generates output.

config.ts
import { definePluginConfig } from '@hey-api/openapi-ts';
import { handler } from './plugin';
import type { MyPlugin } from './types';
export const defaultConfig: MyPlugin['Config'] = {
config: {
user: {
age: 42,
name: 'Stan Smith',
},
},
handler,
name: 'my-plugin',
};
/**
* Type helper for `my-plugin` plugin, returns {@link Plugin.Config} object
*/
export const defineConfig = definePluginConfig(defaultConfig);

The config table is a declarative resolution spec. Each field declares its default value and optional coercion rules. To allow users to set either age or name through the user field, we can add coercion rules to transform primitive values into the expected shape.

config.ts
import { definePluginConfig } from '@hey-api/openapi-ts';
import { handler } from './plugin';
import type { MyPlugin } from './types';
export const defaultConfig: MyPlugin['Config'] = {
config: {
user: {
$coerce: {
number: (v) => ({ age: v }),
string: (v) => ({ name: v }),
},
age: 42,
name: 'Stan Smith',
},
},
handler,
name: 'my-plugin',
};
/**
* Type helper for `my-plugin` plugin, returns {@link Plugin.Config} object
*/
export const defineConfig = definePluginConfig(defaultConfig);

Plugins can declare dependencies on other plugins. A dependency will always be included in the output, even if the user hasn’t explicitly added it to their plugins config. This is useful when your plugin’s output builds on another plugin’s types or symbols.

config.ts
export const defaultConfig: MyPlugin['Config'] = {
config: { ... },
dependencies: ['@hey-api/typescript'],
handler,
name: 'my-plugin',
};

Some dependencies aren’t known at authoring time, such as which validator plugin the user has installed. Use resolveTag inside a coerce() call to look up the active plugin for a given tag at resolution time. In most cases, you’ll also want to declare a dependency on the field to ensure it’s included in the output.

config.ts
import type { PluginContext } from '@hey-api/openapi-ts';
import { coerce } from '@hey-api/openapi-ts';
export const defaultConfig: MyPlugin['Config'] = {
config: {
$dependencies: ['validator'],
validator: coerce((value, context) => {
if (value === true || value === undefined) {
return (context as PluginContext).resolveTag('validator');
}
return value;
}),
},
dependencies: ['@hey-api/typescript'],
handler,
name: 'my-plugin',
};

The handler function generates the actual output. We recommend implementing it in plugin.ts.

plugin.ts
import { $ } from '@hey-api/openapi-ts';
import type { MyPlugin } from './types';
export const handler: MyPlugin['Handler'] = ({ plugin }) => {
plugin.forEach('operation', 'schema', (event) => {
if (event.type === 'operation') {
// do something with the operation
} else if (event.type === 'schema') {
// do something with the schema
}
});
// use the TypeScript DSL to build nodes
const symbolName = plugin.symbol('user');
const node = $.const(symbolName)
.export()
.assign($.literal(plugin.config.user.name));
plugin.node(node);
};

Symbols are a type-safe way to reference identifiers your plugin works with, whether from external libraries or your own generated output. Rather than querying or constructing identifiers manually, you declare them once and access them through plugin.symbols anywhere in your handler.

Symbols are also visible to users customizing your plugin’s behavior. When a user provides hooks like ~resolvers, they receive the plugin instance and can access plugin.symbols to reference your plugin’s identifiers directly.

symbols.ts
import type { PluginInstance } from '@hey-api/openapi-ts';
export function myPluginSymbols(plugin: PluginInstance) {
return {
myLib: plugin.symbol('myLib', { external: 'my-lib' }),
};
}
export type MyPluginSymbols = ReturnType<typeof myPluginSymbols>;

Once you’re satisfied with your plugin, register it in the configuration file.

openapi-ts.config.ts
import { defineConfig } from 'path/to/my-plugin';
export default {
input: 'hey-api/backend', // sign up at app.heyapi.dev
output: 'src/client',
plugins: [
defineConfig(),
],
};

Putting all of this together will generate the following my-plugin.gen.ts file.

my-plugin.gen.ts
export const user = 'Stan Smith';

Congratulations! You’ve successfully created your own plugin! 🎉

Examples

You can view live examples on StackBlitz or on GitHub.