The Unspoken Rules: Philosophy & First Principles of API Design
Every API tells a story, but the most important chapters are often the ones written in invisible ink. You won’t find them in the official documentation, the OpenAPI spec, or the schema definitions. They live in the unspoken agreements, the silent expectations, and the fragile trust between the producer and the consumer. Before we dive into the syntax of RESTful URIs, the elegance of GraphQL queries, or the efficiency of gRPC protobufs, we need to calibrate our compass. This chapter is that calibration. We’re going to strip away the technology choices and examine the foundational philosophy—the first principles—that separate an API that gracefully evolves from one that becomes a legacy nightmare. Think of this as the "Zen and the Art of API Maintenance" portion of the book. We’ll talk about coupling, cohesion, evolvability, and the sacred, non-negotiable contract. Because at the end of the day, an API isn’t just a technical interface; it’s a promise. And like any good promise, its strength lies not in the flourish of the signature, but in the clarity of the understanding beneath it.
Let’s start with a parable from the real world, one I call "The Case of the Mysterious `user_status` Field." Imagine you’re integrating with a third-party identity service. Their documentation proudly lists a `GET /users/{id}` endpoint returning a JSON payload. One of the fields is `user_status`, an enum. The docs say: "Possible values: `active`, `inactive`, `pending`." Your application, a sleek customer portal, uses this to show a green dot for active users. Life is good. Then, one Tuesday morning, without a deprecation notice, a fraction of your active users start showing gray dots. Panic. You check the logs. The API is now returning a new value: `"suspended"`. Your code, doing a simple equality check `status == "active"`, treats `"suspended"` as inactive. Customers scream. Support tickets flood in. The third-party service’s response? "`suspended` is just another form of inactive, which was always implied." Their mental model of "inactive" was broader than yours. The contract, as you understood it, was broken. The unspoken rule—that enums are closed, finite sets—was violated. This isn't a bug in their code; it's a failure of contract design. It’s a crack in the foundation of your integration. This is the world we live in. This is why we need principles.
The Four Pillars: Coupling, Cohesion, Evolvability, Contract
If API design had a Mount Rushmore, these four concepts would be the faces staring down at us, judging our every `curl` command. They are interdependent, often in tension, and ignoring any one of them is a recipe for long-term pain. Let’s meet them properly.
1. Coupling: The Invisible Strings That Bind (and Break)
Coupling is the measure of interdependence between software modules. In API land, it’s the degree to which the client’s logic is entangled with the server’s internal implementation details. High coupling is like building your house on a cliff edge; a small shift in the bedrock (a backend refactor) and your entire living room plummets into the sea (your app crashes).
We often talk about tight coupling versus loose coupling. But a more useful spectrum for API designers is connascence, a term coined by David Parnas and popularized by Meilir Page-Jones. Connascence describes the ways in which two components depend on each other. For APIs, the key types are:
- **Connascence of Name**: The client knows the *name* of a field or endpoint. (`/users` vs `/accounts`). This is the weakest form and unavoidable, but changing names is a breaking change.
- **Connascence of Type**: The client knows the *data type* (string, integer, object). Changing a field from `string` to `number` breaks clients that assume string operations.
- **Connascence of Meaning**: The client knows the *semantic meaning* of a value. This is the `user_status` parable. `"active"` means one thing to you, something subtly different to them. This is the most insidious and common source of breakage.
- **Connascence of Position**: The client knows the *order* of parameters in a positional argument list (more common in RPC-style APIs). Changing the order breaks clients.
- **Connascence of Algorithm**: The client knows the *exact algorithm* used to compute a value. If the server changes how it calculates `score`, the client’s interpretation may become invalid.
The goal is to minimize connascence, especially the stronger forms (Meaning, Algorithm). How? Through abstraction and contractual clarity.
> 📱 Digital Edition Sidebar: The "IKEA API" Analogy > Think of a tightly coupled API like an IKEA bookshelf where every piece is uniquely shaped and only fits in one specific spot. Change one dowel, and the whole thing collapses. A loosely coupled API is like a set of standard, modular bricks (think LEGO). You can rearrange, replace, or even substitute bricks from another set as long as they adhere to the same stud pattern (the contract). The power is in the standard interface, not the specific brick.
Concrete Example: The Payment Processor Imagine a `POST /charges` endpoint.
- **Tightly Coupled (Bad):** `{ "amount": 1999, "currency": "USD", "user_id": "abc123", "card_last4": "4242" }`. The client is now coupled to the server’s internal `user_id` format, and the presence of `card_last4` suggests the server is storing raw card fragments—a PCI compliance nightmare waiting to happen. If the server switches to a payment token system (`payment_method: "pm_123"`), this client breaks.
- **Loosely Coupled (Better):** `{ "amount": 1999, "currency": "USD", "customer": "cus_ABC123" }`. The client knows a `customer` identifier exists, but it doesn't care if it's an internal DB ID or a Stripe-like token. The server abstracts its persistence details. The contract is about *what* (a customer), not *how*.
The unspoken rule? Your API should reveal *what* the system does, not *how* it does it. The moment you leak implementation details (database column names, internal service names, specific algorithm steps), you increase coupling and lock yourself into a path.
2. Cohesion: The Art of Being "About One Thing"
Cohesion is the degree to which the elements inside a module belong together. For an API endpoint or a resource, high cohesion means it has a single, well-defined purpose. Low cohesion is a "junk drawer" endpoint that does everything: `GET /do-everything?action=createUser&action=sendEmail&action=logAudit`.
High cohesion is your first line of defense against complexity and coupling. Why? Because a cohesive resource has a clear, focused contract. Its fields and behaviors are logically related. This makes it easier to understand, easier to test, and crucially, easier to evolve without side effects.
The Single Responsibility Principle (SRP) for APIs: An API resource or endpoint should have one reason to change. That reason should be tied to a single business concept or actor.
Concrete Example: The E-commerce Catalog
- **Low Cohesion (The "God Object"):** A `Product` resource that includes: product details (`name`, `price`), inventory levels (`stock_count`), supplier information (`supplier_name`, `supplier_lead_time`), and pricing rules (`discount_strategy`, `tiered_pricing`). Why does this object change? If the supplier changes their name? If the inventory system gets upgraded? If the marketing team wants a new discount type? *Everything* touches this one object. It’s a change amplifier.
- **High Cohesion (The "Bounded Context"):** We split based on responsibility.
- `Product` (Catalog context): `{ "id", "name", "description", "sku" }`. Changes when product info changes.
- `InventoryItem` (Inventory context): `{ "sku", "stock_count", "warehouse_location" }`. Changes when stock moves.
- `Supplier` (Procurement context): `{ "id", "name", "contact" }`.
The relationships are maintained by the client or a backend orchestrat