openapi-nexus 0.1.3

OpenAPI 3.x multi-language code generator
Documentation
# Target Output Spec

This document defines the target output for the sigil-stitch based code generation. It shows what generated code should look like, serving as the design reference for golden test updates.

**This is a breaking change from pre-sigil-stitch output.** Users who depend on `FromJSON`/`ToJSON`/`instanceOfX` helpers, `ApiXxxRequest` wrapper interfaces, or barrel `index.ts` must migrate.

## Locked Decisions

| # | Decision | Rationale |
|---|---|---|
| 1 | Field terminator is `;` (not `,`) | Standard idiomatic TypeScript |
| 2 | Drop `FromJSON` / `ToJSON` / `FromJSONTyped` / `ToJSONTyped` | Interfaces are structural; runtime conversion is noise when field names already match |
| 3 | Drop `instanceOfX` | Structural checks in TS are a code smell; use discriminators or validators |
| 4 | Drop `XxxPropertyValidationAttributesMap` | Dead data; validation belongs in a schema library |
| 5 | Drop `/* tslint:disable */` and `/* eslint-disable */` headers | tslint is deprecated; keep `@generated` header |
| 6 | Validator is opt-in via `--validator zod\|valibot\|none` (default `none`) | Schemas ship as siblings, never force-imported |
| 7 | No `models/index.ts` barrel | Barrels defeat tree-shaking and slow down IDEs at scale |
| 8 | Every field is `readonly` | Generated DTOs are immutable views of wire data |
| 9 | Runtime is embedded, ~80 lines target (was 450) | Replace dead abstractions with a single `request()` helper |
| 10 | Per-tag API classes + top-level `Client` wrapper | Ergonomic `new Client({ baseUrl }).pets.getPetById(...)` |
| 11 | `Client` name derived from `info.title` via PascalCase + `Client` suffix | `"Studio AMS — ASM"` becomes `StudioAmsAsmClient` |
| 12 | Errors throw `ApiError` with `ApiNetworkError` and `ApiResponseError` subclasses | Idiomatic JS `try/catch` + `instanceof` |
| 13 | Methods take an options object; path params, query, body are named keys | Consistent regardless of param count |

## Model File

### Before (~90 lines)

```ts
/* tslint:disable */
/* eslint-disable */
import { type Category, CategoryFromJSON, CategoryToJSON, instanceOfCategory } from './Category';

export interface Pet {
  category?: null | Category,
  id?: number | null,
  name: string,
}

export function instanceOfPet(value: unknown): value is Pet { ... }
export function PetFromJSON(json: any): Pet { ... }
export function PetToJSON(value?: Pet | null): any { ... }
```

### After (~20 lines)

```ts
/**
 * @generated by openapi-nexus. Do not edit.
 *
 * Petstore API — 1.0.0
 */
import type { Category } from './Category';

export interface Pet {
  readonly category?: Category | null;
  readonly id?: number | null;
  readonly name: string;
}
```

- `readonly` on every field
- `type` imports only (no value imports)
- `Array<T>` becomes `T[]` for simple types

## Enum

```ts
/** Pet status in the store */
export type PetStatus = 'available' | 'pending' | 'sold';
```

String-literal union type alias. No `enum` (not erasable-syntax compatible).

## Validator Schemas (Opt-in)

Emitted only when `--validator zod` or `--validator valibot` is set. Lives next to the model file as `Pet.schema.ts`.

### Zod

```ts
import { z } from 'zod';
export const PetSchema = z.object({
  category: CategorySchema.nullable().optional(),
  name: z.string(),
});
export const parsePet = (input: unknown) => PetSchema.parse(input);
```

### Valibot

```ts
import * as v from 'valibot';
export const PetSchema = v.object({
  category: v.optional(v.nullable(CategorySchema)),
  name: v.string(),
});
export const parsePet = (input: unknown) => v.parse(PetSchema, input);
```

## API File

Per-tag class with methods taking options objects:

```ts
export class PetApi {
  constructor(private readonly config: ClientConfig) {}

  async getPetById(options: { petId: number; signal?: AbortSignal }): Promise<Pet> {
    return request<Pet>(this.config, {
      method: 'GET',
      path: `/pet/${encodeURIComponent(options.petId)}`,
      signal: options.signal,
    });
  }
}
```

Removed: `ApiXxxRequest` wrappers, `PetApiInterface`, `xxxRaw` variants, typed response unions.

## Client Wrapper

Name derived from `info.title` (e.g., `PetstoreApiClient`):

```ts
export class PetstoreApiClient {
  readonly pets: PetApi;
  readonly store: StoreApi;

  constructor(options: PetstoreApiClientOptions = {}) {
    const config: ClientConfig = { /* ... */ };
    this.pets = new PetApi(config);
    this.store = new StoreApi(config);
  }
}
```

## Runtime (~80 lines)

```ts
export interface ClientConfig {
  baseUrl: string;
  fetch: typeof fetch;
  headers: Record<string, string>;
  beforeRequest?: (init: RequestInit & { url: string }) => ...;
}

export class ApiError extends Error { }
export class ApiNetworkError extends ApiError { }
export class ApiResponseError<T = unknown> extends ApiError { }

export async function request<T>(config: ClientConfig, options: RequestOptions): Promise<T> { }
```

No `BaseAPI`, no response wrappers, no `RequiredError`.

## Directory Layout

```
petstore/
├── apis/
│   ├── PetApi.ts
│   ├── StoreApi.ts
│   └── UserApi.ts
├── models/
│   ├── Pet.ts
│   ├── Pet.schema.ts       ← only with --validator
│   └── PetStatus.ts
├── runtime.ts
├── Client.ts
├── index.ts                 ← re-exports Client + types
├── package.json
└── tsconfig.json
```

## Go Side

Go keeps its current shape: per-tag client structs, struct + method receiver idiom, `encoding/json` marshaling. Same `info.title`-derived client naming. Concrete Go target goes in a follow-up spec doc.

## Migration Table

| Old | New |
|---|---|
| `import { Pet, PetFromJSON } from 'petstore/models/Pet'` | `import type { Pet } from 'petstore/models/Pet'` |
| `PetFromJSON(json)` | Direct assign or `parsePet(json)` with `--validator` |
| `instanceOfPet(x)` | `parsePet(x)` with validator, or write your own guard |
| `new PetApi(config).getPetByIdRaw(...)` | `new PetstoreApiClient({ baseUrl }).pets.getPetById(...)` |
| `catch (e: ResponseError) { e.response }` | `catch (e) { if (e instanceof ApiResponseError) e.body }` |