OpenAPI, Schema-First Contracts, and Client SDKs

LESSON

Backend and API Architecture

013 30 min intermediate

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:

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

Key Takeaways

PREVIOUS Backend Error Handling and Failure Semantics NEXT Pagination, Filtering, Sorting, and List API Semantics