Systems · 30 min read
APIs and Contracts
An API is a boundary with a contract: allowed inputs, promised outputs, and failure rules. Implicit contracts break silently. Explicit ones fail loudly and evolvable.
Why this exists
Programs call other programs across teams, machines, and time. Without a shared contract, each side guesses. Guesses rot when one side changes.
REST over HTTP is one popular style for networked APIs. The deeper problem is older and wider: what may a caller send, what must a callee return, and what stays stable across versions?
This lesson teaches contracts and abstraction boundaries first. The REST APIs page then shows how HTTP resources and methods express those ideas on the web.
Axioms & primitives
- 01A contract states inputs, outputs, errors, and guarantees a caller may rely on.
- 02An API is an abstraction boundary: callers depend on the contract, not on hidden internals.
- 03Implicit contracts still exist. They are just undocumented and easy to break.
- 04Backward compatibility is a promise about which changes keep old callers working.
- 05Failing loudly at the boundary beats corrupting data behind it.
- 06Versioning is a tool for changing contracts without pretending nothing changed.
Learning objectives
After this lesson you should be able to:
- Write a one-paragraph contract for a function or HTTP endpoint that names inputs, outputs, and error cases.
- Explain why an undocumented field that callers start depending on becomes part of the real contract.
- Explain whether a proposed API change is breaking or non-breaking for existing callers and justify the decision.
- Give one example where failing fast at validation prevents silent corruption.
- Compare additive change, versioned endpoints, and negotiated capabilities as evolution strategies.
Progressive depth
Read the layers in order for a full explanation. Or open the layer you need.
Intuition
When you call a library function, you rely on a quiet deal: pass these arguments, get this kind of result, or get a defined error. That deal is a contract. Network APIs are the same deal across a wire.
If the deal is only in someone's head, the other side will invent a different deal. One team returns null. Another returns an empty list. One treats dates as timestamps. Another treats them as strings. Each choice can look fine in isolation. Together they produce fragile systems.
Abstraction hides internals behind a boundary. The contract is what makes that boundary usable. Callers should not need the callee's database schema. They need stable meaning for requests and responses.
Good contracts fail loudly. Bad inputs are rejected with a clear error. Bad silent coercion ("we guessed the string was a number") spreads corruption that surfaces far from the bug.
Formal shape
A useful API contract specifies: identity of operations or resources, input shape and validation rules, output shape, error model, authentication and authorization requirements, and performance or consistency guarantees you actually promise (best-effort vs durable write).
Postel's law (be conservative in what you send, liberal in what you accept) is often quoted. Modern API practice softens the liberal half: accept unknown fields carefully, but do not invent aggressive coercion that hides caller bugs. Be precise in what you emit.
Compatibility: a change is backward compatible if old callers keep working without edits. Additive optional fields are often compatible. Renames, type changes, removed fields, and tighter validation are often breaking. "Often" matters. Test with real clients.
Versioning strategies include URL versions (/v1/), header versions, and capability negotiation. Each shifts how callers discover change. None removes the need to document deprecation windows.
Contracts can be expressed as prose, OpenAPI/JSON Schema, protobuf schemas, tests, or all of these. The executable forms catch drift. Prose captures intent that schemas miss.
Worked examples
Example A: createUser(email, name). Contract: email must be valid and unique; on success return {id}; on duplicate return a conflict error. If the server instead inserts a second row with a suffix, callers that assume uniqueness will corrupt accounts. Loud conflict is better.
Example B: a JSON field status used to be "new" | "done". A client switches on those two. The server adds "blocked". Old clients may hit a default branch or crash. The contract changed. Options: treat unknown statuses as non-fatal, or version the API, or document a must-upgrade window.
Example C: pagination. If page size defaults to 20 and a caller depends on receiving all rows in one call, growth breaks them. The implicit contract was "result sets are tiny." Make limits explicit.
Example D: time zones. Storing local wall times without offsets creates ambiguous instants. The contract should state UTC instants or offset-aware values. Silent local-time strings are a classic cross-border bug.
Example E: HTTP mapping preview. Resources and methods on the REST APIs page are one way to publish contracts on the web. The contract ideas here still apply if you use RPC or queues instead.
Edge cases
Bug-for-bug compatibility: callers depend on incorrect behavior. Fixing the bug breaks them. You may need a flag, a new version, or a careful migration.
Partial failure in distributed calls: the contract should say whether retries are safe (idempotency). Without that, double charges appear.
Generated clients: schema changes that look minor can force mass client regenerations. Coordinate versioning with client release trains.
Security surface: every input field is an attack surface. Contracts should include size limits and authz requirements, not only happy-path shapes. See authentication vs authorization for the permission half.
Over-contracting: promising strict latency SLOs or total ordering you cannot keep creates breach of contract under load. Promise only what you can operate.
Undocumented side effects: an endpoint that also sends email has a side-effect contract. Callers and operators need to know.
Mental models
Power outlet
The shape and voltage are the contract. Appliances depend on that, not on how the power plant works. Change the shape and old plugs fail.
Train timetable
A timetable promises departure times and destinations. If trains leave early without notice, dependents miss them. That is a broken contract.
Shipping label
The label states size, address, and handling rules. A warehouse that ignores the label damages goods. Validation is reading the label before shipping.
Language grammar
Speakers share grammar so sentences parse. Slang can spread as a de facto dialect. APIs grow de facto fields the same way.
Common misconceptions
Myth
If both sides are in the same repo, you do not need a contract.
Reality
Same-repo callers still encode assumptions. A refactor that renames a field breaks those assumptions unless the contract is explicit and tested.
Myth
Contracts are only for public third-party APIs.
Reality
Internal service boundaries need contracts too. Organizational distance creates the same failure modes as company distance.
Myth
Adding a new JSON field is always safe.
Reality
It is often safe for tolerant readers. It can still break strict parsers, size limits, or logic that assumes a closed set of fields.
Myth
Version numbers remove the need for compatibility thinking.
Reality
Versions isolate change. You still must support old versions for a while or force a coordinated upgrade.
Exercises
Work these without looking up answers first. Check yourself against the intent notes.
Exercise 01
Write a contract for POST /reports that includes required fields, success response, two error cases, and one compatibility rule for future fields.
Hint: Write it so a second team could implement a client without reading your source code.
What good looks like
Includes input validation, success shape, explicit errors (e.g. 400/409), and a rule such as clients must ignore unknown fields; servers may add optional fields.
Exercise 02
For each change, say breaking or usually non-breaking for existing JSON clients: (a) add optional field, (b) remove field, (c) change field from string to number, (d) tighten max string length from 1000 to 50.
Hint: Ask whether an old caller can keep identical requests and still parse responses correctly.
What good looks like
a usually non-breaking; b breaking; c breaking; d often breaking for callers who sent longer values.
Exercise 03
A callee silently turns the string "12abc" into the number 12. What goes wrong for callers, and what should the contract do instead?
Hint: Prefer loud failure at the boundary over guessing.
What good looks like
Silent coercion hides bad input and can select wrong records. Contract should reject with a validation error.
Sources & further reading
External references. Prefer primary documents and clear explainers.
Go deeper on your own
These picks go past the foundation in this lesson. Use them when you want a primary spec, official docs, or a longer walkthrough.
Step-by-step guide to writing schemas for structured data. Use it when an API contract needs a precise payload shape.
Architectural constraints that make networked interfaces evolvable. Read it when contract design feels ad hoc.
- MDN: APIDocs
Short definition of APIs with links into web platform examples. Use it as a quick vocabulary check.