How to Eliminate TypeScript API Drift with a Single OpenAPI Spec File — Codex CLI + AGENTS.md
You've probably experienced a moment where a backend developer added a response field, but the frontend was still receiving it as any. Honestly, I put up with this pattern for quite a while myself — the complacency of thinking "if it blows up at runtime, I'll fix it then" eventually came back as a hotfix on the night of a deployment. If you use the OpenAPI spec as a single source of truth and auto-generate type code entirely, this problem disappears structurally. When the spec changes, the types change — and when the types change, the compiler tells you first.
Add Codex CLI and AGENTS.md on top of that, and one more thing gets solved: you can completely block AI coding agents from directly modifying auto-generated files "for convenience." If you spell out the regeneration rules in AGENTS.md, Codex CLI will follow them across sessions and across team members. This article walks through the specific steps to tie these two things together.
The Structural Reason API Drift Happens
The Problem Is "Writing Types Twice"
There's a UserResponse type on the backend, and a UserResponse interface on the frontend. They start out identical, but over time the two codebases evolve independently. When the backend adds a field, it doesn't get reflected in the frontend types. This is called API drift, and as long as types exist in two places, it's an unavoidable structural problem.
Let's look at the problem structure first.
Flipping this flow gives us an improved structure.
The solution is to unify ownership of types into one place. The OpenAPI spec plays that role. When you define the spec first (spec-first), the backend and frontend both reference the same contract, and with no manual sync step in between, there's no room for drift to appear. In code-first environments like FastAPI — where the spec is derived from code — it's simply the other way around; from the frontend's perspective, the spec is still used as the input.
The Codegen Ecosystem in 2026
TypeScript codegen tools haven't converged on a single winner. Each has a different focus, and the right choice depends on your project's characteristics. Among them, @hey-api/openapi-ts has seen the widest adoption over the past few years. It's effectively the successor to openapi-typescript-codegen, and its plugin architecture lets you attach various clients like TanStack Query, Axios, or Fetch.
| Tool | Characteristics | Best For |
|---|---|---|
| @hey-api/openapi-ts | Plugin architecture, TanStack Query support | General-purpose TypeScript projects |
| Orval | Auto-generates hooks, built-in mock generation | React / test-heavy projects |
| openapi-typescript | Generates types only, lightweight and flexible | When you prefer writing fetch code yourself |
| Kubb | Multi-plugin, can generate Zod schemas | Complex monorepo environments |
| OpenAPI Generator | Multi-language support | Multi-stack enterprise environments |
This article uses @hey-api/openapi-ts as the example, but the AGENTS.md pattern described below applies equally regardless of which tool you choose.
Basic Setup — From Codegen to AGENTS.md
Step 1: Install Codegen and Register the Script
npm install -D @hey-api/openapi-tsPinning the version matters. Without it, generated output can differ across environments, making diffs noisy.
{
"scripts": {
"codegen": "openapi-ts"
},
"devDependencies": {
"@hey-api/openapi-ts": "0.52.3"
}
}@hey-api/openapi-ts has recommended a config-file approach over CLI flags from a relatively early stage (this is a conceptual example — check the documentation for the version you have installed). Keeping an openapi-ts.config.ts at the project root is also easier to maintain.
// openapi-ts.config.ts
import { defineConfig } from '@hey-api/openapi-ts';
export default defineConfig({
input: './openapi.yaml',
output: './src/generated',
plugins: ['@hey-api/client-fetch'],
});Step 2: Run It Once to Check the Structure
npm run codegenFiles like these will appear under src/generated/ (the list and names of generated files can vary by version and plugin combination, so treat the actual output after the first run as your reference).
src/generated/
├── schemas.gen.ts # OpenAPI schemas → TypeScript types
├── sdk.gen.ts # API call functions
├── types.gen.ts # Request/response types
└── client.gen.ts # fetch client instanceThe .gen.ts suffix on file names is a visual signal that says "this file is generated." I've learned this the hard way — I once edited one of these directly, only to have the next codegen run overwrite everything.
Step 3: Use It in Your Code
import { getUser } from './generated/sdk.gen';
const { data, error } = await getUser({ path: { userId: 42 } });
if (error) throw error;
console.log(data.email); // inferred as email: stringIf the backend renames the email field to emailAddress, this line will produce a compile error after the next codegen run — at build time, not at runtime.
Step 4: Lock in the Rules with AGENTS.md
Here's the key part. Codex CLI automatically reads AGENTS.md when it opens a project — the equivalent of CLAUDE.md for Claude Code. If you nail down the generated-file rules here, agents can no longer say "I'll just tweak this file slightly."
# AGENTS.md
## Generated Code Rules
The `src/generated/` directory contains code auto-generated from the OpenAPI spec.
Files in this directory must never be modified directly.
If you need to change API types or client code:
1. Modify `openapi.yaml` first
2. Run `npm run codegen` to regenerate
3. Include all generated files in the commit
## Codegen Command
```bash
npm run codegen
# See openapi-ts.config.ts for configuration
```
## Wrapper Pattern
If you need custom logic on top of the generated client, write a wrapper layer under `src/api/`.
Directly modifying `src/generated/` is not permitted.Once this is committed to the repository, Codex CLI reads these rules at the start of every session and follows them throughout.
Step 5: Delegate Regeneration to Codex CLI
When the spec changes, you can instruct Codex CLI like this:
codex "Reflect the changes in openapi.yaml and regenerate src/generated"With AGENTS.md in place, the agent won't modify files directly — it will run npm run codegen and then verify the generated output.
Usage Patterns — Monorepo, Backend, and Wrapper Layer
Hierarchical Placement in a Monorepo
In a monorepo where multiple packages each point to different backend specs, you can place AGENTS.md hierarchically. Codex CLI is designed to incorporate AGENTS.md files from subdirectories into its context, but the exact discovery rules and priority can vary by version — it's worth checking the official repository's AGENTS.md documentation for the latest behavior before deploying.
Put global rules in the root AGENTS.md (e.g., no direct modification of generated directories), and specify each package's codegen command in that package's own AGENTS.md.
Connecting to a FastAPI Backend
FastAPI automatically exposes /openapi.json. You can use this directly as input.
// openapi-ts.config.ts (for development)
import { defineConfig } from '@hey-api/openapi-ts';
export default defineConfig({
input: 'http://localhost:8000/openapi.json',
output: './src/generated',
plugins: ['@hey-api/client-fetch'],
});In CI/CD, exporting the spec file as an artifact and referencing it statically is more stable. Here's a GitHub Actions example:
# .github/workflows/codegen.yml
name: Codegen on spec change
on:
push:
paths:
- 'openapi.yaml'
jobs:
codegen:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run codegen
- uses: peter-evans/create-pull-request@v6
with:
title: 'chore: regenerate API client from updated spec'
branch: codegen/auto-updateWhen the spec changes, a PR is automatically opened and merged after review. Spec changes are treated as contract changes subject to review, not something that "sneaks in."
Custom Logic via the Wrapper Pattern
Not being able to touch generated code can feel restrictive at first. But separating the wrapper layer actually clarifies concerns.
The @hey-api/openapi-ts fetch client's standard approach is to configure the client instance or attach interceptors, rather than passing headers as parameters on each individual call. Here's a conceptual example reflecting that structure:
// src/api/client.ts — one-time setup at bootstrap
import { client } from '../generated/client.gen';
import { authStore } from '../stores/auth';
client.setConfig({
baseUrl: import.meta.env.VITE_API_BASE_URL,
});
client.interceptors.request.use((request) => {
const token = authStore.getToken();
if (token) {
request.headers.set('Authorization', `Bearer ${token}`);
}
return request;
});// src/api/users.ts — domain wrapper
import { getCurrentUser as getCurrentUserSdk } from '../generated/sdk.gen';
export async function getCurrentUser() {
const { data, error } = await getCurrentUserSdk();
if (error) {
throw new Error(`Failed to load current user: ${error.message ?? 'unknown'}`);
}
return data;
}src/generated/ is the zone we don't touch; src/api/ is the zone we control. Writing this boundary in AGENTS.md means AI agents respect it too.
Trade-offs — Honestly
This workflow isn't right for every situation. Here are things to consider before adopting it.
When it works well:
- Backend and frontend teams are separate, and the spec needs to serve as a contract
- The API surface is large and the cost of maintaining types manually is high
- Multiple AI agents are working within the same codebase
When to be careful:
- If the spec quality is poor, the types will be wrong. The generated client depends entirely on the accuracy of the spec. Feed it a spec littered with
any, and you getanyback. - Custom logic must be written in the wrapper layer. Adding auth headers, transforming errors, and similar concerns need to be written separately under
src/api/. - When using Codex CLI's auto-run mode, you need to explicitly configure how codegen tasks that involve network requests are approved. Codex CLI's approval and sandbox options are controlled via
--approval-modeand related sandbox flags — if your pipeline fetches a remote spec, refer to the approval modes documentation to verify actual behavior before adopting. - There's an upfront setup cost. Configuring the codegen tool, writing AGENTS.md, and integrating with CI will take about a day.
One more thing — AGENTS.md originally came from the Codex ecosystem, but other coding agents are also moving toward referencing similar project instruction files. Whether this will be standardized by an official body is hard to say, but the broader trend of multi-vendor support is observable. If you write it now, most of the AGENTS.md content will be reusable even if your tooling changes later.
Wrapping Up
The core of this workflow is simple: unify ownership of types into one place (the OpenAPI spec), and codify that rule in a file that both humans and agents can read (AGENTS.md).
Tech stacks change. @hey-api/openapi-ts may be replaced by something else, and you may end up using a different agent instead of Codex CLI. But two things remain: openapi.yaml as the contract, and AGENTS.md specifying how that contract should be handled. You can swap out tools, but the contract and the rules carry forward. That's what makes a spec-driven workflow genuinely compelling.
References
- openai/codex AGENTS.md — Official GitHub
- AGENTS.md for OpenAI Codex: Complete Setup and Configuration Guide 2026
- AGENTS.md Playbook 2026: Codex CLI Hierarchy + Monorepo
- AGENTS.md: the agent instructions file — AgentProtocol
- Typesafe API Code Generation for React in 2026
- OpenAPI to TypeScript: Zero API Drift in 2026
- Which OpenAPI Codegen Should You Choose? — DEV Community
- Codex CLI Complete Reference 2026
- Codex CLI approval policies and sandbox modes explained
- Building Consistent Workflows with Codex CLI & Agents SDK — OpenAI Cookbook