nog-cli is implemented as a unidirectional three-stage compiler:
OpenAPI Spec (Input)
↓
[Parser] - Load, validate, and dereference
↓
Internal Representation (IR) - Normalize to framework-agnostic structure
↓
[Generator] - Emit TypeScript code via ts-morph AST
↓
Generated NestJS Module (Output)
This architecture provides several benefits:
Note for AI Agents: This project follows a strict Compiler Design Pattern. Do not modify files ad-hoc. Follow the data flow: Parser → IR → Generator.
Key Principles:
Location: src/core/parser/
The parser ingests OpenAPI specifications (JSON or YAML) and resolves external references using @apidevtools/json-schema-ref-parser.
Key responsibilities:
$ref) while preserving schema identity.OpenApiDocument object.Key File: src/core/parser/openapi.parser.ts
Location: src/core/ir/
The IR is a simplified, normalized representation of an API specification that abstracts away OpenAPI-specific details.
IS_EMAIL, MIN_LENGTH, etc.).The OpenApiConverter resolves circular dependencies using a two-pass approach:
Pass 1 (Discovery): Iterate through all schemas and create empty model shells in the registry.
UserDto and PostDto exist in memory but have no properties yet.Pass 2 (Population): Iterate again and populate properties. Forward references are resolved from the registry.
UserDto.posts: PostDto[] successfully references the already-created PostDto.Key File: src/core/ir/openapi.converter.ts
Location: src/core/generator/
The generator consumes the IR and emits TypeScript code using the ts-morph library (Abstract Syntax Tree manipulation).
index.ts).Key File: src/core/generator/engine.ts
OpenAPI schemas often contain circular references (e.g., User → Post → User). To handle this without infinite loops during parsing, OpenApiConverter uses a Two-Pass Algorithm:
Pass 1 (Discovery): Iterate through all schemas and create "Empty Shells" (instances of IrModel with just the name) in the modelsRegistry.
UserDto and PostDto exist in memory, but have no properties.Pass 2 (Population): Iterate again and populate properties. When UserDto needs PostDto, it looks it up in the registry. Since PostDto already exists (created in Pass 1), the reference is resolved successfully.
This ensures all circular references are properly resolved without stack overflow errors.
The generator distinguishes between two types of union schemas:
A schema is "pure OneOf" when:
Rendering: Type alias
export type MediaUnion = ImageDto | VideoDto | AudioDto;
A schema is "hybrid" when:
Rendering: Class with properties and optional inheritance
export class BaseContent {
@IsNotEmpty()
public id: string;
@IsString()
public kind: 'text' | 'image' | 'video';
}
Each NestJS service operation generates two method variants:
$)Returns Observable<T> from RxJS:
public getUserById$(id: string): Observable<UserDto> {
const url = `/users/${id}`;
return this.httpService.get<UserDto>(url).pipe(
map(response => response.data),
);
}
The generator provides comprehensive support for file uploads and downloads with proper type mapping:
For multipart/form-data requests, binary fields are typed as Buffer | ReadStream:
// OpenAPI Spec
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
avatar:
type: string
format: binary
description:
type: string
// Generated Method
uploadAvatar$(body?: { avatar?: Buffer | ReadStream; description?: string }): Observable<void>
For application/octet-stream requests, the body is typed as Buffer | ReadStream:
// OpenAPI Spec
requestBody:
content:
application/octet-stream:
schema:
type: string
format: binary
// Generated Method
uploadDocument$(body?: Buffer | ReadStream): Observable<void>
For binary responses (images, PDFs, etc.), the return type is Buffer with appropriate responseType for Node.js:
// OpenAPI Spec
responses:
'200':
content:
application/pdf:
schema:
type: string
format: binary
// Generated Method
downloadFile$(fileId: string): Observable<Buffer>
Request/Response Metadata Handling:
The converter automatically extracts and applies HTTP metadata:
requestContentType: Extracted from requestBody.content media type
application/json: Default, no explicit headermultipart/form-data: Axios sets boundary automaticallyapplication/octet-stream, text/plain: Explicit Content-Type headeracceptHeader: Extracted from responses[200].content media types
Accept header in requestsAccept: image/png for image responsesresponseType: Axios configuration for non-JSON responses
'text' for text/* content types'arraybuffer' for binary content types such as image/*, application/pdf, or application/octet-streamContext-Aware Binary Type Mapping:
multipart/form-data → Buffer | ReadStreamBufferBuffer to a Blob if neededAdvantage: Reactive programming, powerful operators (map, filter, switchMap, etc.).
Wraps the Observable variant using firstValueFrom(), returns Promise<T>:
public getUserById(id: string): Promise<UserDto> {
return firstValueFrom(this.getUserById$(id));
}
Advantage: Familiar async/await syntax, easier for imperative code.
Strategy: Developers choose which variant fits their use case. Both share underlying HTTP logic.
Generated DTOs use class-validator decorators for runtime validation:
import { IsEmail, IsNotEmpty, Min, Max } from 'class-validator';
export class UserDto {
@IsEmail()
public email: string;
@IsNotEmpty()
@Min(18)
@Max(120)
public age: number;
}
Validation rules are automatically extracted from OpenAPI schema constraints:
required → @IsNotEmpty()format: email → @IsEmail()minimum, maximum → @Min(), @Max()pattern → @Matches()enum → @IsIn()We use ts-morph (AST manipulation) instead of string templates because:
Project and inspecting the AST.Instead of using dereferencing (inlining all schemas), we use the two-pass algorithm because:
UserDto, not an expanded object literal.This separation enables:
The project follows the three-stage compiler pattern:
src/
├── cli/ # UI Layer (Commander.js)
│ ├── commands/ # Command logic (e.g., generate.command.ts)
│ ├── options.ts # CLI Flags definition
│ └── program.ts # CLI entry point
├── config/ # Configuration Loading
│ └── config.loader.ts # Merges CLI flags with nog.config.json
├── core/ # THE COMPILER CORE
│ ├── parser/ # [Layer 1] Input Processing
│ │ ├── spec.loader.ts # I/O: FileSystem & HTTP loading
│ │ └── openapi.parser.ts # Parsing & Bundle via swagger-parser
│ ├── ir/ # [Layer 2] Intermediate Representation
│ │ ├── interfaces/
│ │ │ └── models.ts # TYPES ONLY: Defines IrModel, IrService
│ │ ├── analyzer/ # Helpers for type mapping & analysis
│ │ │ ├── type.mapper.ts # Maps OpenAPI types to IR types
│ │ │ ├── validator.map.ts # Maps constraints to class-validator
│ │ │ └── schema.merger.ts # Handles allOf, oneOf, anyOf
│ │ └── openapi.converter.ts # LOGIC: OpenAPI -> IR Transformer
│ └── generator/ # [Layer 3] Code Emission (ts-morph)
│ ├── engine.ts # Main orchestrator (FileSystem writes)
│ ├── helpers/ # Reusable code generation utilities
│ │ ├── type.helper.ts
│ │ ├── import.helper.ts
│ │ ├── decorator.helper.ts
│ │ └── file-header.helper.ts
│ └── writers/ # Logic for writing specific file types
│ ├── dto.writer.ts # Writes *.dto.ts
│ ├── service.writer.ts # Writes *.service.ts
│ ├── module.writer.ts # Writes *.module.ts
│ └── index.writer.ts # Writes index.ts
└── utils/ # Shared utilities (Logger, Naming, FS)
├── logger.ts
├── naming.ts
└── index.ts
test/
├── e2e/
│ └── generator.e2e-spec.ts # End-to-end test suite
├── fixtures/
│ ├── petstore.json # Standard test spec
│ ├── cyclos.json # Real-world complex spec
│ └── complex.json # Edge case spec
└── units/
├── parser/ # Parser unit tests
├── ir/ # IR conversion tests
└── generator/ # Writer unit tests
Scenario: Support a new validation rule (e.g., @Pattern(regex))
'PATTERN' to the IrValidator union type in src/core/ir/interfaces/models.ts.src/core/ir/analyzer/type.mapper.ts, extract the pattern field from OpenAPI schemas and add a PATTERN validator.src/core/generator/writers/dto.writer.ts, read the PATTERN validator and apply the @Matches() decorator using ts-morph.TypeMapper, DtoWriter) in isolation using mocks.OpenApiParser → OpenApiConverter).Test Coverage Requirement: Greater than 90% line coverage across all layers.
strict: true in tsconfig.json. No any types without justification.src/utils/logger instead of console.log().catch blocks.TODO notes without context, no debug console.log().