---
title: "Custom Plugin"
description: "Learn how to create your own Hey API plugin."
url: "https://heyapi.dev/docs/openapi/typescript/plugins/custom"
---

Caution

Plugin API is in development. The interface might change before it becomes stable. We encourage you to leave feedback on [GitHub](https://github.com/hey-api/openapi-ts/issues).

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!

## File Structure

[Section titled “File Structure”](#file-structure)

We recommend following the design pattern of the native plugins. You can browse the code on [GitHub](https://github.com/hey-api/openapi-ts/tree/main/packages/openapi-ts/src/plugins) as you follow this tutorial.

* my-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

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

## Types

[Section titled “Types”](#types)

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

types.ts

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

## Configuration

[Section titled “Configuration”](#configuration)

`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

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

  config.ts

  ```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);
  ```

* input

  openapi-ts.config.ts

  ```js
  import { defineConfig } from 'path/to/my-plugin';


  export default {
    input: 'hey-api/backend', // sign up at app.heyapi.dev
    output: 'src/client',
    plugins: [
      defineConfig({
        user: 'Joe Doe', // or number to set age
      }),
    ],
  };
  ```

* resolved

  ```ts
  {
    user: {
      age: 42,
      name: 'Joe Doe'
    }
  }
  ```

### Dependencies

[Section titled “Dependencies”](#dependencies)

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

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

#### Resolving tags

[Section titled “Resolving tags”](#resolving-tags)

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

```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',
};
```

## Handler

[Section titled “Handler”](#handler)

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

plugin.ts

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

[Section titled “Symbols”](#symbols)

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

  symbols.ts

  ```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>;
  ```

* types

  types.ts

  ```ts
  import type { MyPluginSymbols } from './symbols';


  /** ... */


  export type MyPlugin = DefinePlugin<UserConfig, Config, never, MyPluginSymbols>;
  ```

* config

  config.ts

  ```ts
  import { myPluginSymbols } from './symbols';


  export const defaultConfig: MyPlugin['Config'] = {
    config: { ... },
    handler,
    name: 'my-plugin',
    symbols: myPluginSymbols,
  };
  ```

* handler

  plugin.ts

  ```ts
  export const handler: MyPlugin['Handler'] = ({ plugin }) => {
    const { myLib } = plugin.symbols;
    // use myLib as a typed reference when building nodes
  };
  ```

## Usage

[Section titled “Usage”](#usage)

Once you’re satisfied with your plugin, register it in the [configuration](https://heyapi.dev/docs/openapi/typescript/configuration) file.

openapi-ts.config.ts

```js
import { defineConfig } from 'path/to/my-plugin';


export default {
  input: 'hey-api/backend', // sign up at app.heyapi.dev
  output: 'src/client',
  plugins: [
    defineConfig(),
  ],
};
```

## Output

[Section titled “Output”](#output)

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

my-plugin.gen.ts

```ts
export const user = 'Stan Smith';
```

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

## Examples

You can view live examples on [StackBlitz](https://stackblitz.com/orgs/github/hey-api/collections/openapi-ts-examples) or on [GitHub](https://github.com/hey-api/openapi-ts/tree/main/examples).
