Skip to content

API Design & Contracts

An API is the menu, not the kitchen — consumers should only ever need to know what they can ask for and what they’ll get back, never how it’s cooked.

  • What it is: designing the request/response contract for how other systems call yours (REST, GraphQL, gRPC), plus a versioning and deprecation strategy.
  • Why it matters: a stable contract lets other teams build against you with confidence; an unstable one breaks every integration whenever you refactor.
  • For developers: before changing a public endpoint’s shape, ask is this a breaking change for existing consumers? — if yes, version it.
  • When to use it: any time a component exposes functionality to another team, system, or external partner.
  • When not to use it: internal-only functions called from the same module don’t need a formal contract — that overhead is for boundaries that cross ownership.
  • Prerequisites: Service & Component Boundaries.
sequenceDiagram
    participant Consumer
    participant API as API (v1)
    participant Internal as Internal implementation
    Consumer->>API: Request (contract)
    API->>Internal: delegates
    Internal-->>API: result
    API-->>Consumer: Response (contract)
    Note over API,Internal: Internal implementation can change freely as long as the contract holds
  1. Define the request and response shape explicitly (fields, types, required vs. optional) before writing the implementation.
  2. Document what each field means and any constraints (e.g. max length, allowed values).
  3. Decide a versioning strategy up front (e.g. URL version, header version) — don’t wait until the first breaking change to decide.
  4. When a breaking change is needed, introduce a new version rather than mutating the existing contract; keep the old version live until consumers migrate.
  5. Communicate deprecation timelines to consumers and monitor usage of the old version before removing it.

The checkout team needs to add a required field to a public Order API already used by partners.

  • Input: a required taxBreakdown field needs to be added to the Order API response; three partner integrations currently consume v1 without it.
  • Process: rather than mutating v1’s response shape (which would break existing partner parsers), the team ships v2 with the new required field, documents the difference, and sets a 6-month deprecation window for v1.
  • Output: existing partners keep working unmodified on v1; new and updated integrations adopt v2; v1 is retired only once telemetry shows no remaining traffic.
[ApiController]
[Route("api/v{version:apiVersion}/orders")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class OrdersController : ControllerBase
{
[HttpGet("{id}")]
[MapToApiVersion("1.0")]
public ActionResult<OrderV1Response> GetV1(string id) => Ok(_orders.ToV1(id));
[HttpGet("{id}")]
[MapToApiVersion("2.0")]
public ActionResult<OrderV2Response> GetV2(string id) => Ok(_orders.ToV2(id)); // adds taxBreakdown
}

In front of the API, Azure API Management routes v1 and v2 to the same backend, applies the deprecation notice header, and reports per-version usage — the telemetry the team checks before retiring v1.

Use case When to use Notes
Exposing a new capability to another team Before that team starts integrating Define and document the contract before implementation, not after
Adding a required field Only via a new version Never make an existing required-field change on a live contract
Removing a deprecated endpoint After a communicated sunset date and confirmed zero usage Check telemetry, not assumptions, before removing
  • Breaking vs. non-breaking changes: adding an optional field is usually safe; adding a required field, renaming a field, or changing a type is breaking.
  • Contract-first design: writing the API schema (OpenAPI, GraphQL SDL, protobuf) before implementation catches shape problems before code is written around them.
  • Consumer-driven contracts: for high-stakes internal APIs, consumers can supply automated tests that verify the contract they depend on, catching accidental breaks in CI.
  • The Richardson Maturity Model names how RESTful an API actually is: Level 0 (a single endpoint, RPC-style over HTTP), Level 1 (separate resource URIs), Level 2 (proper HTTP verbs and status codes — where most “REST” APIs, including the Order API above, actually sit), and Level 3 (HATEOAS — responses include links to valid next actions). Most teams stop at Level 2 deliberately; Level 3 trades simplicity for discoverability that few consumers actually need.
Term Meaning
Contract The agreed request/response shape a consumer can rely on
Breaking change A contract change that would fail an existing consumer
Versioning Offering multiple contract versions simultaneously so consumers can migrate on their own schedule
Deprecation window The communicated period during which an old version stays live before removal
Richardson Maturity Model A 4-level scale (0-3) rating how RESTful an API is, from RPC-over-HTTP to full HATEOAS
Symptom Likely cause Fix
Partner integration broke after a deploy A required field or type changed on a live contract Revert, and introduce the change as a new version instead
Consumers are scared to upgrade No clear deprecation timeline or migration guide Publish a sunset date and a migration guide alongside the new version
Old version never gets retired No usage telemetry on the endpoint Instrument version usage before committing to a sunset date
  • Why does adding a required field count as a breaking change but adding an optional one usually doesn’t?
  • Sketch the v1→v2 migration timeline you’d propose for the Order API example.

Part of Introduction to Software & Application Architecture. See also Service & Component Boundaries, Event-Driven & Asynchronous Communication, and the Glossary.