Lesson 01 / 34
What Is an API
What makes an interface a contract: the consumer's unwritten assumptions, defining compatible and breaking change by consumer behavior, the different response a tolerant reader and a strict reader give to the same change, and who owns the contract.
Contents
The previous course established the cases where the server hands out a file or a document to the outside: content a human will read. The library lending service’s actual consumers, however, are programs. The shelf display at the branch queries a book’s status, the report job that runs at night pulls a listing by author, and the audit script checks the shape of the records. All three call the same endpoint, and all three expect something different from it.
The concept that emerges at this point is the application programming interface: the callable surface a program opens to other programs. This lesson establishes why that surface counts as a contract and when the contract breaks.
The Contract Is More Than the Body
Looking at the JSON body an endpoint returns is not the same as seeing the whole contract. The contract covers everything the two sides can expect from each other:
- Surface: which addresses exist, which methods they accept, which parameters they recognize.
- Format: the names, types, requiredness, and value sets of the fields in the body.
- Meaning: what a field represents. Does the
statusfield’sonShelfvalue report the book’s physical location, or its availability for loan? - Behavior: the result of making the same request twice, which status code reports errors, whether ordering is guaranteed.
Only some of this is written down. The unwritten part is still part of the contract, because the consumer relies on it. A consumer who notices that a list always comes back in the same order writes code that assumes that order; when the order changes, the consumer breaks even though no “documented” part of the interface has changed. The contract’s boundary is not drawn by the scope of the documentation but by everything the consumer has come to depend on.
Compatibility Is a Measurement Question
Whether a change is breaking is defined not by the size of the change but by whether existing consumers keep working after it. This definition is measurable: it is enough to run the same consumers against different published contract versions.
The library service’s book endpoint is written so it can publish three separate versions.
// server.mjs — the endpoint that publishes a book record. The CONTRACT variable selects the published version. import { createServer } from "node:http"; const CONTRACT = process.env.CONTRACT ?? "v1"; const RECORD = { isbn: "978-0262033848", title: "Introduction to Algorithms", author: "Cormen", status: "onShelf" }; const buildBody = () => { if (CONTRACT === "v1") return RECORD; if (CONTRACT === "v2-addition") return { ...RECORD, pages: 1312 }; // field added return { isbn: RECORD.isbn, title: RECORD.title, // field name and type changed author: { name: RECORD.author }, loanStatus: RECORD.status, pages: 1312 }; }; createServer((req, res) => { res.sendDate = false; const body = JSON.stringify(buildBody()); res.setHeader("Content-Type", "application/json; charset=utf-8"); res.setHeader("Content-Length", Buffer.byteLength(body)); res.writeHead(200).end(body); }).listen(8441, "127.0.0.1");
Three consumers represent three separate reading habits. The shelf display reads only the two fields it needs. The report job also reads few fields, but it depends on one field’s type. The audit script checks the body as a whole and counts an unrecognized field as an error.
// consumer.mjs — three separate consumers. Usage: node consumer.mjs <shelfDisplay|report|auditor> const role = process.argv[2]; const record = await (await fetch("http://127.0.0.1:8441/books/978-0262033848")).json(); const ROLES = { // Tolerant reader: reads only the fields it needs, ignores fields it does not recognize. shelfDisplay: (r) => { if (typeof r.status !== "string") throw new Error("status field missing"); return `shelf display: ${r.title} -> ${r.status}`; }, // Tolerant, but depends on a field's type. report: (r) => { if (typeof r.author !== "string") throw new Error("author field is not a string"); return `report line: ${r.author.toUpperCase()} / ${r.title}`; }, // Strict reader: rejects every field not listed in the contract. auditor: (r) => { const EXPECTED = ["isbn", "title", "author", "status"]; const extra = Object.keys(r).filter((f) => !EXPECTED.includes(f)); if (extra.length) throw new Error(`unexpected field: ${extra.join(", ")}`); return `check: ${r.isbn} valid`; }, }; try { console.log(` ${role.padEnd(13)} OK ${ROLES[role](record)}`); } catch (e) { console.log(` ${role.padEnd(13)} BROKEN ${e.message}`); }
#!/usr/bin/env bash # Tests three contract versions against three consumers. A=http://127.0.0.1:8441/books/978-0262033848 for c in v1 v2-addition v2-change; do CONTRACT=$c node server.mjs & p=$! echo "== published contract: $c ==" printf ' body: ' curl -s --retry 20 --retry-all-errors --retry-delay 0 --retry-connrefused "$A"; echo for role in shelfDisplay report auditor; do node consumer.mjs $role; done kill $p 2>/dev/null; wait $p 2>/dev/null || true done
== published contract: v1 ==
body: {"isbn":"978-0262033848","title":"Introduction to Algorithms","author":"Cormen","status":"onShelf"}
shelfDisplay OK shelf display: Introduction to Algorithms -> onShelf
report OK report line: CORMEN / Introduction to Algorithms
auditor OK check: 978-0262033848 valid
== published contract: v2-addition ==
body: {"isbn":"978-0262033848","title":"Introduction to Algorithms","author":"Cormen","status":"onShelf","pages":1312}
shelfDisplay OK shelf display: Introduction to Algorithms -> onShelf
report OK report line: CORMEN / Introduction to Algorithms
auditor BROKEN unexpected field: pages
== published contract: v2-change ==
body: {"isbn":"978-0262033848","title":"Introduction to Algorithms","author":{"name":"Cormen"},"loanStatus":"onShelf","pages":1312}
shelfDisplay BROKEN status field missing
report BROKEN author field is not a string
auditor BROKEN unexpected field: loanStatus, pages
Nine results give three rules.
Adding a field is not inherently compatible. Adding the pages field left two
consumers unaffected and broke the audit script. The provider’s “I only added something”
claim showed up as an error on the consumer side. For an addition to count as compatible
depends on consumers ignoring fields they do not recognize.
Renaming a field is the same as deleting it. When the status field was renamed to
loanStatus, that field ceased to exist for the shelf display. Even though the provider’s
intent was “fixing the name,” what the consumer sees is a deleted field.
Changing a type is breaking even when the name is preserved. The author field is
still in place, but it is now an object instead of a string. A check that only looks at
field names does not catch this change; the report job breaks at run time.
The Two Directions of Compatibility
The measurement tested only one direction: the server new, the consumers old. This direction is called backward compatibility, and it is the criterion that determines the provider’s release decision, because the server is updated before the consumers.
The reverse direction also exists, and it is easy to miss: the consumer new, the server
old. Forward compatibility is a consumer written against a contract not yet published
being able to work with the old server. In the library service, this situation arises when
the terminals are updated before the central branch: the new terminal tries to read the
pages field, and the old server never sends that field. Whether the terminal crashes or
shows a blank when the field is missing is the consumer’s decision, not the contract’s.
The two directions together give a release rule: the server that adds the field is published first, then the consumer that reads the field; the order reverses when a field is removed. A contract change is not a single event but a sequential transition between two sides.
The Tolerant Reader
The audit script in the measurement caused its own break by its own rule: it raised an error when it saw a field not listed in the body. The opposite of this behavior is the tolerant reader: a consumer that reads only the fields it needs and ignores everything it does not recognize.
Tolerant reading is a consumer habit, but its result concerns the provider: an interface stays extensible to the degree that its consumers read tolerantly. In a system with strict-reading consumers, every addition requires a version bump; in a system with tolerant-reading consumers, additions roll out silently.
This has a limit in the reverse direction. Tolerance does not apply to input validation.
When the server ignores fields it does not recognize in an incoming body, a consumer’s
typo is silently swallowed: a client that writes dueDat instead of dueDate gets no
error, runs with the default value, and only learns of the mistake from the result. The
rule is therefore different in the two directions: be strict in what you send, be
tolerant in what you receive.
The Consumer Owns the Contract
The owner of an interface is the party that publishes it, but the party that determines the contract’s validity is the consumer. This distinction has two practical consequences.
First, the cost of a breaking change grows with the number of consumers. In an internal
interface with a single consumer, renaming the status field means publishing both sides
together. In an interface distributed to terminals across branches, the same change means
every terminal that has not been updated breaks. The same technical change is two separate
decisions in two contexts.
Second, the wider the written part of the contract, the smaller the share of unwritten assumptions the consumer has to rely on. When the types, requiredness, and value sets of fields are explicitly stated, the consumer binds by “reading” rather than by “observing.” If it is written that ordering is not guaranteed, a consumer that relies on ordering is at fault. This is where the value of a machine-readable definition comes from, and it is covered later in this course.
Summary
- An API’s contract is wider than its body format: surface, format, meaning, and behavior together make up the contract, and the unwritten assumptions the consumer relies on fall within this scope.
- Whether a change is breaking is defined by whether existing consumers keep working after it, and it is measured by running the consumers against the new version.
- In the measurement, adding a field broke the strict-reading consumer, renaming a field had the same effect as deleting it, and changing a type produced a run-time break even though the name was preserved.
- The tolerant reader ignores fields it does not recognize and keeps the interface extensible through additions; the same tolerance does not apply to validating incoming requests.
- The contract’s cost grows with the number of consumers; every undocumented detail is a surface the consumer will bind to by observation.
Next Step
This lesson established what a contract is but left the contract’s shape open: why is a book record read from an address, why is it not requested with a procedure call, why does the client not say which fields it wants? The same job can be met with three separate approaches — resource-based, remote-procedure-call style, and query-based — and the difference among these three approaches is not a matter of taste. The next lesson writes all three for the same scenario and shows the difference among them with three measures: how many requests went out, how many bytes were carried, and how much of the carried data was used?
To keep your progress and take notes, Log in
My notes
Log in to take notes.