OpenAPI, Schema-First Contracts, and Client SDKs
LESSON
OpenAPI, Schema-First Contracts, and Client SDKs
The core idea: OpenAPI turns an API contract into a checkable artifact, but the trade-off is that every precise schema also becomes a promise clients may rely on.
Core Insight
Imagine the course platform now has three clients: a web app, a mobile app, and a partner integration. All three call POST /courses/{course_id}/reviews. The backend team changes the review response from rating: 5 to rating: {"score": 5, "scale": 5} because the product may later support ten-point ratings. The backend tests pass. The deployment succeeds. Then the mobile app crashes because its generated client expected rating to be an integer.
That failure is not really a serialization bug. It is a contract bug. The server and clients had different beliefs about the shape of the API boundary. OpenAPI is useful because it gives those beliefs one explicit place to live: operations, parameters, request bodies, response bodies, error payloads, authentication requirements, examples, and reusable schemas.
The non-obvious part is that a schema is not just documentation. Once clients generate SDKs, write type-safe code, branch on error codes, or validate responses in tests, the schema becomes executable evidence. A field type, enum value, required property, status code, or pagination token can become a compatibility promise.
The trade-off is precision versus flexibility. A vague contract lets the backend change more freely, but clients must guess and break later. A precise contract lets machines check compatibility, generate clients, and catch mistakes before deployment, but it also forces the team to decide which details are stable enough to promise.
The Contract Artifact
An OpenAPI file describes the public surface of an HTTP API. It says: these paths exist, these methods are allowed, these parameters are accepted, these request bodies are valid, these responses may come back, and these schemas define the shapes.
For the review endpoint, the contract is not only "there is a POST route." It includes the route parameter, authentication expectation, request body, success response, and error responses:
paths:
/courses/{course_id}/reviews:
post:
operationId: createCourseReview
parameters:
- name: course_id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateReviewRequest"
responses:
"201":
description: Review created
content:
application/json:
schema:
$ref: "#/components/schemas/ReviewResponse"
"400":
description: Invalid request body
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
"409":
description: Review already exists
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
This is a small example, but it changes the engineering conversation. Instead of saying "the endpoint returns a review," the contract forces sharper questions. Is course_id a string or number? Is comment optional or required? Can the API return 409? Does an error response use the same Problem Details shape described in the previous lesson? Which fields can clients branch on?
OpenAPI is not the only way to describe an API, and it is not magic. The important mechanism is the explicit contract artifact. It is useful because humans can review it, tools can validate it, and clients can build against it.
Schema-First and Code-First
Teams usually create OpenAPI specs in one of two ways. In a code-first flow, the implementation is written first and tooling extracts the spec from route annotations, controller types, serializers, or framework metadata. In a schema-first flow, the contract is designed first and implementation follows it.
Neither style is automatically better. The design question is which artifact is authoritative when there is disagreement.
Code-first can be pragmatic when one backend owns the API and clients are internal. The risk is that accidental implementation details become public contract. A field appears because a serializer included it. A 500 appears because an exception path was not documented. The generated spec can describe what the server currently does without proving that the shape was intentionally designed.
Schema-first is valuable when multiple teams, SDKs, or external clients depend on the boundary. It makes the API reviewable before the backend is finished. A mobile engineer can ask whether an enum needs an unknown fallback. A partner engineer can test their client against examples. The backend team can stub the endpoint and wire validation before persistence exists.
The cost is synchronization. A schema-first process fails if the OpenAPI file becomes a wish list that the server does not actually satisfy. A code-first process fails if the generated file is treated as documentation but never reviewed as a contract. The healthy version, in either style, is to connect the spec to automated checks.
contract design
-> generated server/client types
-> request and response validation
-> compatibility diff in CI
-> examples used in tests and docs
The contract should not sit beside the system as a static document. It should participate in the development loop.
Worked Compatibility Check
Return to the rating change. Suppose the current public schema says:
ReviewResponse:
type: object
required: [id, course_id, rating, comment]
properties:
id:
type: string
course_id:
type: string
rating:
type: integer
minimum: 1
maximum: 5
comment:
type: string
Changing rating from an integer to an object is breaking for existing clients. A generated TypeScript SDK might expose:
type ReviewResponse = {
id: string
course_id: string
rating: number
comment: string
}
If the server starts returning an object, the client type is wrong. The safest design is usually additive:
ReviewResponse:
type: object
required: [id, course_id, rating, comment]
properties:
id:
type: string
course_id:
type: string
rating:
type: integer
minimum: 1
maximum: 5
rating_details:
type: object
required: [score, scale]
properties:
score:
type: integer
scale:
type: integer
comment:
type: string
Now old clients can keep reading rating, and new clients can opt into rating_details. Later, the API can deprecate rating with a planned versioning strategy. The schema makes that compatibility discussion concrete.
The same logic applies to errors. If clients branch on problem.type == "review-already-exists", changing that string is a breaking change even if the HTTP status remains 409. Error schemas are not secondary; they are part of the same boundary contract.
Client SDKs Change the Stakes
Generated SDKs are useful because they move API knowledge into typed functions, models, and exceptions. Instead of every client manually building URLs and parsing dictionaries, the generated client can expose createCourseReview(courseId, body) and return a typed ReviewResponse.
That improves speed and consistency, but it also hardens assumptions. A generated SDK may encode required fields, enum values, nullable properties, response status handling, authentication headers, and retry wrappers. Once distributed, that SDK becomes another compatibility surface. Breaking the OpenAPI contract can mean breaking compiled mobile apps, partner integrations, or internal services that update slowly.
This is why contract review should look at how clients will consume the schema:
- Required fields are promises that the server will provide them.
- Optional fields require clients to handle absence.
- Enums should consider future values or an
unknownstrategy. - Nullable fields should be intentional, not a substitute for unclear design.
- Error types and codes should be stable if clients branch on them.
- Examples should demonstrate realistic success and failure payloads.
Precision is still worth it. Without precision, generated clients become weak wrappers around any or untyped maps. The goal is not to avoid promises. The goal is to make only the promises the system can keep.
Operational Failure Modes
Issue: The spec says one thing and the server returns another.
Why it is tempting: The implementation moves quickly, while the OpenAPI file is updated manually after the fact.
Correction: Validate real responses against the schema in tests or at the boundary. Treat contract drift as a build or release problem, not a documentation cleanup task.
Issue: The schema describes only happy paths.
Why it is tempting: Success responses are easier to model, and errors feel like implementation detail.
Correction: Document expected 400, 401, 403, 404, 409, and transient failure responses where clients need different behavior. Reuse a stable problem schema rather than inventing one payload per handler.
Issue: Generated SDKs hide breaking changes until release.
Why it is tempting: If backend tests pass, the server feels correct.
Correction: Run compatibility diffs on the OpenAPI file and regenerate SDKs in CI or pre-release checks. If a change breaks generated client types, decide whether to version, deprecate, or redesign the change.
Issue: The contract is too vague to protect clients.
Why it is tempting: Loose schemas make backend changes easier in the short term.
Correction: Tighten the fields that clients actually need for decisions, display, retries, and support. Leave extension points deliberately, not accidentally.
Close the lesson and inspect one endpoint you have built or used. Write down the request schema, the success schema, two error schemas, and one compatibility promise a client might rely on. Then ask: would a machine be able to detect if the server broke that promise?
Connections
The previous lesson treated error responses as semantic contracts. OpenAPI is where those semantics become visible to client teams and compatibility tooling.
The next lesson applies the same contract discipline to list endpoints. Pagination cursors, sort order, filter names, and total counts are not just implementation choices; they are promises about how clients traverse changing data.
Contract design also connects back to versioning. A schema diff gives the team concrete evidence for deciding whether a change is additive, breaking, deprecated, or suitable for a new API version.
Resources
- [SPEC] OpenAPI Specification
- Focus: Study paths, parameters, request bodies, responses, reusable schemas, and examples as the core contract surface.
- [DOC] OpenAPI Generator
- Focus: See how OpenAPI contracts become generated clients, server stubs, and typed models.
- [DOC] RFC 9457: Problem Details for HTTP APIs
- Focus: Connect structured error payloads to documented OpenAPI response schemas.
- [ARTICLE] Martin Fowler: Consumer-Driven Contracts
- Focus: Compare schema-based API contracts with tests driven by consumer expectations.
Key Takeaways
- OpenAPI is valuable because it turns API assumptions into a reviewable and machine-checkable contract artifact.
- Schema precision helps clients, SDKs, and CI catch breakage, but every precise field can become a compatibility promise.
- Generated SDKs amplify both the benefits and the risks of a contract because they encode API assumptions into client code.
- Error responses, pagination shapes, enum values, and examples deserve the same contract discipline as successful response bodies.
← Back to Backend and API Architecture