Skip to content
Santekno.com | Level Up Your Engineering Skills
EN
📖 0%
24 Jul 2026 · 15 min read ·Article 8 / 208
Go

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.

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

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.

text
1Feature Spec (specs/order/cancel-order.md)
23         ▼ [Claude Code]
4OpenAPI Spec (api/openapi.yaml)
56         ├─► 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.

text
 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.md

Notice 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.

yaml
  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.

text
 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.yaml

The 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.

bash
 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.yaml

What 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.

go
 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.

bash
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 DELETE

Beyond 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.

yaml
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 only

Semantic 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.

text
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.yaml

The 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.

yaml
 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.

markdown
 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.

text
1api/
2├── openapi.yaml                               ← full API spec
3└── contracts/
4    └── consumed-by-notification-service.yaml  ← subset consumed by another service

The 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.

text
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.go

The 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 just type: string, so the generator emits a real UUID type.
  • Issue 2 — int64 overflow in JSON: annotate with x-go-type: int64 to 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 oneOf with discriminator.propertyName set to error_code to 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.

bash
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.

  1. Use $ref aggressively — extract any schema used more than once into components/schemas.
  2. Provide realistic examplesexample: "ORD-20250715-ABC1234", never example: "string".
  3. Mark required fields explicitly — put required: [field1, field2] on every object schema.
  4. Use enums for status fieldsenum: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED].
  5. 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.

go
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.

Related Articles

💬 Comments