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.
Table of Contents
- 1. The drift problem with hand-written API types
- 2. What an OpenAPI specification actually is and why it works as a single source of truth
- 3. openapi-typescript: pure types with zero runtime overhead
- 4. Building a typed fetch client on top of the generated types
- 5. Handling versioned APIs and multiple specs in one project
- 6. Wiring codegen into npm scripts and CI
- 7. Detecting breaking changes between old and new generated types
- 8. Limits and pitfalls: incomplete specs and nullable ambiguities
- 9. Hand-written vs. generated API types compared
- 10. Summary
- 11. FAQ
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.