Skip to content

How to Generate a TypeScript Client From an OpenAPI Spec

How to generate a typed TypeScript client from an OpenAPI spec — the operationId gotcha, a worked example, and why to regenerate instead of hand-edit.

Try it now: OpenAPI TypeScript Client Generator Turn an OpenAPI 3 document into a typed TypeScript fetch client with no dependencies — and see exactly which parts the spec left too vague to type.

Step 1: Understand Why You're Generating, Not Hand-Writing

An OpenAPI spec already describes every endpoint your API exposes: the path, the method, every parameter and its type, the request body schema, and the shape of every possible response. That's the entire set of information a TypeScript API client needs to exist. Hand-writing that client — typing out an interface for each response, a function for each endpoint, a fetch()call with the right method and headers — means retyping information that's already sitting in the spec, in a second place, by hand.

The problem isn't the initial typing effort. It's what happens six weeks later when the API adds a field, renames a parameter, or makes something that used to be optional required. The spec gets updated because it's the source of truth the backend is built from. The hand-written client does not get updated automatically — someone has to remember it exists, find every call site it affects, and edit it by hand. Nothing forces that to happen, which is exactly how a client silently drifts out of sync with the API it's supposedly describing. This walkthrough covers the actual steps to generate a typescript client from openapi instead, so that regenerating from the spec — not hand-editing — is how the client stays correct.

Step 2: Know What 'Generate a Client' Actually Produces

Before touching any tooling, it helps to be precise about the output, because "generate a client" is vague enough to mean several different things. Concretely, running openapi typescript codegen against a spec produces two categories of output:

  • TypeScript types for every schema. Each object schema in the spec — a request body, a response body, a reusable component referenced by $ref — becomes a TypeScript interface or type alias. This is the openapi to typescript types half of the job: no functions, no HTTP logic, just the shapes.
  • A typed function for every operation.Each path-and-method pair (an "operation" in OpenAPI terms) becomes an async function whose parameters match the operation's path parameters, query parameters, and request body — typed from their schemas — and whose return type matches the declared response schema. This is the typescript api client generator half: the actual HTTP call, wrapped so you never write the URL or the method by hand.

Put together, calling an endpoint through the generated client gives you full autocomplete on what to send and a compile-time guarantee — not a runtime guess — about what comes back. Pass a value the schema doesn't allow, or read a field the response type doesn't have, and TypeScript flags it before the code ever runs.

Step 3: Prepare the Spec — Set operationId on Every Operation

The one input a generator needs is a valid OpenAPI document, in YAML or JSON. It doesn't need to be hand-crafted — most teams either author it directly or generate it from route decorators in their framework of choice — but there's one field worth checking before you generate anything, because it determines whether the client you get back is pleasant to use or awkward to read.

Every operation object supports an operationId— a short, unique name for that specific path-and-method combination. A generator uses it, when present, as the name of the generated function. If it's missing, the generator has to derive a name from the path and method instead, and that derived name is rarely as clean. This is the single most common gotcha in this whole workflow:

with operationId — the generated function name is obvious
paths:
  /users/{id}:
    get:
      operationId: getUserById
      # → generates: getUserById(id: string): Promise<User>
without operationId — the generator has to guess
paths:
  /users/{id}:
    get:
      # no operationId set
      # → generates something like: get_0(id: string): Promise<User>
      #   or getUsersById, depending on the generator's naming heuristic —
      #   either way, a name nobody chose on purpose

A spec with no operationId values still generates a working client — the types are still correct, the HTTP calls are still right — but the method names are auto-derived, and auto-derived names tend to be either meaninglessly generic (get_0, post_1) or long and stitched together from the URL path in an order that doesn't read naturally. Setting a clear, human-chosen operationId on every operation in the source spec — getUserById, createOrder, listInvoices — is a five-minute fix that pays off directly in how the generated client reads at every call site, for as long as that client exists.

Step 4: A Worked Example — GET /users/{id}

Here's the smallest complete example: a single GEToperation with a path parameter and a typed JSON response, and the TypeScript output it should produce. This is illustrative of the pattern any reasonable generator follows — the exact syntax of the emitted code varies between tools, but the shape of the transformation doesn't.

input — a minimal OpenAPI path definition
paths:
  /users/{id}:
    get:
      operationId: getUserById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The requested user
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
components:
  schemas:
    User:
      type: object
      required: [id, email, active]
      properties:
        id:
          type: string
        email:
          type: string
        active:
          type: boolean
expected output — a type and a typed function
export interface User {
  id: string;
  email: string;
  active: boolean;
}

export async function getUserById(id: string): Promise<User> {
  const res = await fetch(`/users/${encodeURIComponent(id)}`);
  if (!res.ok) throw new Error(`getUserById failed: ${res.status}`);
  return res.json() as Promise<User>;
}

Notice what this buys you at the call site: getUserById(id) only accepts a string for id, and its return value is typed as User — so .email and .active autocomplete, and a typo like .emial is a compile error instead of an undefined discovered at runtime. That's the entire value proposition of openapi client generation made concrete: the same guarantee, repeated automatically for every operation in a spec that might have dozens or hundreds of them.

Step 5: Validate the Spec Before You Generate From It

Generating from a broken spec doesn't fail loudly — it produces a broken or incomplete client that looks fine until someone tries to use the part that was wrong. A response object with a missing schema, a $refpointing at a component that doesn't exist, a required field left out of a schema's required array by mistake — a generator either has to guess at what was meant or silently produce a gap (a field typed any, a function with no return type at all), and neither outcome is something you want to discover after the client is already checked into the codebase.

The fix is to make validation the step immediately before generation, every time, not an occasional sanity check. Confirm the spec is structurally valid — every required field present, every $ref resolved to something that actually exists — with GenKitLab's OpenAPI Validator before handing that document to a client generator. The full mechanics of what gets checked, and how errors are located by JSON Pointer, are covered in the OpenAPI Validator guide. If the spec started life as Swagger 2.0 or needs reformatting into a cleaner YAML/JSON layout first, Swagger & OpenAPI Formatter handles that conversion.

Step 6: Generate the Client

With a validated spec and clear operationIds in place, generation itself is the straightforward step: GenKitLab's OpenAPI TypeScript Client Generator takes an OpenAPI 3 document and emits a dependency-free TypeScript file built on plain fetch() — no bundled runtime package to install alongside it, no custom client class to configure, just the types and functions shown above, generated for every operation in the spec at once. It also tells you explicitly when part of the spec was too ambiguous to type precisely — an additionalProperties: true schema with no defined shape, a oneOfwith no discriminator — rather than fabricating a type that looks specific but isn't backed by anything the spec actually guarantees.

For the full design reasoning behind that behavior — why zero dependencies matters, and why an honest unknown beats a guessed type — the OpenAPI SDK Generator guide covers it in depth; this walkthrough is focused on the sequence of steps to get from spec to client, so consider that guide the companion piece for the product itself.

Step 7: Regenerate on Every Spec Change — Don't Hand-Edit Generated Code

The last step isn't really a step, it's an ongoing habit: whenever the spec changes, regenerate the client from it. Don't open the generated file and patch it by hand to reflect an API change, because the next time someone regenerates that file — which they will, the moment the spec changes again — any hand-edit gets silently overwritten with no warning. A generated file is an output, not a place to make manual changes; treat it the same way you'd treat a compiled binary or a lockfile.

The practical version of this is wiring regeneration into CI: run the generator against the current spec as a build or pre-commit step, and fail the check if the committed client file doesn't match what gets freshly generated from the spec that's currently checked in. That turns "someone forgot to regenerate the client" from a silent bug into a failed check — the same category of protection an OpenAPI diff gives you for catching breaking changes in the spec itself before they ship.

Frequently asked questions

How do I generate a TypeScript client from an OpenAPI spec?

Validate the spec first, make sure every operation has a clear operationId set, then run it through an OpenAPI-to-TypeScript generator. The output is a TypeScript file with an interface for each schema and a typed async function for each operation, built on the operation's method, parameters, and response schema.

What's the difference between openapi to typescript types and a full client generator?

Openapi to typescript types only produces the interfaces for each schema — the shapes of request and response bodies, with no HTTP logic. A full typescript api client generator produces both: the same types, plus a typed function per operation that actually makes the fetch() call, so you get autocomplete and type-checking on the call itself, not just on the data.

Why does operationId matter for the generated client?

A generator uses operationId as the name of the generated function. Without it, the generator has to derive a name from the path and method, which tends to produce either meaninglessly generic names or long, awkwardly ordered ones. A spec still generates a working client without operationId set — the names are just worse to read and call.

Do I need to validate my OpenAPI spec before generating a client?

Yes. A spec with a missing response schema, an unresolved $ref, or a field left out of a required array doesn't fail generation loudly — it produces a client with a gap or a guessed type in the exact place the spec was broken. Validating first, with a tool like GenKitLab's OpenAPI Validator, catches those problems before they show up as a confusing type in generated code.

Should I hand-edit a generated TypeScript client?

No. A generated file is regenerated whenever the spec changes, and any manual edit made directly to it gets silently overwritten the next time that happens. If the generated output needs a change — a custom header, different error handling — make that change in a wrapper around the generated client, not inside the generated file itself.

How often should I regenerate the client?

Every time the OpenAPI spec changes, ideally as an automated CI step rather than a manual task someone has to remember. Wiring generation into CI and failing the build if the committed client doesn't match what the current spec produces turns spec drift into a caught error instead of a silent bug.

Does generating a TypeScript client from OpenAPI require installing a runtime dependency?

It depends on the generator. Many OpenAPI client generators emit code that imports a bundled runtime support package alongside the generated functions. GenKitLab's OpenAPI SDK Generator produces plain, dependency-free TypeScript built on fetch(), so the output is just source code you can read and commit directly, with nothing extra to install or track.

Last updated