Skip to main content

What an API Contract Is and Who It Is For

What This Concept Is

An API contract is the written, enforceable agreement between a service and its callers. It specifies what a caller may send, what the service promises to return, and what is not guaranteed. It is not your code, your database schema, or your team's intent. It is whatever a consumer can observe and depend on.

A contract typically includes:

  • endpoint URLs or RPC names
  • request and response shapes, including field types, presence, and units
  • status codes and error models
  • authentication and authorization rules
  • quotas, rate limits, and idempotency guarantees
  • behavioral guarantees: side effects, ordering, retries, eventual vs strong consistency

Everything you promise a consumer, even informally, becomes part of the contract in practice. If you log a consistent error.code field for a year, consumers will build on it, and removing it will break them.

Why It Matters Here

A contract is the thing that survives your refactors. Implementation changes constantly; the contract either stays stable or it breaks the ecosystem on top of it. Every cluster that follows in this module defends, narrows, or evolves this contract:

  • REST verbs and status codes are contract syntax (Cluster 2)
  • custom actions and LROs are contract shape choices (Cluster 3)
  • RPC and GraphQL are alternate contract formats (Cluster 4)
  • versioning and deprecation are how contracts move over time (Cluster 5)

If you cannot name what your API promises, you cannot safely change it.

Concrete Example

Two views of the same endpoint. The implementation view is internal. The contract view is what consumers see.

Implementation (pseudocode):

@app.post("/orders")
def create_order(user_id, items):
total = price_service.quote(items)
order = Order(user_id=user_id, items=items, total=total, status="PENDING")
db.save(order)
queue.publish("order.created", order.id)
return jsonify(order.to_dict()), 201

Contract (OpenAPI excerpt):

/orders:
post:
summary: Place a new order
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateOrderRequest"
responses:
"201":
headers:
Location:
schema: { type: string }
description: URL of the created order
content:
application/json:
schema: { $ref: "#/components/schemas/Order" }
"400": { $ref: "#/components/responses/ValidationError" }
"409": { $ref: "#/components/responses/IdempotencyConflict" }

The contract is silent about price_service, the queue, and the database. Consumers only see the request schema, the 201 Created with a Location header, the Order response, and the error model. Replacing the queue or the database is not a breaking change; renaming total to amount is.

The audience splits into distinct roles:

  • Direct consumers: the developers writing client code against this API
  • Indirect consumers: their users, who feel every outage or breaking change
  • Your future team: who will be held to everything this document says
  • Auditors / regulators: who read the contract to verify controls

Design for the direct consumer's reading experience, but remember the others exist.

Common Confusion / Misconception

"We don't have a contract; we just have code." You do have a contract - it is whatever the code currently returns. The risk is that nobody has written it down, so every change is implicitly breaking because nobody knows what callers depend on.

"The OpenAPI spec is the contract." Only if you treat it as authoritative. If the spec says one thing and the server returns another, the server wins - consumers build against what ships, not what is documented. Generate the spec from the implementation (via tests or reflection) or enforce it with schema validation.

"Internal APIs don't need contracts." Two teams are two consumers. The moment Team B ships code that calls Team A's API, there is a contract between them. Not writing it down does not remove the coupling; it just makes it invisible.

How To Use It

When starting or reviewing any API:

  1. State the audience in one sentence: "this API is for X building Y."
  2. Inventory what is promised: endpoints, fields, status codes, idempotency, ordering, SLAs.
  3. Inventory what is deliberately not promised: response time, internal IDs, ordering within unstructured fields.
  4. Write the contract down (OpenAPI, gRPC .proto, GraphQL SDL) and check it into the same repo as the service.
  5. Review the contract as a separate artifact from the code in code review.

"Contract review" should be a distinct PR gate, even if the spec lives alongside the implementation.

Check Yourself

  1. A consumer starts relying on the order of fields in a JSON response. Is field order part of your contract? Should it be?
  2. Your error responses include a stack trace in debug mode. Is the stack trace part of the contract? What happens when you remove it?
  3. Your service always returns created_at in UTC with trailing Z. Is timezone format part of the contract?

Mini Drill or Application

Pick an API you have shipped (or use a public API you depend on). Produce a one-page contract document answering:

  1. Who is the audience?
  2. What are the promised request shapes and response shapes?
  3. What status codes may be returned? What does each mean?
  4. What is the error model (fields, codes, semantics)?
  5. What is not guaranteed (latency, internal ordering, retry semantics)?
  6. Where is this written down, and how is drift detected?

If any answer is "I don't know," that is a contract gap your consumers will find first.

Read This Only If Stuck