This document describes the high-level architecture, package structure, and common patterns of the @nahkies/openapi-code-generator monorepo.
@nahkies/openapi-code-generator is a monorepo that produces a CLI tool for generating TypeScript client SDKs and server scaffolding from OpenAPI 3.0, 3.1, and TypeSpec specifications.
The system follows a typical compiler/generator pipeline:
- Loading: Specifications are loaded and resolved (including remote/local references).
- Validation: Loaded specifications are validated against their respective schemas using pre-compiled AJV validators.
- Normalization: The specification is transformed into a normalized Intermediate Representation (IR).
- Building: Template-specific builders transform the IR into code structures (types, schemas, clients/routers).
- Emission: The generated structures are formatted and written to the file system.
packages/openapi-code-generator: The core package containing the CLI, loader logic, IR normalization, and code generation templates.packages/typescript-common-runtime: Shares common types (e.g.,Res,StatusCode) and utilities (e.g., query parsing, body validation) used by all generated TypeScript code.packages/typescript-*-runtime: Runtime-specific support packages.typescript-axios-runtime: Provides utilities for Axios clients.typescript-fetch-runtime: ProvidesAbstractFetchClient.typescript-express-runtime: Provides utilities for routing and response handling in Express.typescript-koa-runtime: Provides utilities for routing and response handling in Koa.
integration-tests: Validates the generator by running it against real-world specifications (Github, Stripe, etc.) and ensuring the output compiles.e2e: End-to-end tests for the CLI and generated code.
The generator uses adaptors to abstract environment-specific logic, allowing it to run in both Node.js and the browser (playground).
IFsAdaptor: Abstraction for file system operations (NodeFsAdaptor,WebFsAdaptor).IFormatter: Abstraction for code formatting (TypescriptFormatterPrettier,TypescriptFormatterBiome).
Complex code generation is managed through builders that accumulate state and then produce a CompilationUnit.
TypeBuilder: Generates TypeScript type definitions.SchemaBuilder: Generates runtime validation schemas (Joi or Zod).ClientBuilder: (e.g.,TypescriptFetchClientBuilder) Generates client-side SDK code.RouterBuilder/ServerBuilder: (e.g.,ExpressRouterBuilder,ExpressServerBuilder) Generates server-side scaffolding and routing.
A CompilationUnit (defined in packages/openapi-code-generator/src/typescript/common/compilation-units.ts) represents a single file to be emitted. It encapsulates:
- The target filename.
- An
ImportBuilderto manage required imports without duplicates. - The raw string of code.
The Input class (in src/core/input.ts) serves as the IR. It takes a raw loader and provides normalized access to operations, schemas, and servers. This decoupling ensures that templates don't need to handle OpenAPI version differences or complex reference resolution.
graph TD
Spec[OpenAPI/TypeSpec Spec] --> Loader[OpenapiLoader]
Loader --> Validator[OpenapiValidator]
Validator --> Input[Input / IR]
Input --> Generator[Template Generator]
Generator --> Builders[Builders: Type, Schema, Client, Router]
Builders --> Units[CompilationUnits]
Units --> Emitter[TypescriptEmitter]
Emitter --> Formatter[Formatter: Biome/Prettier]
Formatter --> FS[Filesystem]
- No
npx: Always usepnpm runorpnpm exec. - Read-Only Generation: Never manually edit files in
src/generated/directories; they are overwritten during generation. - Jest Globals: Always explicitly import Jest globals (
describe,it,expect) from@jest/globals.
- Co-location: Unit tests (
*.spec.ts) are co-located with the source code they test. Use explicit jest imports (import {describe, it, expect} from "@jest/globals"). - Import Extensions: Use
.tsextensions in imports (e.g.,import {foo} from "./foo.ts") to support ESM. - Dependency Migration: Prefer
pnpmworkspace references (e.g.,"@nahkies/typescript-common-runtime": "workspace:*").
- Identify the Stage: Determine if the change belongs in the
Loader,Input(IR), or a specificGenerator/Builder. - Update IR (if needed): If the feature requires new data from the OpenAPI spec, update
src/core/openapi-types-normalized.tsand the normalization logic insrc/core/input.ts. - Update Builders: Implement the code generation logic in the relevant builders (e.g.,
ClientOperationBuilder). - Add Unit Tests: Add a
.spec.tsnext to the modified file. - Add Integration Spec: If the feature is a new OpenAPI concept, add a minimal reproduction spec to
integration-tests-definitions. - Run Pipeline:
pnpm buildpnpm integration:generatepnpm integration:validate
- Documentation: Update the external documentation in
packages/documentationif user-facing.
- Cyclic Dependencies: When generating schemas (Joi/Zod), the generator must handle cyclic references in the OpenAPI spec. Check
SchemaBuilder's handling ofReference. For Zod, we have multiple versions (zod-v3,zod-v4) as Zod v4 handles lazy schemas differently. - Filename Casing: The generator supports different filename conventions. Ensure your builders respect
config.filenameConvention. - ESM vs CJS: The project targets ESM. Relative imports must include file extensions.
- Generated Schema Validators:
src/core/schemas/openapi-*-specification-validator.jsare generated. If you update the meta-schemas, you must run the build script to refresh them.
- Direct Spec Access: Avoid accessing the raw
Loaderorloader.entryPointfrom within template builders. Use the normalizedInput(IR) instead. - Hardcoded Filenames: Avoid hardcoding filenames in builders; use the provided config and
ImportBuilderto manage paths. - Complex Logic in Templates:
*.generator.tsfiles should be orchestrators. Move complex logic into dedicatedBuilderclasses (e.g.ClientOperationBuilderorSchemaBuilder).