Design‑First OpenAPI 3.x Workflow with Contract Testing for Microservices
Keep your API docs, server code, and client SDKs in sync by writing the OpenAPI spec first, generating stubs, and running contract tests. This post walks through a concrete example, trade‑offs, and a practical checklist for teams.
27 Jan 2026, 12:11 UTC

Why a Design‑First OpenAPI Workflow?
In a microservice landscape, the contract between a provider and its consumers is the single source of truth. When the contract lives in a human‑readable spec, teams can agree on expectations before writing any code. The OpenAPI 3.x specification gives that contract a formal shape: paths, parameters, request/response schemas, security, and examples all live in one YAML or JSON file. When the spec is the source of truth, the risk of drift between documentation, server implementation, and client SDKs drops dramatically.
Key Ingredients of the Workflow
- Write the spec first. Define all endpoints, payloads, and security in a single file.
- Generate code. Use
openapi-generatororswagger-codegento produce server stubs (Spring Boot, Node/Express, Go Chi, etc.) and typed client SDKs (TypeScript, Java, Go). - Contract test. Run a spec‑driven test suite (e.g.,
schemathesisordredd) against the running service to validate that the actual API matches the spec. - CI integration. Lint the spec with
spectral, bundle it withredocly, and run contract tests on every PR. - Publish the spec. Host the rendered YAML at a stable URL (e.g.,
/openapi.yaml) so consumers can fetch it at build time.
Concrete Example: Polymorphic Payment Method
Below is a minimal OpenAPI 3.1 document that defines a single endpoint for creating an order. The request body can be one of several payment methods, each with its own schema. The discriminator field tells the consumer which schema applies.
openapi: 3.1.0
info:
title: Order Service
version: 1.0.0
paths:
/orders:
post:
operationId: createOrder
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OrderRequest'
responses:
"201":
description: Order created
content:
application/json:
schema:
$ref: '#/components/schemas/OrderResponse'
components:
schemas:
OrderRequest:
type: object
required: [items, payment]
properties:
items:
type: array
items:
$ref: '#/components/schemas/OrderItem'
payment:
oneOf:
- $ref: '#/components/schemas/CreditCardPayment'
- $ref: '#/components/schemas/PayPalPayment'
discriminator:
propertyName: type
mapping:
credit-card: '#/components/schemas/CreditCardPayment'
paypal: '#/components/schemas/PayPalPayment'
OrderItem:
type: object
required: [productId, quantity]
properties:
productId:
type: string
quantity:
type: integer
minimum: 1
CreditCardPayment:
type: object
required: [type, cardNumber, expiry]
properties:
type:
type: string
enum: [credit-card]
cardNumber:
type: string
expiry:
type: string
PayPalPayment:
type: object
required: [type, email]
properties:
type:
type: string
enum: [paypal]
email:
type: string
OrderResponse:
type: object
properties:
orderId:
type: string
status:
type: string
enum: [created]
Generate a Spring Boot Server Stub
Run the following command in a terminal where openapi-generator-cli is installed (requires Java 17+):
openapi-generator-cli generate \
-i order-service.yaml \
-g spring \
-o ./order-service-server \
--additional-properties=interfaceOnly=true,library=webflux
Key points:
-g springtells the generator to produce a Spring WebFlux server.interfaceOnly=truecreates only interfaces; you can add your own implementation in a separate module to avoid overwriting on regeneration.- Generated code lives under
./order-service-server/src/main/java/com/example/orders.
Generate a TypeScript Client SDK
For front‑end or another microservice that needs to consume the API, generate a typed client:
openapi-generator-cli generate \
-i order-service.yaml \
-g typescript-axios \
-o ./order-service-client
This yields an index.ts file with strongly‑typed request/response models and helper functions.
Run Contract Tests with Schemathesis
Assuming the server is running locally on http://localhost:8080 and serves the spec at /openapi.yaml, execute:
schemathesis run http://localhost:8080/openapi.yaml \
--request-method POST \
--request-path /orders \
--request-body "{\"items\": [{\"productId\": \"p1\", \"quantity\": 2}], \"payment\": {\"type\": \"credit-card\", \"cardNumber\": \"4111111111111111\", \"expiry\": \"12/24\"}}"
Expected checks:
- Response status code 201.
- Response body matches
OrderResponseschema. - No validation errors on request body.
If any of these checks fail, the test will report a mismatch, indicating that the implementation no longer satisfies the contract.
Trade‑offs and Limitations
- Feature support. Not all generators handle OpenAPI 3.1 keywords (callbacks, webhooks, JSON Schema 2020‑12). Test your target language before committing to 3.1.
- Over‑specification. Locking down every enum or optional field can make backward‑compatible evolution harder. Prefer flexible schemas and versioned paths (e.g.,
/v1/orders) for breaking changes. - Behavior vs. shape. OpenAPI describes the shape of requests/responses, not side‑effects. Business rules (idempotency, rate limits, eventual consistency) still need separate documentation or ADRs.
- Generated code maintenance. Generated stubs often require hand‑written adapters. Keep them in a separate module to avoid overwrites on regeneration.
- Large specs. A monolithic spec can be hard to review. Split into domain files and bundle with
redocly bundleorswagger-clifor distribution. - Security schemes. The spec only defines how to authenticate; authorization logic remains in code.
Practical Checklist for Teams
- Define a versioned OpenAPI spec in a dedicated repo or branch.
- Run
spectral linton every PR to catch structural issues. - Generate server stubs and client SDKs on CI; compare generated code against a baseline to detect accidental changes.
- Run contract tests (
schemathesisordredd) against the latest build; fail the pipeline if mismatches occur. - Host the spec at a stable URL; tag releases with the spec version.
- Document non‑spec behavior (rate limits, idempotency) in ADRs or README files.
- When a breaking change is required, bump the API path version (e.g.,
/v2/orders) and keep the old version for a deprecation window.
Actionable Takeaway
Adopting a design‑first OpenAPI 3.x workflow with contract testing turns your API into a living contract. Start by writing a minimal spec, generate stubs, and run a spec‑driven test against a local instance. Once the pipeline is in place, every change to the API surface will be validated automatically, keeping documentation, server code, and client SDKs in sync and reducing integration surprises.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.