Architecture
Kubb separates the input specification, generated content, output syntax, and destination. Each layer has one job:
| Layer | Responsibility |
|---|---|
| Adapter | Read the specification and produce an InputNode. |
| AST | Describe schemas and operations in a shared tree. |
| Plugin | Run generators that emit FileNodes. |
| Parser | Convert emitted nodes into source code. |
| Storage | Persist the generated files. |
You declare the input spec, the output folder, an adapter, and the plugins to run. defineConfig pre-wires the OpenAPI adapter and the default parsers.
From one operation to generated files
Consider a specification with GET /pet/{petId}, the operation ID getPetById, and a response referring to a reusable Pet schema.
- The adapter reads the specification and resolves its references into schemas and operations.
- The shared AST describes the
Petfields, the path parameter, and the operation's response. It does not choose Axios, Fetch, or Zod. - The TypeScript plugin generates model and response types. An Axios plugin generates a
getPetByIdclient that imports those types. A Zod plugin can generate validation schemas from the same input. - A parser turns each plugin's emitted nodes into source text. Storage writes the resulting files to the configured destination.
The same specification can therefore produce several outputs in one run. The first-client tutorial follows this operation using TypeScript and Axios.
Adapters
An adapter resolves input-specific details, including references, nullability, discriminators, and formats. The default OpenAPI adapter supports OpenAPI 2.0, 3.0, and 3.1. A custom adapter converts another input format into the same AST, so downstream plugins do not read that format directly.
Pet: properties: owner: $ref: '#/components/schemas/User' tag: type: string nullable: true createdAt: type: string format: date-time
{ kind: 'Schema', name: 'Pet', properties: { owner: { kind: 'ref', name: 'User' }, tag: { kind: 'union', of: ['string', 'null'] }, createdAt: { type: 'datetime' }, },}
$ref resolves to a ref node by name. Plugins never see the JSON pointer.
AST
The AST is the shared model between the adapter and plugins. Normalizing the input once means each plugin can work with schemas and operations without implementing OpenAPI reference resolution itself. A new adapter can feed the same plugins, and a new plugin can consume the same model.
An InputNode contains reusable schemas and operations. Operations connect parameters, request bodies, and responses to schema nodes. Request bodies and responses have one ContentNode per content type.
Nodes carry a kind discriminant. Schemas also carry a type discriminant. The transform visitor rewrites nodes, while collect gathers matching nodes. Import the ast namespace from kubb/kit. See AST reference for node builders, visitors, and guards.
Plugins and generators
Each plugin owns an output: types, clients, hooks, validators, mocks, or custom files. Its generators handle individual schemas, individual operations, or the complete operation set.
Macros transform AST nodes before generators use them. They run per plugin, so one plugin's transformations do not modify another plugin's input. Resolvers keep names and paths consistent across generated files.
For the pet operation above, the type and client plugins need to agree on the response type's name and file path. The client reads the type plugin's resolver so its imports continue to work when names are customized.
See Extension model for how these pieces cooperate.
Parsers
Each parser claims file extensions. Kubb selects it for the emitted file, prints nodes during generation, and assembles the final source through parse. The default parsers handle .ts, .tsx, and .md.
The TypeScript parser prints the banner, the imports, and each source node of a FileNode into the file's text:
{ kind: 'File', path: 'src/gen/types/Pet.ts', banner: '/* eslint-disable */', imports: [{ name: ['Status'], path: './Status', isTypeOnly: true, }], sources: [{ nodes: [{ kind: 'TypeAlias', name: 'Pet', members: { id: 'number', status: 'Status' }, }] }],}
/* eslint-disable */import type { Status } from './Status' export type Pet = { id: number status: Status}
banner prints first. A footer would print last.
These responsibilities differ. The plugin chooses which files and declarations to emit. A printer chooses how an individual schema appears in that output. The parser assembles the file's source text.
A printer inside a generator handles schema-specific output. A parser handles the assembled file. See Customize printers and Parser reference.
Storage
Storage separates generation from its destination, so the same build runs on disk for the CLI or in memory for tests. fsStorage() writes to disk and memoryStorage() keeps results in a Map. A custom driver implements the Storage interface to target another backend.