OpenAPI code generation eliminates an entire class of bugs: the client-server type mismatch. When your TypeScript client types are generated from the same spec your Go server validates against, a breaking API change fails at compile time — not in production. Claude Code generates OpenAPI specs from existing code, generates client and server code from specs, and helps design APIs contract-first before any implementation.
This guide covers OpenAPI code generation with Claude Code: writing specs, generating TypeScript clients with openapi-typescript, Go servers with oapi-codegen, mock servers for development, and CI validation.
CLAUDE.md for OpenAPI Workflows
## OpenAPI Workflow
- OpenAPI 3.1 specs in api/openapi.yaml
- TypeScript client: generated with openapi-typescript (types only, no fetcher)
- Go server: generated with oapi-codegen (interface + types, impl in handlers/)
- Mock server: Prism for local development
- CI: spectral for linting, oasdiff for breaking change detection
## Invariants
- Never edit generated files — regenerate instead
- Add x-go-type annotations for custom Go types (time.Time, uuid.UUID)
- All endpoints document error responses: 400, 401, 403, 404, 500
- Required fields have no default — optional fields have explicit defaults
Contract-First API Design
Design the OpenAPI spec for an Orders API before writing any implementation.
Include pagination, filtering, proper error schemas, and examples.
# api/openapi.yaml
openapi: 3.1.0
info:
title: Orders API
version: 1.0.0
description: Order management for the e-commerce platform
servers:
- url: https://api.example.com/v1
description: Production
- url: http://localhost:8080/v1
description: Local development
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
OrderStatus:
type: string
enum: [pending, confirmed, shipped, delivered, cancelled]
Money:
type: object
required: [amount_cents, currency]
properties:
amount_cents:
type: integer
minimum: 0
example: 4999
currency:
type: string
pattern: '^[A-Z]{3}$'
example: USD
OrderItem:
type: object
required: [product_id, quantity, unit_price]
properties:
product_id:
type: string
format: uuid
product_name:
type: string
quantity:
type: integer
minimum: 1
maximum: 100
unit_price:
$ref: '#/components/schemas/Money'
Order:
type: object
required: [id, user_id, status, items, total, created_at]
properties:
id:
type: string
format: uuid
user_id:
type: string
format: uuid
status:
$ref: '#/components/schemas/OrderStatus'
items:
type: array
items:
$ref: '#/components/schemas/OrderItem'
total:
$ref: '#/components/schemas/Money'
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
CreateOrderRequest:
type: object
required: [items, shipping_address_id]
properties:
items:
type: array
minItems: 1
maxItems: 50
items:
type: object
required: [product_id, quantity]
properties:
product_id:
type: string
format: uuid
quantity:
type: integer
minimum: 1
maximum: 100
shipping_address_id:
type: string
format: uuid
PaginatedOrders:
type: object
required: [items, total, page, per_page, pages]
properties:
items:
type: array
items:
$ref: '#/components/schemas/Order'
total:
type: integer
page:
type: integer
per_page:
type: integer
pages:
type: integer
Error:
type: object
required: [error, code]
properties:
error:
type: string
code:
type: string
details:
type: object
additionalProperties: true
security:
- BearerAuth: []
paths:
/orders:
get:
operationId: listOrders
summary: List orders for the authenticated user
parameters:
- name: page
in: query
schema:
type: integer
default: 1
minimum: 1
- name: per_page
in: query
schema:
type: integer
default: 20
minimum: 1
maximum: 100
- name: status
in: query
schema:
$ref: '#/components/schemas/OrderStatus'
responses:
'200':
description: Paginated list of orders
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedOrders'
'401':
$ref: '#/components/responses/Unauthorized'
post:
operationId: createOrder
summary: Create a new order
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOrderRequest'
examples:
singleItem:
summary: Single item order
value:
items:
- product_id: "550e8400-e29b-41d4-a716-446655440000"
quantity: 2
shipping_address_id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
responses:
'201':
description: Order created
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'422':
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/orders/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
get:
operationId: getOrder
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'404':
$ref: '#/components/responses/NotFound'
components:
responses:
Unauthorized:
description: Authentication required
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
TypeScript Client Generation
Generate a type-safe TypeScript client from the OpenAPI spec.
I want strong types but I'll use my own fetch wrapper.
# Install openapi-typescript (types only — no runtime code)
npm install -D openapi-typescript
# Generate types
npx openapi-typescript api/openapi.yaml -o src/types/api.d.ts
// src/api/orders-client.ts
import type { paths, components } from '../types/api';
type Order = components['schemas']['Order'];
type CreateOrderRequest = components['schemas']['CreateOrderRequest'];
type PaginatedOrders = components['schemas']['PaginatedOrders'];
// Type-safe parameters extracted from OpenAPI spec
type ListOrdersParams = paths['/orders']['get']['parameters']['query'];
export class OrdersClient {
constructor(
private baseUrl: string,
private getToken: () => string,
) {}
async list(params?: ListOrdersParams): Promise<PaginatedOrders> {
const url = new URL(`${this.baseUrl}/orders`);
if (params?.page) url.searchParams.set('page', String(params.page));
if (params?.per_page) url.searchParams.set('per_page', String(params.per_page));
if (params?.status) url.searchParams.set('status', params.status);
const response = await fetch(url, {
headers: { Authorization: `Bearer ${this.getToken()}` },
});
if (!response.ok) {
const error = await response.json();
throw new ApiError(response.status, error.error);
}
return response.json();
}
async create(data: CreateOrderRequest): Promise<Order> {
const response = await fetch(`${this.baseUrl}/orders`, {
method: 'POST',
headers: {
Authorization: `Bearer ${this.getToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
const error = await response.json();
throw new ApiError(response.status, error.error);
}
return response.json();
}
}
Go Server Stub Generation
# oapi-codegen config
cat > oapi-codegen.yaml << 'EOF'
package: handlers
generate:
chi-server: true # Generate Chi router interface
models: true # Generate types from schemas
spec: false
output: internal/generated/api.gen.go
EOF
go generate ./... # Run: oapi-codegen -config oapi-codegen.yaml api/openapi.yaml
// internal/handlers/orders.go — implements the generated interface
package handlers
import (
"net/http"
"orders-service/internal/generated"
"orders-service/internal/service"
)
// Compile-time check: OrdersHandler implements the generated interface
var _ generated.StrictServerInterface = (*OrdersHandler)(nil)
type OrdersHandler struct {
svc service.OrdersService
}
func (h *OrdersHandler) ListOrders(w http.ResponseWriter, r *http.Request, params generated.ListOrdersParams) {
page := 1
if params.Page != nil {
page = *params.Page
}
// Implementation here — interface enforced by compiler
}
CI: Spec Linting and Breaking Change Detection
# .github/workflows/api-validation.yml
name: API validation
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Lint OpenAPI spec with Spectral
uses: stoplightio/spectral-action@latest
with:
file_glob: 'api/openapi.yaml'
- name: Check for breaking changes with oasdiff
run: |
# Compare current spec against main branch
git show origin/main:api/openapi.yaml > /tmp/old-spec.yaml
npx oasdiff breaking /tmp/old-spec.yaml api/openapi.yaml \
--fail-on ERR # Only fail on breaking changes, not warnings
- name: Validate generated code is up to date
run: |
go generate ./...
git diff --exit-code internal/generated/
# Fails if generated code doesn't match spec
For contract testing between services using Pact (consumer-driven contracts that complement OpenAPI), see the contract testing guide. For API gateway patterns that apply rate limiting and auth in front of OpenAPI services, see the API gateway guide. The Claude Skills 360 bundle includes API design skill sets covering OpenAPI, versioning strategies, and SDK generation. Start with the free tier to try OpenAPI code generation.