---
title: 'What Is an API'
source: 'https://academia.sh/en/courses/api-design/what-is-an-api'
course: 'Web API Design'
language: en
updated: '2026-08-19T05:19:29+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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 `status` field's `onShelf` value 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.

```js
// 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.

```js
// 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}`); }
```

```bash
#!/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?
