OpenAPI Client Generation skill for Codex
Typed clients from the spec, regenerated in one command. Codex makes the OpenAPI spec generator-ready first, using a bundled checker for missing or duplicate operationIds, inline schemas and broken refs, then picks the lightest generator for the stack and pins it. Auth, retries and error mapping live in a wrapper instead of edited generated files, and CI fails when the committed client drifts from the spec.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- openapi-client-generation Installs to ~/.agents/skills/openapi-client-generation/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install openapi-client-generation The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project openapi-client-generation Installs to .agents/skills/openapi-client-generation/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/openapi-client-generation.zipunzip -o openapi-client-generation.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) openapi-client-generation PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $openapi-client-generation, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Generates typed API clients from OpenAPI 3.x specs - openapi-typescript with openapi-fetch, Hey API, Orval for React Query, openapi-python-client, oapi-codegen for Go, Kiota and OpenAPI Generator - with spec linting, stable operationIds, pinned generator versions, auth and retry wrappers, and CI drift checks. Use when asked to generate an SDK or client from a swagger or OpenAPI file, replace hand-written fetch calls with typed ones, regenerate after an API change, or fix generated code that has bad names or any types.
Use it when
- Turning an OpenAPI or Swagger file into a typed client or SDK
- Replacing hand-written HTTP calls with generated, type-checked ones
- Regenerating a client after the API changed
- Generated code has unstable names, any types or missing models
Use something else when
- You are designing the API and writing its spec: use openapi-first
- You need mocks for tests rather than a client: use api-mocking-msw
How to check the result
The readiness script reports no errors, the app type-checks against the regenerated client without new casts, and the CI drift check passes on a clean checkout.
The mistake it prevents
Hand-editing generated files to fix a type, which the next regeneration silently erases.
Done well means: the generated client compiles with no any/unknown holes on the endpoints the
app uses, names are stable across regenerations because the spec has explicit operationIds, the
generator version and spec snapshot are pinned, regeneration is one command, and CI fails when the
committed client drifts from the spec.
Inspect first
- Read
AGENTS.mdfor language, package manager, and where generated code should live. - Locate the spec: a repo file, a URL (
/openapi.json,/v3/api-docs,/swagger.json), or a vendor download. Fetching remote specs needs network; save a copy into the repo either way. - Check
openapi:version. Swagger 2.0 must be converted to 3.x first (for example withswagger2openapi) or many generators produce poor types. - Look for an existing generated client and its generator (
rg -n "openapi-generator|orval|hey-api|openapi-typescript|oapi-codegen" -g '!node_modules'). Regenerate with the same tool unless the user wants a switch.
Make the spec generator-ready
npx @redocly/cli lint openapi.yaml # or: npx @stoplight/spectral-cli lint openapi.yaml
python3 scripts/codegen_readiness.py openapi.json
codegen_readiness.py (standard library; YAML needs PyYAML or a JSON bundle first) reports missing or
duplicate operationIds, non-identifier ids, inline object schemas, unresolved local $refs and
operations without a 2xx response. Exit code 1 means errors.
Fixes, in order of impact:
| Finding | Effect on generated code | Fix |
|---|---|---|
| Missing operationId | Names like getApiV1UsersId that change when paths change | Add operationId: getUser |
| Inline object schemas | Anonymous types (InlineResponse2001) | Move to components/schemas with a name |
| No 2xx schema | Return type unknown / void | Describe the success body |
nullable vs type: [x, "null"] mixups | Wrong optionality | Match the spec’s 3.0 or 3.1 dialect |
Missing required arrays | Every field optional | List required properties |
Polymorphism without discriminator | Union types that cannot narrow | Add discriminator.propertyName |
If the spec belongs to a third party, do not edit it in place. Keep the vendor file untouched and
apply fixes with an overlay (redocly decorators, an OpenAPI Overlay file, or a small patch script)
so the next vendor update is a clean replace.
Choose a generator
| Stack and need | Default | Output |
|---|---|---|
TypeScript, minimal runtime, keep native fetch | openapi-typescript + openapi-fetch | Types only + tiny typed fetch wrapper |
| TypeScript SDK with functions per operation, optional Zod / TanStack Query | @hey-api/openapi-ts | Generated SDK functions and types |
| React app wanting ready-made TanStack Query hooks and MSW mocks | Orval | Hooks per operation |
| Python | openapi-python-client (httpx + attrs); datamodel-code-generator for Pydantic models only | Package |
| Go | oapi-codegen | Types + client in one file |
| .NET, Java, many languages from one spec | Kiota or OpenAPI Generator (needs Java) | Full SDK |
Prefer the lightest option that types the calls the app makes; heavy SDK generators add runtime classes that are hard to review.
TypeScript: openapi-typescript + openapi-fetch
npx openapi-typescript ./openapi/api.yaml -o ./src/api/schema.d.ts
// src/api/client.ts
import createClient, { type Middleware } from 'openapi-fetch';
import type { paths } from './schema';
const auth: Middleware = {
async onRequest({ request }) {
request.headers.set('Authorization', `Bearer ${await getToken()}`);
return request;
},
};
export const api = createClient<paths>({ baseUrl: import.meta.env.VITE_API_URL });
api.use(auth);
// usage: path, params and body are checked against the spec
const { data, error, response } = await api.GET('/users/{id}', { params: { path: { id: userId } } });
if (error) throw new ApiError(response.status, error);
TypeScript: Hey API
// openapi-ts.config.ts
import { defineConfig } from '@hey-api/openapi-ts';
export default defineConfig({
input: './openapi/api.yaml',
output: 'src/client',
plugins: ['@hey-api/client-fetch', '@tanstack/react-query'],
});
Run with npx @hey-api/openapi-ts. Pin the exact version in package.json; the generator’s output
changes between minor versions.
Python and Go
uvx openapi-python-client generate --path openapi/api.yaml --output-path clients/acme_api --overwrite
# oapi-codegen.yaml
package: acmeapi
output: internal/acmeapi/client.gen.go
generate:
models: true
client: true
go get -tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest # Go 1.24+ tool directive
go tool oapi-codegen -config oapi-codegen.yaml openapi/api.yaml
Wrap, do not edit, generated code
- Never hand-edit generated files; the next run erases the change. Put auth, retries, base URL, telemetry and error mapping in a thin wrapper or middleware.
- Retries: only for idempotent methods (GET, PUT, DELETE) or requests with an idempotency key;
back off on 429 and 503 honouring
Retry-After. - Map error responses to one error type the app understands; keep the raw status and body.
- Mark generated folders in
.gitattributes(src/client/** linguist-generated=true) so reviews collapse them, and exclude them from lint and coverage.
Regeneration workflow
{
"scripts": {
"api:fetch": "curl -fsSL https://api.example.com/openapi.json -o openapi/api.json",
"api:gen": "openapi-typescript openapi/api.json -o src/api/schema.d.ts",
"api:check": "npm run api:gen && git diff --exit-code -- src/api/"
}
}
- Fetch or update the spec snapshot (network) and commit it with the generated output.
- Lint and run
codegen_readiness.py. - Generate with the pinned version.
- Type-check the whole app (
tsc --noEmit,mypy,go build ./...): breaking API changes show up as compile errors at call sites; fix them in the same PR. - Run
api:checkin CI so a spec change without regeneration fails the build.
Verification
- Compile: the app type-checks against the new client with no new
anyor// @ts-ignore. - Contract: call one read endpoint against a dev server or recorded fixture and confirm the response parses into the generated type (with Zod or Pydantic validation where enabled).
- Diff review: read the generated diff summary; renamed functions usually mean an operationId changed.
- When a type is wrong, fix the spec (or overlay) and regenerate; do not cast at call sites.
Gotchas
additionalPropertiesdefaults to allowed in OpenAPI; some generators then emit index signatures that weaken types. Set it explicitly when the shape is closed.format: date-timebecomesstringin most TS generators; convert at the wrapper boundary if the app needsDate.- Large integers (
int64) overflow JavaScript numbers; keep them as strings in the spec when IDs exceed 2^53. - Tags drive file splitting in some generators; untagged operations land in a catch-all file.
- A server spec generated from code (FastAPI, springdoc, NestJS) may differ per environment; snapshot from the deployed version the client targets.
Output
Report: spec source and version, generator and pinned version, files generated, spec fixes or overlays applied, wrapper features, call sites changed, and the CI drift check added.
What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "OpenAPI Client Generation"
short_description: "Typed clients from the spec, regenerated in one command"
default_prompt: "$openapi-client-generation Generate a typed TypeScript client from our openapi.yaml and replace the raw fetch calls" scripts/codegen_readiness.py
#!/usr/bin/env python3
"""Check an OpenAPI 3.x document for problems that produce bad generated clients.
Usage: python3 codegen_readiness.py <spec.json | spec.yaml>
YAML needs PyYAML if installed; otherwise convert first, for example
npx @redocly/cli bundle openapi.yaml --ext json -o openapi.json
Checks: missing or duplicate operationIds, operationIds that are not identifiers,
inline object schemas in request/response bodies (anonymous generated types),
unresolved local $refs, and missing 2xx responses. Exits 1 on errors.
"""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
METHODS = ("get", "put", "post", "delete", "options", "head", "patch", "trace")
IDENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
def load(path: Path):
text = path.read_text(encoding="utf-8")
if path.suffix.lower() in (".yaml", ".yml"):
try:
import yaml # type: ignore
except ImportError:
sys.exit("error: PyYAML not installed; convert the spec to JSON first (see --help)")
return yaml.safe_load(text)
return json.loads(text)
def resolve(doc, ref: str):
if not ref.startswith("#/"):
return True # external refs are out of scope
node = doc
for part in ref[2:].split("/"):
part = part.replace("~1", "/").replace("~0", "~")
if not isinstance(node, dict) or part not in node:
return False
node = node[part]
return True
def walk_refs(doc, node, where, errors):
if isinstance(node, dict):
ref = node.get("$ref")
if isinstance(ref, str) and not resolve(doc, ref):
errors.append(f"{where}: unresolved $ref {ref}")
for k, v in node.items():
walk_refs(doc, v, f"{where}/{k}", errors)
elif isinstance(node, list):
for i, v in enumerate(node):
walk_refs(doc, v, f"{where}[{i}]", errors)
def inline_object(schema) -> bool:
if not isinstance(schema, dict) or "$ref" in schema:
return False
if schema.get("type") == "object" and schema.get("properties"):
return True
if schema.get("type") == "array":
return inline_object(schema.get("items"))
return False
def main(argv: list[str]) -> int:
if len(argv) != 1 or argv[0] in ("-h", "--help"):
print(__doc__.strip())
return 2
doc = load(Path(argv[0]))
errors: list[str] = []
warnings: list[str] = []
version = str(doc.get("openapi", ""))
if not version.startswith("3."):
errors.append(f"expected an OpenAPI 3.x document, got openapi={version or 'missing'} (Swagger 2.0 needs conversion)")
seen: dict[str, str] = {}
ops = 0
for path, item in (doc.get("paths") or {}).items():
if not isinstance(item, dict):
continue
for method in METHODS:
op = item.get(method)
if not isinstance(op, dict):
continue
ops += 1
label = f"{method.upper()} {path}"
op_id = op.get("operationId")
if not op_id:
errors.append(f"{label}: missing operationId (generators invent unstable names)")
else:
if op_id in seen:
errors.append(f"{label}: duplicate operationId '{op_id}' (also {seen[op_id]})")
seen[op_id] = label
if not IDENT.match(op_id):
warnings.append(f"{label}: operationId '{op_id}' is not a plain identifier")
responses = op.get("responses") or {}
if not any(str(code).startswith("2") for code in responses):
warnings.append(f"{label}: no 2xx response defined; return type will be unknown")
for code, resp in responses.items():
for mt, media in ((resp or {}).get("content") or {}).items():
if inline_object((media or {}).get("schema")):
warnings.append(f"{label} {code} {mt}: inline object schema; move it to components/schemas")
body = ((op.get("requestBody") or {}).get("content") or {})
for mt, media in body.items():
if inline_object((media or {}).get("schema")):
warnings.append(f"{label} requestBody {mt}: inline object schema; move it to components/schemas")
walk_refs(doc, doc, "#", errors)
for w in warnings:
print(f"warn: {w}")
for e in errors:
print(f"error: {e}")
print(f"\n{ops} operations, {len(errors)} error(s), {len(warnings)} warning(s)")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.