# Generate a TypeScript client from OpenAPI: openapi-typescript vs openapi-generator vs openapi-fetch

A TypeScript codebase that talks to an HTTP API should not hand-write request and response interfaces. It will not keep them in sync for a month. The question is which generator to use, and the answer depends on how much runtime you want generated alongside the types. The ecosystem splits into three camps — types-only, lightweight typed fetchers, and full SDK generators — and teams routinely pick the heaviest option when the lightest would do. Here is the comparison on a real 3.2 spec with enums, polymorphic responses, file uploads, and SSE.

## Camp 1: types-only with openapi-typescript

`openapi-typescript` turns the document into TypeScript types and nothing else. No runtime, no request functions, no dependencies.

``` bash
npx openapi-typescript openapi.yaml -o src/api/schema.d.ts
```

You keep using fetch (or a thin wrapper), and apply the generated types with helper types:

``` typescript
import type { paths, components } from "./api/schema";

type Project = components["schemas"]["Project"];
type CreateProjectBody =
  paths["/v1/projects"]["post"]["requestBody"]["content"]["application/json"];

async function createProject(body: CreateProjectBody): Promise<Project> {
  const res = await fetch("https://api.example.com/v1/projects", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`create failed: ${res.status}`);
  return (await res.json()).data as Project;
}
```

Strengths: zero runtime weight, full control over fetch behavior, middleware, retries, and auth; the generated file is types only, so it cannot contain a broken dependency; it tracks 3.1/3.2 JSON Schema features fastest because it only has to express them as types. Weaknesses: you write the request plumbing, including path parameter interpolation, query serialization, multipart, and per-status error handling; enums come out as string unions (fine) but there is no runtime enum object; nothing validates at runtime.

## Camp 2: typed fetchers — openapi-fetch and similar

`openapi-fetch` (from the same maintainer as openapi-typescript) adds a minimal typed fetch wrapper over the types-only output:

``` typescript
import createClient from "openapi-fetch";
import type { paths } from "./api/schema";

const client = createClient<paths>({ baseUrl: "https://api.example.com" });

const { data, error, response } = await client.POST("/v1/projects", {
  body: { name: "Apollo", plan_code: "TEAM" },
});
if (error) throw new Error(error.detail);
console.log(data.data.id);
```

The method and path are jointly typed, the request body is checked against that operation, and `data`/`error` narrow by documented status codes. Runtime is a few kilobytes and the API stays close to fetch. This is the sweet spot for most application codebases: contract-checked calls without an SDK's opinions. Weaknesses: advanced needs (global retries, complex auth refresh, multipart progress, streaming) still sit on you or on middleware; the ergonomics assume a conventional REST shape.

## Camp 3: full SDK generators — openapi-generator and friends

OpenAPI Generator (Java toolchain, npm wrapper) and its TypeScript targets (`typescript-axios`, `typescript-fetch`, and the newer `typescript` client) emit a complete SDK: service classes, models, interceptors, retries. The strengths matter for some organizations:

-   A distributable client for external customers with a stable surface and documentation.
-   Batteries-included auth flows, retries, and (in some generators) WebSocket support.
-   Consistent clients across languages generated from the same spec.

The costs are why application teams often regret it: thousands of lines of generated code in the repo (or a heavy internal package), generator-version churn producing enormous diffs, templates that lag OpenAPI 3.1/3.2 features, and an abstraction layer that fights you when the API does something the template did not anticipate. Enums become runtime objects you must learn the naming of; polymorphic schemas generate verbose inheritance hierarchies.

## The comparison on the things that actually hurt

| Concern                        | openapi-typescript | openapi-fetch               | Full generator               |
|--------------------------------|--------------------|-----------------------------|------------------------------|
| Runtime size                   | Zero               | \~few KB                    | Large                        |
| Request/response type checking | Manual wiring      | Built in                    | Built in                     |
| Per-status error narrowing     | Manual             | Native                      | Varies by template           |
| OpenAPI 3.1/3.2 tracking       | Fast               | Fast                        | Often lags                   |
| Multipart / file upload        | You implement      | Supported                   | Supported                    |
| SSE / streaming                | You implement      | You extend                  | Limited/template-dependent   |
| Retries / interceptors         | Yours              | Middleware                  | Built in                     |
| Generated-code churn           | One .d.ts          | One .d.ts                   | Large diffs                  |
| Best for                       | Custom stacks      | App frontends/Node services | External multi-language SDKs |

## Wiring generation into CI

Whichever you choose, the generated client is a build artifact, and the discipline is the same:

1.  **Generate in CI, not by hand.** A `typecheck` job regenerates from the pinned spec and fails if the output differs from what is committed (or generate at build time and keep types out of the repo entirely).
2.  **Pin the spec by version.** Consume a released spec artifact (registry, git tag, or hosted URL with a version header), not someone's working branch.
3.  **Fail the build on breaking changes for consumers.** Pair generation with an OpenAPI diff; a removed enum value breaks the union and should break the consuming build before release.
4.  **Validate at the boundary.** Types do not survive runtime: the server can still return an undocumented field or null. For trusted internal APIs that is acceptable; for external input, validate responses with a schema validator (or a generated runtime validator) at the edge.
5.  **Keep one wrapper module.** Even with openapi-fetch, instantiate the client once so base URL, auth headers, and error normalization have a single home.

## Enums, nulls, and the 3.2 details that matter

-   Enums described as `enum: [x, y]` become string unions in types-only and fetch camps; full generators emit runtime objects. Unions are usually what modern TypeScript wants; do not let a generator's enum naming drive your domain model.
-   `type: [string, "null"]` maps to `string | null`; older generators built for 3.0-era `nullable` still emit `?`-optional instead of nullable. Check the output on one nullable field before adopting.
-   Polymorphism (`discriminator`, `oneOf`) is where templates show their age; generate one discriminated union response and inspect it — a hierarchy of base classes is a warning sign.
-   SSE endpoints are not REST calls; the client for them is an EventSource/fetch-stream wrapper keyed off the documented event schema, not a generated service method.

## Practical default

For a React app or a Node service consuming one or two internal APIs: openapi-typescript + openapi-fetch, generated in CI from a versioned spec, with a single client module and explicit handling of the documented problem+json errors. Reach for the full generator when you are shipping an SDK to external customers in multiple languages and need the same conventions everywhere. Types-only alone is the right call when your transport is non-standard (custom streaming, signed requests) and any abstraction would be in the way.

In a spec-driven workspace the document these generators consume is the same file used for design, mocks, and tests, so the generated TypeScript tracks the contract the team actually exercised — and the MCP server exposes the same operations to coding agents. The [demo](https://www.powerduck.com/app/?ref=powerduck.com) shows the document-to-tools loop.

**What to read next:** [detect breaking API changes in CI](https://www.powerduck.com/blog/detect-breaking-api-changes-openapi-diff-ci/) pairs with client generation, and [organize a large OpenAPI spec with $ref](https://www.powerduck.com/blog/organize-large-openapi-spec-multiple-files/) covers the source structure that makes generated types clean.

