Automatically Generating API Types From OpenAPI/Swagger
AI generated
<T>
type
TypeScript · OpenAPI · Codegen · CI/CD
Automatically Generating API Types From OpenAPI/Swagger
Keeping frontend and backend guaranteed in sync

Hand-written TypeScript interfaces for REST endpoints quietly go stale as the backend evolves, offering false type safety instead of real protection against runtime errors. This article shows how openapi-typescript and related tools generate TypeScript types and typed fetch clients directly from an OpenAPI specification, and how to wire that process reliably into build and CI pipelines.

13 min. read openapi-typescript · openapi-fetch Magento REST API · GitHub Actions

1. The drift problem with hand-written API types

The classic pattern in growing Magento and headless projects: a developer writes an interface Product { id: number; name: string; price: number; } at the start of the project, because it matches the current API response exactly. Months later, the backend team adds a new required field, makes price optional for draft products, or renames a field. The hand-written type stays unchanged, TypeScript keeps compiling without errors, because the compiler has no knowledge of the actual API response, it only checks against the claim in the interface, not against the truth on the server.

The result is false type safety: the code looks protected but breaks at runtime with undefined is not an object the moment a field is missing that the type declares as guaranteed. Code reviews rarely catch this drift, because nobody compares the actual API response against the interface on every pull request. Over time, trust in the type system erodes, teams start reaching for any as an escape hatch, and the exact benefit TypeScript was introduced for quietly disappears.

2. What an OpenAPI specification actually is and why it works as a single source of truth

OpenAPI (formerly Swagger) is a machine-readable YAML or JSON format for describing REST APIs: paths, HTTP methods, parameters, request and response schemas, and authentication are all captured declaratively. Many backend frameworks generate this spec automatically from code annotations instead of maintaining it by hand, Symfony API Platform, NestJS with @nestjs/swagger, Laravel with L5-Swagger, and Magento's own REST API also serves a complete Swagger definition under /rest/V1/schema or /swagger.

When the spec is generated directly from backend code, it cannot structurally go stale: every change to a controller or DTO is automatically reflected in the next spec generation. That turns the OpenAPI file into a binding contract between backend and frontend teams, and this exact contract can be used as the input for TypeScript type generation instead of typing out interfaces a second time by hand from documentation.


openapi: 3.1.0
info:
  title: Mironsoft Product API
  version: "1.2.0"
paths:
  /products/{sku}:
    get:
      operationId: getProductBySku
      parameters:
        - name: sku
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: A single product
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Product"
        "404":
          description: Product not found
components:
  schemas:
    Product:
      type: object
      required:
        - id
        - sku
        - name
        - price
      properties:
        id:
          type: integer
        sku:
          type: string
        name:
          type: string
        price:
          type: number
          format: float
        specialPrice:
          type: number
          format: float
          nullable: true
        tags:
          type: array
          items:
            type: string

3. openapi-typescript: pure types with zero runtime overhead

openapi-typescript reads an OpenAPI file, whether a local YAML/JSON file or fetched via URL from a running instance, and produces a single .d.ts file with a paths interface for every endpoint plus a components["schemas"] interface for every data model. Crucially, the tool generates types only, no runtime library: no extra bytes land in the production bundle, because type information is fully erased at compile time. That sets it apart from heavier codegen approaches that generate full client classes with their own runtime logic.

In practice, a single command is enough: npx openapi-typescript ./openapi/product-api.yaml -o ./src/api/schema.d.ts. A watch mode automatically regenerates the types on every change to the local spec file, which pays off especially during joint backend-frontend development. If the spec is instead pulled live via URL from a running Magento instance, the generated types mirror the exact state of the actually reachable API, not just a snapshot that was checked in at some point.


// Auto-generated by openapi-typescript. DO NOT EDIT BY HAND.
export interface paths {
  "/products/{sku}": {
    get: operations["getProductBySku"];
  };
}

export interface components {
  schemas: {
    Product: {
      id: number;
      sku: string;
      name: string;
      price: number;
      /** @description Nullable in the spec, so number | null in TS */
      specialPrice: number | null;
      tags?: string[];
    };
  };
}

export interface operations {
  getProductBySku: {
    parameters: {
      path: { sku: string };
    };
    responses: {
      200: {
        content: { "application/json": components["schemas"]["Product"] };
      };
      404: {
        content: never;
      };
    };
  };
}

4. Building a typed fetch client on top of the generated types

Plain types alone don't prevent runtime errors, only a client that actually enforces them closes the loop. openapi-fetch, from the same tooling family as openapi-typescript, is a tiny wrapper around native fetch that fully checks paths, HTTP method, parameters, and response body against the generated paths interface. The IDE offers autocomplete for valid paths and parameters along the way, and a typo in an endpoint name becomes a compile error instead of a silent 404 at runtime.

Anyone who doesn't want an extra dependency in the bundle can achieve the same effect with a slim, hand-rolled generic wrapper around fetch that uses the generated operations types as type parameters. In both cases, one thing stays true: the type checking happens exclusively at compile time. Neither openapi-typescript nor openapi-fetch verifies whether the response actually matches the schema at runtime, that requires an additional runtime validation layer, for example with zod, if needed.


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

// The client is fully typed against the generated `paths` interface
const client = createClient<paths>({ baseUrl: "https://shop.example.com" });

async function loadProduct(sku: string) {
  const { data, error } = await client.GET("/products/{sku}", {
    params: { path: { sku } },
  });

  if (error) {
    // error is typed based on the non-2xx responses in the spec
    console.error("Product lookup failed", error);
    return null;
  }

  // data.price is a number, data.specialPrice is number | null -
  // both checked at compile time against the OpenAPI schema
  return data;
}

5. Handling versioned APIs and multiple specs in one project

Real Magento projects rarely talk to just one API. A typical setup combines the Magento REST API for catalog, cart, and customer data with one or more internal microservices, for example a custom pricing engine or an external search service. Each of these sources gets its own generated type file with a distinct name, for instance schema-magento.d.ts and schema-pricing.d.ts, so that identically named schema types like Product from different services don't collide and stay unambiguously referenceable through separate namespace imports.

Versioning follows a similar principle: when the backend introduces a breaking-change version v2, a second spec is generated in parallel with the existing v1 spec, so both type versions coexist until the migration in the frontend is complete. Clearly named npm scripts per spec, such as generate:magento and generate:pricing, keep this process traceable and prevent a single global codegen command from becoming unmanageable once a project spans three or four backends.

6. Wiring codegen into npm scripts and CI

Without automation, type generation degenerates into a manual step someone eventually forgets. The simplest safeguard is prebuild and predev hooks in package.json that automatically run the codegen command before every build or dev-server start. That way, locally developed types can never go stale for longer than a single session, since they're pulled fresh from the current spec every time.

The CI pipeline adds a second, stricter step: the spec is fetched fresh from the backend, the types are regenerated, and then git diff --exit-code checks the generated files against the committed state in the repository. If the result differs, the build fails, a clear signal that the backend schema changed without the frontend types being regenerated and committed. This schema drift check turns a silent, creeping problem into a loud, immediately visible CI failure.


{
  "scripts": {
    "generate:api-types": "openapi-typescript ./openapi/product-api.yaml -o ./src/api/schema.d.ts",
    "generate:pricing-types": "openapi-typescript ./openapi/pricing-api.yaml -o ./src/api/pricing-schema.d.ts",
    "prebuild": "npm run generate:api-types && npm run generate:pricing-types",
    "predev": "npm run generate:api-types && npm run generate:pricing-types",
    "build": "vite build"
  }
}

- name: Regenerate API types and check for drift
  run: |
    npm run generate:api-types
    npm run generate:pricing-types
    # Fail the build if the freshly generated types differ from
    # what is committed in the repository - this is schema drift.
    git diff --exit-code -- src/api/schema.d.ts src/api/pricing-schema.d.ts

- name: Fail with a clear message on drift
  if: failure()
  run: |
    echo "Generated API types are out of sync with the OpenAPI spec."
    echo "Run 'npm run generate:api-types' locally and commit the result."
    exit 1

7. Detecting breaking changes between old and new generated types

Because the generated .d.ts file is deterministic and human-readable, a plain git diff on that file becomes a surprisingly effective tool: a removed property, a field changed from string to string | null, or a new required field show up exactly where they belong in the pull request diff, visible, commentable, reviewable. With hand-written types, the same change often happens silently, because a developer adjusts the type "along the way" to match the new reality, without the actual trigger in the backend ever showing up in the diff at all.

For even more precise results, dedicated diff tools like oasdiff or openapi-diff compare two spec versions semantically and automatically classify changes as breaking or non-breaking, a new optional field counts as safe, a removed or newly required field counts as a breaking change. Combining a generated type diff with a semantic spec diff gives double coverage in the CI pipeline: one shows the concrete TypeScript impact, the other the business severity of the change.

8. Limits and pitfalls: incomplete specs and nullable ambiguities

Codegen is only as trustworthy as the underlying spec itself. If a Swagger file is maintained by hand instead of generated from code, it drifts over time just like hand-written types, only one level higher up the stack. If the spec is missing a correct required array, openapi-typescript marks nearly every field as optional, forcing TypeScript code to use optional chaining everywhere, even where a field is in practice always present, a safety gain bought at the cost of unnecessarily clunky code.

nullable semantics are also inconsistent across frameworks: some backends return a missing field, others null, others a placeholder like "" or 0, and all of that can end up translated differently into the spec. Making matters worse, OpenAPI 3.0 and 3.1 handle nullable syntactically differently: 3.0 uses nullable: true, 3.1 follows the JSON Schema standard with type: ["string", "null"]. A clean codegen workflow therefore doesn't replace the conversation with the backend team about a shared understanding of required fields.

9. Hand-written vs. generated API types compared

The table below summarizes how hand-written and OpenAPI-generated TypeScript types differ in practice, beyond the initial time savings that often look deceptively small when first setting up an interface.

Criterion Hand-written types Generated types
Accuracy over time Drifts unnoticed with every backend change Always in sync with the current spec
Maintenance effort Manual per endpoint, scales poorly One command covers all endpoints at once
Onboarding speed New developers read docs and guess at fields IDE autocomplete straight from the spec
Breaking-change detection Only surfaces at runtime Visible in the diff, blockable in CI
Nullable handling Depends on the developer's discipline Derived directly from the schema, consistent

The comparison isn't a niche tool for large projects, it's a structural advantage that pays off starting from just a few dozen endpoints. Generated types consistently shift errors from runtime into compile time or even into the CI pipeline, long before code ever reaches production.

Mironsoft

TypeScript codegen and type-safe API integrations for Magento and headless projects

Ready to keep your API types reliably in sync?

We set up openapi-typescript and typed fetch clients for your Magento REST API and internal microservices and wire schema drift checks firmly into your CI pipeline, so outdated API types never get a chance to happen in the first place.

OpenAPI codegen setup

Wiring type generation from Magento and microservice specs

CI pipeline integration

Schema drift checks and breaking-change diffs in GitHub Actions

Typed API clients

openapi-fetch wrappers with optional runtime validation via zod

10. Summary

Automatically generating API types from OpenAPI/Swagger solves a problem that hand-written interfaces cannot structurally solve: guaranteed agreement between what the type promises and what the backend actually delivers. openapi-typescript produces pure, runtime-free types directly from the spec, openapi-fetch or a slim hand-rolled wrapper actually enforces those types on every API call, and schema drift checks in the CI pipeline prevent outdated types from ever getting merged in the first place.

The decisive lever isn't any single codegen command, it's automation: prebuild hooks for local development and a hard git diff --exit-code step in CI ensure frontend types never fall more than one build cycle behind the real API contract. Keeping the approach's limits in view, chiefly incomplete or hand-maintained specs and inconsistent nullable semantics, turns type generation into a reliable, almost invisible part of the daily development routine.

Generating API Types From OpenAPI - The Essentials at a Glance

Single source of truth

The OpenAPI spec, ideally generated from backend code, replaces hand-written interfaces as the binding contract.

Zero-runtime types

openapi-typescript produces pure .d.ts files with no extra bundle weight in production.

CI drift check

git diff --exit-code on freshly generated types turns schema drift into an immediately visible build failure.

Breaking-change diff

Type diffs and semantic spec diffs like oasdiff surface changes before they break the frontend.

11. FAQ: Generating API Types From OpenAPI

1What is openapi-typescript and how does it differ from other codegen tools?
Generates only TypeScript type definitions with no runtime library. Other tools additionally generate full client classes with their own runtime logic and more bundle weight.
2Do I need openapi-fetch or is native fetch enough with the generated types?
Native fetch doesn't automatically check paths and response types. openapi-fetch establishes that connection in a type-safe way, but it isn't mandatory, a hand-rolled generic wrapper works too.
3How do I keep generated types in sync with a running Magento instance?
Load the spec directly via URL from the Magento REST schema route. A prebuild or predev hook pulls the types fresh on every build or dev-server start.
4What happens if the OpenAPI spec itself is incomplete or incorrect?
Missing required arrays lead to unnecessarily many optional fields. A hand-maintained spec that isn't generated from code drifts over time just like hand-written types.
5How do I catch breaking changes in the API before they break the frontend?
A git diff on the generated type file shows structural changes directly in the pull request. Semantic diff tools like oasdiff additionally classify changes as breaking or non-breaking.
6Can I combine multiple OpenAPI specs in one project?
Yes. Each spec is generated into its own clearly named type file and used through separate namespace imports to avoid type collisions.
7Is codegen worth it for small projects with few endpoints?
Yes, setup effort is low and protection against drift kicks in from the very first backend update. The benefit grows with endpoint count but isn't tied to a minimum project size.
8How do OpenAPI 3.0 and 3.1 differ when it comes to nullable fields?
3.0 uses nullable: true. 3.1 follows the JSON Schema standard and expresses nullable fields via type: ["string", "null"]. Codegen tools need to interpret both notations correctly.
9Does the generated type replace runtime validation with zod or io-ts?
No. Generated types work only at compile time. For real runtime safety, additional validation with zod still makes sense, especially with untrusted third-party APIs.
10How do I prevent generated types from accidentally being edited by hand?
A comment header "Auto-generated, DO NOT EDIT BY HAND" signals the rule. The CI drift check makes it binding: manual edits get overwritten on the next codegen run.