100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
API Design & Best Practices
40 minintermediate

Capstone: Design and Document a Complete API

Every lesson in this course has asked you to make one judgment call at a time — pick a status code, name a field, choose a pagination style. The capstone asks something harder: hold all of those judgment calls in your head simultaneously, on one coherent domain, and produce a written design that a real engineering team could actually build from. There's no code to run at the end of this lesson and no automated grader — the deliverable is a design document, and the discipline this lesson is really testing is whether you can defend every decision in it with a reason, not just a pattern you remembered seeing somewhere in this course.

The domain for this capstone is a multi-venue event ticketing platform — think venues, events, ticket types, orders, and the partners and internal teams who all need to interact with it differently. This domain was chosen because it forces every tradeoff this course has covered to show up naturally: a public consumer-facing surface for people buying tickets, a partner surface for box-office integrations that need contractual stability, an internal surface for the operations team, a genuinely long-running operation (processing a large group order), a resource that needs idempotent creation (an order, where money changes hands), and a plausible AI-agent use case (a booking concierge that searches events and completes purchases on a user's behalf).

This lesson gives you three staged deliverables, each with explicit acceptance criteria, rather than one open-ended prompt to 'design a good API.' That's deliberate, and it mirrors the very first lesson of this course: 'good' is a bundle of tradeoffs, not one property, and a design brief that doesn't say which tradeoffs matter here produces exactly the kind of inconsistent, undirected design this course has spent its entirety warning against. Work through the three stages in order — each one builds on decisions made in the previous stage, the same way a real design review would catch you if Stage 2's error handling contradicted Stage 1's resource model.

Analogy🏏Cricket
🏏 Think of it like cricket: A young all-rounder's final trial before selection to a national academy isn't graded on any single skill in isolation — a batting session, a bowling spell, and a fielding drill run separately would each tell selectors something, but none of them would tell selectors whether the player can actually hold a match together end to end. The trial that actually matters is a full simulated match situation: bat under pressure in a chase, then bowl a defensive spell with the game on the line, then field under fatigue in the same session — because that's the only format that reveals whether skills the player has individually demonstrated actually integrate into one coherent performance when it counts. A player who bats brilliantly in isolation but has never had to immediately switch to a tight bowling spell straight afterward hasn't actually been tested on what selection is really assessing. Just as a full simulated match reveals whether a player's separate skills integrate under real pressure, this capstone reveals whether your separate lessons on resources, errors, versioning, and agent design integrate into one coherent, defensible document. Just as selectors give the trial explicit, staged components — bat, then bowl, then field — rather than one vague 'show us you're good' prompt, this capstone gives you three staged deliverables with explicit criteria rather than one vague design prompt. The insight is that genuine mastery isn't demonstrated by excelling at isolated skills — it's demonstrated by holding all of them together, coherently, under the compressed pressure of one continuous piece of work, which is exactly what a real API design has to survive on the day an actual team starts building from it.

The Brief: What You're Building

Stage 1: Resource Model and Paradigm Choice

Produce a resource map for the platform: identify at least six core resources — plausible choices include `Venue`, `Event`, `TicketType`, `Order`, `Payment`, and `Attendee` — and state each resource's ownership relationship explicitly, either as a root-level resource with a stated reason it doesn't belong under anything else, or as a subresource nested under its logical parent, the way this course's lesson on resources and URL structure taught. From this map, write out the URL structure for at least ten endpoints that together cover standard CRUD operations plus at least two actions that don't map cleanly onto a CRUD verb — refunding an order or checking a venue's live seat availability are two plausible candidates — and state explicitly why each of those two used a sub-resource or action-oriented path instead of forcing it into a `PATCH`.

Choose a primary API paradigm — REST, GraphQL, gRPC, or an explicit hybrid — and justify the choice in two to three sentences grounded in this platform's actual consumers, not in the abstract. A justification that says 'REST is simpler' without naming which consumer's actual access pattern that simplicity serves is not sufficient; a justification that says 'the partner box-office integration needs simple, cacheable, resource-oriented reads it can retry safely, and GraphQL's flexible querying isn't worth the added complexity for a consumer that mostly needs the same five fields on every call' is the level of specificity this stage requires.

Acceptance criteria for Stage 1: every resource in your map has a stated owning parent, or a stated reason it's root-level; naming is consistent across every endpoint (plural nouns, one consistent casing convention); the ten-endpoint list includes at least two non-CRUD actions, each with a one-sentence justification for its path shape; and your paradigm choice is justified against a named consumer's actual need, not asserted as a general preference.

Analogy🏏Cricket
🏏 Think of it like cricket: A ground being newly built has to have its layout decided before a single brick is laid — where the pavilion sits relative to the pitch, how the practice nets relate to the main ground, where the media center connects to both — because a layout decided piecemeal, structure by structure, as construction progresses tends to produce a ground where getting from the dressing room to the pitch takes an inexplicable detour nobody planned on purpose. An architect who draws the full site plan first, naming every structure's relationship to every other structure before construction starts, can catch that the media center's proposed location would block the sightline from the members' pavilion, while it's still a line on paper and free to redraw — a mistake that would cost a fortune to fix once the concrete's poured. Just as a ground's site plan names every structure's relationship to every other structure before construction starts, a resource map names every resource's relationship to every other resource before a single endpoint gets built. Just as catching a sightline conflict on paper is nearly free while catching it after construction is enormously expensive, catching an inconsistent resource-nesting decision in a design document is nearly free while catching it after client integrations exist is enormously expensive. The insight is that the value of drawing the full picture first isn't aesthetic — it's that mistakes are catastrophically cheaper to fix while they're still just a line that can be redrawn than after something real has been built on top of them.

Stage 2: Contract, Errors, and Versioning

Fully specify the request and response shape for three endpoints from your Stage 1 map: the list-events endpoint (must demonstrate a pagination strategy, with a stated reason for the choice), the create-order endpoint (must demonstrate an idempotency-key strategy, since this is the one operation in the platform where a duplicate write means a customer is charged twice), and one more endpoint of your choosing. Write these as simplified JSON schema fragments — field names, types, and a one-line note on any non-obvious constraint — rather than full OpenAPI YAML; the goal is demonstrating the judgment, not the tooling syntax.

Define a single error envelope shape used consistently across all three endpoints, and write an error taxonomy table listing at least six distinct error codes, each with a one-line description and an explicit retryable flag — exactly the structured, agent-actionable shape this course's tool-use lesson argued every API should have regardless of whether an agent is actually calling it. Then choose and justify a versioning strategy for the platform as a whole, and state explicitly what deprecation notice period you'd commit to for a breaking change at each of the internal, partner, and public tiers, applying the audience-tiering framework from earlier in this course.

Acceptance criteria for Stage 2: the error envelope's field names and shape are identical across all three endpoints, with no ad hoc per-endpoint variation; the idempotency-key strategy explicitly states what happens on a retry with the same key and an identical body, and what happens on a retry with the same key but a different body, since leaving that second case unspecified is the single most common gap in a written idempotency design; and your versioning strategy names a specific, different notice period for each of the three audience tiers, not one blanket number applied to all three.

Analogy🏏Cricket
🏏 Think of it like cricket: A stadium's emergency evacuation plan is only as good as its handling of the ambiguous cases, not the obvious ones — everyone already knows what to do if there's a clear, immediate danger; the plan's real value is in specifying exactly what happens for the messier situations, like a partial power outage in one stand only, or a medical emergency in a section during a tense final over. A plan that only covers the clean, obvious scenario and stays silent on the messy edge cases forces stewards to improvise exactly when improvisation is most dangerous — during the actual event, under real pressure, with thousands of people watching their reaction for a cue. A genuinely complete plan writes down the ambiguous cases specifically because they're the ones an improvised response is most likely to get wrong. Just as a stadium's evacuation plan earns its value by specifying the ambiguous cases, an idempotency-key design earns its value by specifying the ambiguous case — a retried key with a changed body — not just the clean case of an identical retry. Just as stewards forced to improvise during a real emergency are working under exactly the wrong conditions to think clearly, a payments system left to improvise how to handle a retried key with mismatched data is being forced to guess at exactly the moment — real money, real customer, real production traffic — where a guess is most costly. The insight is that a design's real test isn't whether it handles the case everyone already agreed on — it's whether it specifies the ambiguous case in advance, precisely because that's the case a system under real pressure has no time to reason out from scratch.
python
# stage2_error_envelope.py -- a runnable sketch of the consistent envelope Stage 2 requires
ERROR_TAXONOMY = {
    "VALIDATION_FAILED":      {"retryable": False, "desc": "one or more fields failed validation"},
    "RESOURCE_NOT_FOUND":     {"retryable": False, "desc": "the referenced resource does not exist"},
    "IDEMPOTENCY_CONFLICT":   {"retryable": False, "desc": "same idempotency key reused with a different request body"},
    "RATE_LIMITED":           {"retryable": True,  "desc": "too many requests -- retry after backoff"},
    "SEAT_UNAVAILABLE":       {"retryable": False, "desc": "requested seats were reserved by another order first"},
    "UPSTREAM_TIMEOUT":       {"retryable": True,  "desc": "a dependency timed out -- safe to retry with the same idempotency key"},
}

def build_error(code: str, field: str = None) -> dict:
    """One envelope shape, reused by every endpoint in the platform --
    the Stage 2 requirement this function exists to demonstrate."""
    meta = ERROR_TAXONOMY[code]
    return {"code": code, "field": field, "detail": meta["desc"], "retryable": meta["retryable"]}

for code in ("VALIDATION_FAILED", "IDEMPOTENCY_CONFLICT", "RATE_LIMITED"):
    print(build_error(code, field="ticket_type_id" if code == "VALIDATION_FAILED" else None))

retryable_count = sum(1 for m in ERROR_TAXONOMY.values() if m["retryable"])
print(f"\n{retryable_count} of {len(ERROR_TAXONOMY)} error codes are retryable -- "
      f"an agent or client can act on this flag without parsing the detail text.")

Stage 3: Documentation and Client Experience

Write a short getting-started documentation excerpt, aimed at a new partner developer, for one endpoint of your choosing — it must state the authentication method, any applicable rate limit, and a complete example request and response, in the self-describing style this course's lesson on documentation argued readers actually use. Then design a tool schema, in the JSON-Schema-shaped format this course's agent-design lesson covered, exposing one capability of this platform to an AI booking concierge — `search_events` or `book_ticket` are plausible choices — with every parameter typed precisely, every non-obvious constraint stated in the description, and a structured, retryable-aware error shape reusing the taxonomy from Stage 2.

Finally, classify every one of the ten endpoints from your Stage 1 map as internal, partner, or public, applying the audience-tiering framework from earlier in this course, and identify at least one endpoint that would need different response shaping depending on which tier is calling it — a good candidate is an order-detail endpoint, where an internal caller might see a fraud-risk score, a partner sees carrier and fulfillment status, and a public caller sees only their own order's status and total. State explicitly which fields that endpoint would show or hide per tier.

Acceptance criteria for Stage 3: the documentation excerpt includes a complete, concrete example request and response, not a schema alone; the agent tool schema uses a closed enum for any parameter with a genuinely closed value space, and states at least one cross-field constraint explicitly in its description; the audience classification names a tier for every one of the ten Stage 1 endpoints, with no endpoint left unclassified; and the identified multi-tier endpoint lists specific fields shown or hidden per tier, not a vague statement that the response 'differs.'

Analogy🏏Cricket
🏏 Think of it like cricket: A franchise's media guide, written for three different readers under one cover, doesn't hand every reader the same page and expect it to work for all of them — the section for a broadcaster's commentary team is dense with tactical detail and historical head-to-head numbers they'll reference live on air, the section for a casual matchday visitor is a simple seating and gate-times guide, and the section for the team's own new signings is an internal onboarding document with facility access codes that never appear anywhere in the public-facing print run at all. One underlying set of facts about the franchise — where things are, how things work — gets deliberately shaped into three different documents because handing the commentary team's tactical depth to a first-time matchday visitor would be useless clutter, and handing facility access codes to the public print run would be an actual security failure. Just as one media guide's underlying facts get shaped into three different documents for three different readers, one platform's underlying data gets shaped into three different response shapes for the internal, partner, and public tiers. Just as the media guide's authors decided, deliberately, what each reader actually needs rather than defaulting to giving everyone everything, this capstone's Stage 3 asks you to decide, deliberately, what each tier actually needs rather than defaulting to one shared response shape. The insight is that documentation and response-shaping are the same underlying discipline applied to two different artifacts — both are about deliberately matching what's shown to who's actually receiving it, not about how much detail you're technically capable of including.
python
# stage3_agent_tool_schema.py -- a runnable sketch of the tool schema Stage 3 requires
BOOK_TICKET_TOOL_SCHEMA = {
    "name": "book_ticket",
    "description": (
        "Books tickets for a single event on behalf of the user. quantity must not exceed "
        "the event's remaining_capacity at call time, or the call fails with SEAT_UNAVAILABLE. "
        "This action charges the user's saved payment method immediately."
    ),
    "parameters": {
        "event_id": {"type": "string", "description": "the event's unique identifier"},
        "ticket_type": {"type": "string", "enum": ["general", "premium", "vip"]},
        "quantity": {"type": "integer", "minimum": 1, "maximum": 8},
        "idempotency_key": {"type": "string", "description": "required; reused on retry to avoid a duplicate booking"},
    },
}

def validate_call(event_remaining_capacity: int, quantity: int, ticket_type: str) -> dict:
    """What a well-built calling layer checks BEFORE this ever reaches the platform --
    the closed enum and the stated cross-field constraint make this possible."""
    allowed_types = BOOK_TICKET_TOOL_SCHEMA["parameters"]["ticket_type"]["enum"]
    if ticket_type not in allowed_types:
        return {"code": "VALIDATION_FAILED", "field": "ticket_type", "retryable": False}
    if quantity > event_remaining_capacity:
        return {"code": "SEAT_UNAVAILABLE", "field": "quantity", "retryable": False}
    return {"code": None, "result": "call is valid, proceed"}

print(validate_call(event_remaining_capacity=3, quantity=2, ticket_type="vip"))
print(validate_call(event_remaining_capacity=3, quantity=5, ticket_type="vip"))
print(validate_call(event_remaining_capacity=3, quantity=2, ticket_type="platinum"))

How Your Submission Will Be Evaluated

Before treating any stage as finished, run your own design against the acceptance criteria listed for that stage, line by line, the same way this course's lesson on contract testing argued a design should be checked against a written contract rather than a gut feeling of 'looks done.' A design is not complete because it feels thorough — it's complete because every stated criterion has a specific, checkable answer in the document, and 'the error handling seems reasonable' is not the same claim as 'the error envelope is identical across all three specified endpoints,' which is the actual, checkable criterion Stage 2 asks for.

Measurable, specific evaluation criteria matter here for the same reason they matter in the API you're designing: a vague acceptance bar produces inconsistent results depending on who's judging it, exactly like a vague API contract produces inconsistent behavior depending on which engineer happened to implement a given endpoint. Every acceptance criterion in this lesson was written to be checkable by reading your document and answering yes or no, not by forming a subjective impression — and that's the standard your own design document should be held to internally as well, since the whole point of this capstone is practicing the judgment a real design review would apply to your work.

python
# self_review_checklist.py -- turning each stage's acceptance criteria into a checkable pass/fail
def review_stage1(resource_map: dict, endpoints: list, paradigm_justification: str) -> list[str]:
    issues = []
    for resource, owner in resource_map.items():
        # "ROOT: <reason>" is an explicitly stated root-level decision -- only a bare
        # None (never actually decided) is a real gap in the map.
        if owner is None:
            issues.append(f"resource '{resource}' has no stated owner and no stated reason to be root-level")
    non_crud = [e for e in endpoints if e.get("is_action")]
    if len(non_crud) < 2:
        issues.append(f"only {len(non_crud)} non-CRUD action endpoints -- need at least 2")
    if len(paradigm_justification.split()) < 15:
        issues.append("paradigm justification too thin -- must name a specific consumer's actual need")
    return issues

sample_map = {
    "Venue": "ROOT: independent entity, not owned by any single event",
    "Event": "Venue",
    "Order": "ROOT: spans multiple events via its line items",
    "Payment": "Order",
}
sample_endpoints = [
    {"path": "/orders/{id}/refund", "is_action": True},
    {"path": "/events", "is_action": False},
]
sample_justification = "REST fits the partner integration's simple, cacheable, resource-oriented needs."

for issue in review_stage1(sample_map, sample_endpoints, sample_justification):
    print("FLAG:", issue)
print("review complete -- fix every FLAG before moving to Stage 2")
Analogy🏏Cricket
🏏 Think of it like cricket: A fast bowler's own coach doesn't evaluate a training session by asking whether the session 'felt fast' — the coach checks specific, measurable things: release point consistency across deliveries, seam position on the ball, follow-through completing rather than pulling up short, each one checkable frame by frame on video rather than judged by a general impression of effort. A bowler who trusts only the subjective feeling of a session going well can walk away from a session with a technical flaw creeping in that felt completely fine in the moment, because 'felt fast' and 'was mechanically sound' are two different claims that don't always agree, and only the specific, checkable measures catch the gap between them. Just as a bowler's coach checks specific, measurable technical criteria rather than a general feeling of the session going well, your own review of this capstone should check the specific, measurable acceptance criteria rather than a general feeling that the design is thorough. Just as catching a technical flaw on video, while it's still just a training session, is far cheaper than discovering it costing wickets in a real match, catching a gap against a written acceptance criterion while it's still a design document is far cheaper than discovering it once a real team has started building from it. The insight is that specific, checkable criteria exist precisely because a general feeling of confidence and an actual verified fact are two different things, and the gap between them is exactly where undetected problems live.

Common Pitfalls to Avoid

The most common Stage 1 failure is modeling resources around a plausible internal database schema instead of around what a consumer actually needs to accomplish — designing a `TicketInventoryRow` resource because that's how the data would naturally live in a table, rather than a `TicketType` resource shaped around what a buyer or a partner integration actually needs to request. This is the exact failure this course's second lesson warned about: an API modeled around internal structure instead of consumer intent leaks implementation detail into a contract that then can't evolve independently of that internal structure.

The most common Stage 2 failure is leaving the idempotency-key conflict case unspecified — stating that retries with the same key are handled, without stating what happens if the retried request's body doesn't match the original. Left unspecified, a real implementation either silently returns the original result regardless of what changed, masking a genuine bug in the caller, or silently processes the new body as if it were the original request, defeating the entire point of the key. The most common Stage 3 failure is writing an agent tool schema exactly like a human-facing docstring — free-text parameters, constraints mentioned in prose a model has to parse loosely rather than expressed as an enum or a validation rule the calling layer can enforce structurally before your platform ever sees a bad call.

Analogy🏏Cricket
🏏 Think of it like cricket: A young wicketkeeper's most common early mistake isn't dropping catches — it's standing in a position that was comfortable for the previous bowler's pace and forgetting to adjust for a change in bowler, so a keeper set up correctly for a fast bowler's carry gets caught flat-footed the first over a slower bowler comes on and the ball arrives with completely different timing. The mistake isn't a lack of skill at keeping itself — it's applying yesterday's correct setup to today's different situation without re-checking whether the assumption still holds. A keeper who's been coached to explicitly re-check their position at every bowling change, rather than trusting whatever felt right last over, catches this kind of drift before it costs a chance. Just as a keeper's positioning has to be re-checked at every bowling change rather than carried over from the last bowler by habit, a resource model has to be re-checked against actual consumer need rather than carried over from whatever the database schema happens to look like. Just as the keeper's fix isn't more raw catching practice but a specific habit of re-checking an assumption at the right moment, the fix for these capstone pitfalls isn't writing more content but specifically re-checking each stage's acceptance criteria at the right moment, before moving to the next stage. The insight is that the most common mistakes in disciplined work aren't usually failures of raw skill — they're failures to re-verify an assumption that was valid somewhere else but was never actually checked against the situation actually in front of you.

A specific trap worth calling out directly: it's tempting to treat Stage 3's agent tool schema as an afterthought layered on top of a REST API you've already fully designed, reusing the REST endpoint's existing parameter names and error shapes without re-checking whether they actually meet an agent's stricter needs. The symptom is a tool schema with a free-text `status` parameter because that's what the REST endpoint used, even though the actual value space is five closed options. The root cause is designing for a human reader first and assuming 'good enough for a person' transfers automatically to 'good enough for an agent.' The fix, as this course's lesson on agent design argued, is re-checking every parameter against the agent-specific bar directly — closed enums, explicit cross-field constraints, structured retryable errors — rather than assuming the REST design already cleared it.

Submission Checklist and What Good Looks Like

A finished submission is a single design document containing, in order: your Stage 1 resource map and ten-endpoint list with paradigm justification; your Stage 2 three-endpoint schema fragments, error taxonomy table, idempotency-conflict specification, and per-tier versioning commitment; and your Stage 3 documentation excerpt, agent tool schema, and full ten-endpoint audience classification with the one multi-tier endpoint's field-level breakdown. Nothing here requires a working server or a deployed API — every deliverable is checkable by reading the document itself against the acceptance criteria already stated for each stage.

What separates a genuinely strong submission from an adequate one isn't extra length or extra endpoints beyond what was asked for — it's whether every decision in the document is defensible on the specific grounds this course has spent thirty-four lessons building: named consumer needs driving the resource model, a consistent contract instead of ad hoc per-endpoint choices, and an explicit accounting of who each part of the API actually serves. A submission that hits every acceptance criterion technically but can't explain why any particular choice was made over the alternative has completed the exercise without absorbing the judgment the exercise exists to build — and that judgment, not the specific ticketing-platform design itself, is the actual deliverable this capstone is checking for.

Analogy🏏Cricket
🏏 Think of it like cricket: A team's post-tour report to the board isn't judged well just because it covers every fixture and includes every statistic anyone could want — a report that lists every number with no analysis of why a particular batting order worked against spin but not pace, or why a specific bowling change succeeded in one match and failed in a near-identical situation two matches later, has technically covered everything and explained nothing. The report the board actually values is the one that shows the team management understood why each decision worked or didn't, because that understanding is what transfers to the next tour, while a list of results without reasoning doesn't transfer anywhere — it just documents what already happened. Just as a post-tour report's value is in the reasoning behind the decisions, not just a record that they were made, this capstone's value is in the reasoning behind your API decisions, not just a document confirming they were made. Just as a team that understands why a decision worked can apply that understanding to a completely different tour, an engineer who understands why a resource model or an error taxonomy was designed a certain way can apply that understanding to a completely different API, on a completely different domain, for the rest of their career. The insight is that the specific artifact — this report, this ticketing platform — was never really the point; it's the vehicle for building a transferable judgment that outlasts the specific artifact entirely.
  • A capstone with staged deliverables and explicit, checkable acceptance criteria produces a more defensible design than one open-ended prompt, for the same reason a clear API contract produces more consistent behavior than a vague one.
  • Resources must be modeled around what a consumer actually needs to accomplish, not around a plausible internal database schema, or the resulting contract leaks implementation detail it can never cleanly remove later.
  • An idempotency-key design is incomplete until it explicitly specifies the conflict case -- a retried key with a changed request body -- since that ambiguous case, left unspecified, is where a real implementation is most likely to fail silently.
  • A single, consistent error envelope reused across every endpoint, carrying a stable code and an explicit retryable flag, is what makes an API's error handling genuinely usable by both human developers and AI agents.
  • Every endpoint in a platform needs an explicit audience classification -- internal, partner, or public -- because an unclassified endpoint tends to default to whichever tier's assumptions the original implementer happened to have in mind.
  • An agent-facing tool schema needs its own explicit pass for closed enums, stated cross-field constraints, and structured errors -- reusing a human-facing REST design's shape unexamined is the most common gap between a working API and a genuinely agent-ready one.
  • The actual deliverable of this capstone is transferable judgment about tradeoffs, not the specific ticketing-platform design -- a submission that satisfies every criterion without being able to explain why has completed the exercise without absorbing what it was built to teach.
Lesson 35 of 35
0% complete