Your First OpenAPI Spec with Claude: Contract-First API Design
A complete guide to building an OpenAPI specification with Claude Code using a contract-first approach — from feature spec to OpenAPI YAML, generating Go server code, and validating contracts between services.
In many teams, API design happens implicitly — developers write code, endpoints appear, and documentation is written after the fact. That is an expensive anti-pattern: consumer services must wait for the implementation to finish before they know how to call it, changes can break silently, and there is no single source of truth for the contract.
Contract-first API design reverses the order: define the API contract first in an OpenAPI specification, review it with stakeholders, then implement. In this article we learn how to use Claude Code to generate a production-grade openapi spec golang claude code straight from a feature spec you have already written.
08.1 Why Contract-First Is More Than Just Documentation
Contract-first is not merely about nicer docs — it delivers concrete engineering leverage. The four benefits below are the reasons the extra up-front step pays for itself.
Parallel Development: Frontend and backend can work simultaneously. The frontend mocks the API from the contract while the backend implements against it — neither waits on the other.
Explicit Breaking-Change Detection: When the contract changes, every consumer learns immediately through schema validation instead of a runtime error in production.
Testable Contract: An OpenAPI-defined contract can drive automated contract testing between producer and consumer.
More Accurate AI Code Generation: Given a precise OpenAPI contract, Claude Code generates handlers, middleware, and tests that hit the target far more reliably.
The takeaway: contract-first buys you parallelism, safety, testability, and better AI output at the cost of one disciplined step — writing the contract before the code.
08.2 From Feature Spec to OpenAPI: Workflow
Before generating anything, it helps to see where the OpenAPI spec sits in the pipeline. The diagram below shows the feature spec fanning out into generated server code and contract tests.
1Feature Spec (specs/order/cancel-order.md)
2 │
3 ▼ [Claude Code]
4OpenAPI Spec (api/openapi.yaml)
5 │
6 ├─► Go Server Code (using oapi-codegen)
7 └─► Contract Tests (using schemathesis)What to take from the flow: the OpenAPI spec is the hub, not a byproduct. Everything downstream — generated Go types, the server interface, contract tests — is derived from it, which is exactly why getting the contract right first matters so much.
08.3 Prompt to Generate OpenAPI from Feature Spec
You rarely write OpenAPI YAML by hand anymore. The prompt below hands Claude your feature spec plus a set of conventions, and asks for a complete OpenAPI 3.0.3 document in return.
1Based on the following feature spec, generate an OpenAPI 3.0.3 specification in YAML format.
2
3Requirements:
41. Each AC mentioning an HTTP endpoint must be represented
52. Each error case must have a response schema with error_code and message fields
63. Use reusable components for schemas used in multiple places
74. Include a JWT Bearer security scheme
85. Response schema consistent: { data: ... } for success, { error_code: ..., message: ... } for error
96. Include realistic example values (not "string" but "ORD-20250715-ABC123")
10
11Additional context:
12- Base URL: /api/v1
13- Authentication: Bearer JWT (all endpoints except public)
14- All IDs are UUID v4
15
16---
17@specs/order/cancel-order.mdNotice requirement 6: forcing realistic examples up front is what makes the generated spec immediately usable for mocking and testing, instead of a skeleton full of "string" placeholders nobody can act on.
08.4 Complete OpenAPI Spec for Cancel Order
Prompts are abstract until you see the output they produce. The YAML below is the cancel-order slice Claude generates from the feature spec — note how each acceptance criterion maps to a specific status code and response schema.
1# api/openapi.yaml (excerpt: cancel order endpoint)
2openapi: "3.0.3"
3info:
4 title: Santekno Shop — Order Service API
5 version: "1.0.0"
6 description: API for managing orders in Santekno Shop
7
8servers:
9 - url: https://api.santekno.com/v1
10 description: Production
11 - url: http://localhost:8080/v1
12 description: Local development
13
14security:
15 - BearerAuth: []
16
17paths:
18 /orders/{id}:
19 delete:
20 operationId: cancelOrder
21 summary: Cancel an order
22 description: |
23 Cancels a PENDING order within the 15-minute cancellation window.
24 Restores stock for all items atomically.
25 Publishes ORDER_CANCELLED event to Kafka (best effort).
26 tags:
27 - orders
28 parameters:
29 - name: id
30 in: path
31 required: true
32 description: Order UUID
33 schema:
34 type: string
35 format: uuid
36 example: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
37 responses:
38 "204":
39 description: Order successfully cancelled
40 "401":
41 description: Authentication required
42 content:
43 application/json:
44 schema:
45 $ref: "#/components/schemas/ErrorResponse"
46 example:
47 error_code: UNAUTHORIZED
48 message: "authentication required"
49 "404":
50 description: Order not found
51 content:
52 application/json:
53 schema:
54 $ref: "#/components/schemas/ErrorResponse"
55 example:
56 error_code: ORDER_NOT_FOUND
57 message: "order not found"
58 "409":
59 description: Order cannot be cancelled
60 content:
61 application/json:
62 schema:
63 $ref: "#/components/schemas/OrderNotCancellableError"
64 examples:
65 wrongStatus:
66 summary: Order already confirmed
67 value:
68 error_code: ORDER_NOT_CANCELLABLE
69 message: "order cannot be cancelled: current status is CONFIRMED"
70 windowExpired:
71 summary: Cancellation window expired
72 value:
73 error_code: CANCEL_WINDOW_EXPIRED
74 message: "cancellation window has expired"
75
76components:
77 securitySchemes:
78 BearerAuth:
79 type: http
80 scheme: bearer
81 bearerFormat: JWT
82
83 schemas:
84 ErrorResponse:
85 type: object
86 required:
87 - error_code
88 - message
89 properties:
90 error_code:
91 type: string
92 example: "ORDER_NOT_FOUND"
93 message:
94 type: string
95 example: "order not found"
96
97 OrderNotCancellableError:
98 allOf:
99 - $ref: "#/components/schemas/ErrorResponse"
100 properties:
101 current_status:
102 type: string
103 enum:
104 - CONFIRMED
105 - SHIPPED
106 - DELIVERED
107 - CANCELLED
108 example: "CONFIRMED"The key details to carry forward: operationId: cancelOrder becomes the generated Go method name; the 409 carries two named examples (ORDER_NOT_CANCELLABLE and CANCEL_WINDOW_EXPIRED) so consumers see both failure modes; and reusable $ref components keep the error shape identical across every endpoint.
08.5 Validating OpenAPI Spec with Claude
Generation is only half the loop — you also want Claude to critique what it produced. The prompt below asks it to check the spec back against the feature spec and flag anything missing before you commit.
1Review the following OpenAPI spec and identify:
2
31. Are all ACs from the feature spec represented?
42. Are the response schemas complete and consistent?
53. Are there missing examples that would make the spec more useful?
64. Is naming consistent with existing API conventions?
75. Are there security concerns to add?
86. Can this spec be used directly to generate server code?
9
10Feature spec: @specs/order/cancel-order.md
11OpenAPI spec: @api/openapi.yamlThe takeaway: asking Claude to compare the spec against the feature spec closes the loop between the two documents, catching acceptance criteria that silently failed to make it into a path or response.
08.6 Generating Go Server Code from OpenAPI
Once the spec is approved, the contract becomes executable Go. The commands below install oapi-codegen, configure it, and generate a type-safe server interface from the YAML.
1# Install oapi-codegen
2go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
3
4# Configure
5cat > api/codegen-config.yaml << 'EOF'
6package: api
7generate:
8 - types
9 - server
10 - spec
11output: internal/delivery/http/api/api.gen.go
12EOF
13
14# Generate
15oapi-codegen -config api/codegen-config.yaml api/openapi.yamlWhat matters about the output: the generator emits a StrictServerInterface your handler must satisfy, so any drift between the contract and the implementation becomes a compile error rather than a runtime surprise.
08.7 Implementing the Handler from the Generated Interface
With the interface in hand, your job is to fill in the business logic behind it. The handler below implements the generated CancelOrder method, translating the use case result into the type-safe response the contract expects.
1// OrderHandler implements api.StrictServerInterface
2// Feature spec: specs/order/cancel-order.md v1.3
3func (h *OrderHandler) CancelOrder(
4 ctx context.Context,
5 request api.CancelOrderRequestObject,
6) (api.CancelOrderResponseObject, error) {
7
8 userID := ctx.Value(userIDKey{}).(uuid.UUID) // from auth middleware
9 input := cancelorder.CancelOrderInput{
10 OrderID: uuid.UUID(request.Id),
11 UserID: userID,
12 }
13
14 if err := h.cancelOrderUC.Execute(ctx, input); err != nil {
15 return h.mapCancelOrderError(err)
16 }
17
18 // AC7: 204 No Content
19 return api.CancelOrder204Response{}, nil
20}The design win here is compile-time enforcement: because the response types are generated from the OpenAPI spec, returning the wrong shape simply won’t build. The contract is no longer a document you hope everyone reads — it is enforced by the Go type checker.
08.8 Contract Testing
A contract is only trustworthy if both sides are tested against it. The command below runs schemathesis to fuzz your running server against the spec, verifying that real responses match the declared schemas.
1# Automated contract testing with schemathesis
2schemathesis run api/openapi.yaml \
3 --base-url http://localhost:8080/v1 \
4 --auth "Bearer $TEST_JWT_TOKEN" \
5 --endpoint "/orders/{id}" \
6 --method DELETEBeyond fuzzing the producer, Consumer-Driven Contract Testing with Pact lets each consumer assert exactly the interaction it depends on — so both sides of a service boundary can be verified independently, without spinning up a full integration environment.
08.9 API Versioning Strategy
A living contract needs an explicit versioning policy so consumers know what a version bump implies. The snippet below encodes a semantic-versioning convention directly in the spec’s info block.
1info:
2 version: "1.3.0"
3 # MAJOR: breaking change (field removed, path changed)
4 # MINOR: backward-compatible addition (new field added)
5 # PATCH: clarification, documentation update onlySemantic versioning tells consumers what changed, but detecting the change is a job you can hand to Claude. The prompt below diffs two spec versions and classifies every difference.
1Compare these two OpenAPI spec versions and identify:
21. Breaking changes: will break existing consumers
32. Non-breaking additions: backward compatible
43. Deprecations: fields/endpoints being phased out
5
6Old version: @api/openapi-v1.yaml
7New version: @api/openapi-v2.yamlThe takeaway: pairing a versioning convention with automated diffing means a breaking change can never slip through unlabeled — the version number and the change classification always agree.
08.10 CI/CD Integration
Contract validation should run on every pull request, not on a good day when someone remembers. The workflow below lints the spec and checks for breaking changes automatically whenever openapi.yaml changes.
1# .github/workflows/api-validation.yml
2name: API Contract Validation
3on:
4 pull_request:
5 paths: ['api/openapi.yaml']
6
7jobs:
8 validate:
9 runs-on: ubuntu-latest
10 steps:
11 - uses: actions/checkout@v4
12
13 - name: Validate OpenAPI spec
14 run: npx swagger-parser validate api/openapi.yaml
15
16 - name: Check for breaking changes
17 uses: oasdiff/oasdiff-action@main
18 with:
19 base: refs/heads/main
20 revision: refs/heads/${{ github.head_ref }}The point of wiring this into CI is that it makes the contract self-enforcing: an unintended breaking change fails the build before it ever reaches a consumer, turning governance from a promise into a gate.
08.11 API Contract Change Governance
Once multiple teams depend on a contract, changing it needs a documented process rather than a quiet merge. The policy below distinguishes non-breaking from breaking changes and assigns the right reviewers and timelines to each.
1## Non-breaking changes (add field, new endpoint)
2- PR review by: API Lead + Consumer Team Lead
3- Approval: 1 from each, then merge
4
5## Breaking changes (remove field, change type, change path)
6- RFC required 2 sprints before the change
7- Review period: 1 week for all consumer teams
8- Deprecation notice: old field/endpoint deprecated for 1 sprint
9- Removal: only after all consumers have migrated
10
11## Endpoint deprecation marker
12deprecated: true
13x-deprecation-notice: "Will be removed in API v2 on 2026-01-01"The takeaway: heavier process is reserved for the changes that can actually hurt consumers. Non-breaking additions stay fast, while breaking changes carry an RFC and a deprecation window — so velocity and safety are balanced, not traded.
08.12 OpenAPI for Microservice Contracts
In a distributed system the most dangerous dependencies are the invisible ones. Publishing an explicit consumer contract, as in the layout below, makes each service’s dependency on another a documented, reviewable artifact.
1api/
2├── openapi.yaml ← full API spec
3└── contracts/
4 └── consumed-by-notification-service.yaml ← subset consumed by another serviceThe value of a dedicated contract file is that it names the coupling: the Notification Service’s dependency on the Order Service is no longer folklore but a versioned document, which is exactly what prevents silent breaking changes across service boundaries.
08.13 Generating Test Cases from OpenAPI with Claude
The spec that generated your handler can also generate the tests that exercise it. The prompt below asks Claude to produce comprehensive Go tests covering every response the contract declares.
1Based on the OpenAPI spec for DELETE /orders/{id},
2generate comprehensive test cases in Go using testify/suite and gomock.
3
4Test cases must cover:
51. All response codes defined in the spec (204, 401, 404, 409, 500)
62. Edge cases implied by the spec (concurrent requests, expired window)
73. Schema validation — the response body must match the defined schema
8
9Follow the pattern from: internal/usecase/order/create_order_test.goThe takeaway: because the tests are derived from the same contract as the handler, they stay in lockstep with it — regenerate the spec and you regenerate both the code it must satisfy and the tests that prove it does.
08.14 Common OpenAPI + Go Issues
A handful of mismatches between OpenAPI and Go’s type system trip up almost everyone. The list below captures the four that recur most and the one-line fix for each.
- Issue 1 — UUID type: use
format: uuid, not justtype: string, so the generator emits a real UUID type. - Issue 2 — int64 overflow in JSON: annotate with
x-go-type: int64to avoid precision loss when a client parses large numbers. - Issue 3 — circular schema references: design schemas flat, since circular
$refs are poorly supported across tooling. - Issue 4 — polymorphic responses: use
oneOfwithdiscriminator.propertyNameset toerror_codeto model multiple error shapes.
The takeaway: nearly all Go/OpenAPI friction comes from the type boundary, and each case above is fixed in the spec — not by hand-editing generated code, which you should never do.
08.15 OpenAPI as an API Documentation Source
A single spec can fan out into every documentation format your teams need. The commands below turn openapi.yaml into browsable HTML, a Postman collection, and hand-written curl examples.
1# Generate HTML docs with Redoc
2npx redoc-cli bundle api/openapi.yaml -o docs/api.html
3
4# Generate a Postman collection
5npx openapi-to-postman --spec api/openapi.yaml --output docs/postman.json
6
7# Generate curl examples with Claude
8claude "Generate curl command examples for all endpoints in api/openapi.yaml
9 Include request body examples and expected responses"The takeaway: because all of these outputs derive from one contract, your docs, Postman collection, and examples can never drift apart — regenerate them from the spec and they are consistent by construction.
08.16 Schema Design Best Practices
Good OpenAPI is mostly good schema hygiene. The five practices below are the ones that keep a spec consistent, tool-friendly, and pleasant to consume as it grows.
- Use
$refaggressively — extract any schema used more than once intocomponents/schemas. - Provide realistic examples —
example: "ORD-20250715-ABC1234", neverexample: "string". - Mark required fields explicitly — put
required: [field1, field2]on every object schema. - Use enums for status fields —
enum: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]. - Separate error schemas — one schema per error type unlocks better tooling and clearer clients.
The takeaway: these are cheap habits with compounding returns — followed consistently, they keep a large contract as legible on day 300 as it was on day one.
08.17 Spec-to-Code Traceability
Six months later, someone will ask why a handler behaves the way it does. A short comment linking the code back to both the contract and the feature spec, as below, answers that question instantly.
1// CancelOrder implements DELETE /orders/{id}
2// OpenAPI operationId: cancelOrder
3// Feature spec: specs/order/cancel-order.md v1.3
4func (h *OrderHandler) CancelOrder(...) (api.CancelOrderResponseObject, error) {The takeaway: three comment lines make any behavior traceable to its contract and its rationale, turning “why does this return 409?” from an archaeology project into a one-line lookup.
08.18 Tips & Gotchas
Contract-first has its own set of habits worth keeping and traps worth avoiding. The tips and gotchas below distill the ones that most affect day-to-day work.
💡 Tip 1: Start with a minimal spec and add detail iteratively — don’t try to perfect it before implementation begins.
💡 Tip 2: Use $ref aggressively for consistency — shared schemas guarantee identical shapes across endpoints.
💡 Tip 3: Have consumer teams review before implementation — contract-first only enables parallel work if consumers approve the contract first.
💡 Tip 4: Version the spec in git with clear commit messages that state whether a change is breaking or non-breaking.
⚠️ Gotcha 1: Never edit generated files manually — edit the spec and regenerate.
⚠️ Gotcha 2: OpenAPI can’t express every business rule — a rule like “15-minute window” still lives in the feature spec; OpenAPI only captures the HTTP contract.
⚠️ Gotcha 3: Avoid circular references — they aren’t reliably supported across OpenAPI tooling.
⚠️ Gotcha 4: Examples must be realistic — placeholder examples make the spec markedly less useful.
The through-line: treat the spec as the single source of truth — evolve it deliberately, keep it reviewed, and never route around it by hand-editing generated artifacts.
08.19 Beyond REST: The Same Principle Everywhere
Contract-first is a mindset, not a REST-only trick. The same “define the contract first” discipline applies across every interface style your system exposes:
- gRPC: Proto files serve the same role as an OpenAPI spec.
- GraphQL: Schema definition files (
.graphql) are the contract. - Kafka: AsyncAPI is the equivalent for event-driven APIs.
The takeaway is invariant across all of them: define the contract first, get stakeholder alignment, then implement — whether the payload rides on HTTP, gRPC, GraphQL, or an event bus.
08.20 Summary
Contract-first API design with OpenAPI delivers concrete engineering benefits: parallel development, explicit breaking-change detection, testable contracts, and more accurate AI code generation.
The workflow we established: Feature spec → OpenAPI spec (via Claude) → generated Go code → contract tests → API documentation.
Key tools:
oapi-codegen— generates type-safe Go interfaces from OpenAPI.schemathesis— automated contract testing against a running server.oasdiff— breaking-change detection in CI/CD.
Critical governance:
- Version the OpenAPI spec with semantic versioning.
- Breaking changes need an RFC and a deprecation period.
- Consumer teams must review the contract before implementation starts.
In the next article we cover the dimension of specifications most often skipped: Non-Functional Requirements — the performance, security, and scalability targets that must exist in the spec before implementation begins.