Learning pathsA
GUIDED PRACTICE

API Design

An API is a promise between independently failing programs.

An API is a promise between independently failing programs. Its path names are less important than what a successful response means, which errors can be retried, and how callers discover the result after a timeout. Design the contract before distributing implementation work. A precise contract makes both the happy path and the uncertain path understandable.

Learning goals

Resources and commands; request/response contracts; authentication versus authorization; validation; cursor pagination; idempotency scope; retryable errors; versioning.

The mechanism at a glance

Caller → Authenticate (request); Authenticate → Authorize object (principal); Authorize object → Validate payload (allowed); Validate payload → Atomic create (key + fingerprint); Atomic create → Status resource (resource ID)
Scroll to inspect the diagram, or open it at full size.

Figure — Caller → Authenticate (request); Authenticate → Authorize object (principal); Authorize object → Validate payload (allowed); Validate payload → Atomic create (key + fingerprint); Atomic create → Status resource (resource ID)

The numbered components identify responsibilities. Follow the labeled arrows rather than treating the numbers as a global execution order. The scenario later in this lesson shows one concrete sequence.

Step-by-step reasoning

1. Define resources and scope

For a notice service, POST /notices creates an intent and GET /notices/{id} reads its status. The authenticated principal supplies the tenant context; do not trust a tenant ID in the body as proof of access. Authentication identifies the caller. Authorization checks whether that caller may perform this action on this particular notice, including every read and download.

2. Make creation retryable

Accept an idempotency key scoped to tenant and operation. Store a request fingerprint and the resulting resource ID atomically with creation. A repeated key with the same payload returns the original outcome; a changed payload conflicts. Define retention explicitly: an expired deduplication record cannot protect retries forever. Concurrent requests using the same key must meet a unique constraint or equivalent atomic decision.

3. Return honest states and errors

A 202 response should mean the job was durably accepted, not that it was delivered. Provide a status resource for queued, processing, completed, and failed outcomes. Distinguish malformed input, denied access, state conflicts, throttling, and temporary dependency failure. Do not automatically retry every 4xx or every non-idempotent request. Document rate-limit scope and retry guidance.

4. Paginate and evolve

Use a stable ordering with a tie-breaker, such as created_at and id. A cursor encodes the position plus relevant filter or snapshot context; validate it and avoid exposing sensitive internals. Offset pagination can skip or duplicate entries when rows shift. Add optional fields compatibly, tolerate unknown response fields, and version semantics when their meaning changes rather than renaming paths for every addition.

Contracts and state

The following sketch makes the decision boundary concrete. Field names and capacity assumptions are illustrative; adapt them to the stated product contract.

Contract / pseudocode
POST /notices
Idempotency-Key: school42-announcement7
{ "audienceId": "grade8", "templateVersion": 3 }

202 Accepted
{ "id": "n_92", "status": "queued" }

GET /notices/n_92/deliveries?after=<opaque-cursor>

Worked example

Two requests arrive with the same creation key before either sees a saved result. A preliminary lookup is insufficient: both may observe “missing.” Put the idempotency record and notice creation in one transaction with a unique key on tenant, operation, and key. One request wins. The other waits or receives an in-progress response and later resolves to the same notice. If the API publishes a queue message too, close the database-to-broker gap with an outbox.

Failure walkthrough

Authorization is not inherited from a list page. A caller can change an ID manually and ask for another tenant’s notice. Recheck object ownership on the read endpoint. Similarly, a cursor from one tenant must not be usable to cross tenant boundaries. Log denials without recording message bodies or secret tokens; diagnostics should not create a second data leak.

Request key K arrives → Create notice + key atomically → Response times out → Retry K with same body → Return original notice ID
Scroll to inspect the diagram, or open it at full size.

Figure — Request key K arrives → Create notice + key atomically → Response times out → Retry K with same body → Return original notice ID

Decisions and trade-offs

ContractDecisionWhy
CreationDurable acceptanceSeparates API latency from delivery time
Duplicate keyReturn original resultProtects ambiguous retries
Changed payloadConflictPrevents accidental reuse of an old identity
PaginationStable cursorBounds scans and describes continuation

Check your understanding

A client retries a notice with the same key but a changed audience. Should the service overwrite the first notice?

Show answer and explanation

Answer: No. Reject the mismatched fingerprint and require a new key for new intent. Silently overwriting changes the meaning of an operation the first caller may already believe was accepted.

Transfer to a new scenario

Create a notice with an idempotency key, reject a changed payload using the same key, and paginate deliveries.

A caller knows another tenant's notice ID. Which authorization check prevents access?

Continue the connection

Study Data Modeling and explain which guarantee from this lesson carries into that topic.

Design a retryable resource contract

Consider creating an export job. The first POST authenticates the caller, validates tenant ownership and stores both the job and its idempotency key in one transaction. A retry with the same key and fingerprint returns the same job ID. A different body under that key returns a conflict. A 202 response means durable acceptance under the stated contract, not completed export. Expose queued, running and terminal states through a separate resource.

For pagination, use a stable ordering such as (created_at,id). Encode the last pair in an opaque cursor and keep the tenant and filter context bound to it. Offset pagination gets expensive at large offsets and can skip or repeat rows as new data arrives. An opaque cursor is not an authorization token: every page still needs access checks.

Version the contract when meaning changes. Adding an optional field is often compatible; reinterpreting an existing enum value may not be. Publish error semantics, retry hints, rate limits and maximum request size. Use conditional updates with a version or ETag to avoid overwriting another editor’s change.

A decision worksheet for API Design: read the mechanism and its guarantee together.
Scroll to inspect the diagram, or open it at full size.

Figure — A decision worksheet for API Design: read the mechanism and its guarantee together.

Operational sketch

Contract / pseudocode
POST /exports
Idempotency-Key: tenant42-export17
{format:"csv", filter:{schoolId:"s1"}}
202 {id:"e17",state:"queued",statusUrl:"/exports/e17"}
PATCH /exports/e17 with expectedVersion=3

A tempting mistake

A successful HTTP status alone does not define durability. Likewise, GET should not trigger an irreversible business mutation merely because it is convenient. Distinguish malformed input, forbidden access, state conflict, quota rejection and temporary service failure so clients can act safely.

Transfer exercise

A client reuses an idempotency key with a different export filter. Should you return the old result?

Show answer and explanation

Answer: No. Compare the stored request fingerprint and reject the conflicting reuse. Otherwise the caller may believe the returned export matches the new request.

8:00Self-guided practice timer
The timer resets when you leave this page. Save your design separately.
Your challenge

A client retries a notice with the same key but a changed audience. Should the service overwrite the first notice?

Your design draft

Clarify assumptions, explain your approach, and test the difficult cases. Save your draft, then compare it with the study notes.

Read study notes

Self-review checklist

Self-guided practice. Automated AI feedback and code execution are not connected.