REST APIs in Depth · Capstone

Capstone: Design an Orders API

Design a production-grade REST API for e-commerce orders.

Scenario

An online store is replacing its legacy order endpoints. Web, mobile and partner (B2B) clients must create orders, list a customer's orders, cancel orders before shipping, and download invoices. Partners import orders in bulk. Clients retry aggressively on flaky mobile networks.

Requirements

  • Create, read, list, cancel orders; download invoice PDFs
  • Bulk import for partners (up to 10,000 orders per file)
  • Safe retries for order creation
  • Concurrent edits (support staff and customer) must not lose updates
  • Consistent errors and pagination across endpoints

Constraints

  • p95 < 300 ms for reads
  • Partners rate-limited per API key
  • Order IDs must not be enumerable
  • Mobile clients on older app versions for 12+ months

Deliverables

  1. List the resources, URLs and methods for the Orders API.
  2. How do you make order creation safe to retry, and how do you prevent lost updates?
  3. Define the error format and three concrete error examples.

Hints

  • Separate the interactive API from the bulk import path.
  • Think about what mobile apps released a year ago will still send.

Draft each deliverable below before revealing the model answer and rubric.

Further reading: Google API Improvement Proposals

Deliverables

List the resources, URLs and methods for the Orders API.

Model answer

  • POST /v1/orders (Idempotency-Key required) → 201 + Location
  • GET /v1/orders/{order_id} → 200 + ETag; ?expand=customer,items.product
  • GET /v1/orders?customer_id=&status=paid,shipped&created_after=&sort=-created_at&limit=&cursor=
  • PATCH /v1/orders/{order_id} (If-Match required; shipping address/notes only)
  • POST /v1/orders/{order_id}:cancel → 200 or 409 if already shipped
  • GET /v1/orders/{order_id}/invoice → application/pdf (supports Range), or 302 to a signed URL
  • POST /v1/order-imports (upload file or pre-signed upload) → 202 + /v1/operations/{id}
  • IDs: opaque prefixed ULIDs (ord_01J…)

Rubric

Strong answers: noun-based resources with shallow nesting; explicit cancel action instead of GET; cursor pagination with allow-listed filters; async import job; invoice via signed URL. Common misses: no Idempotency-Key on create, offset pagination for a large table, sequential IDs, cancel modeled as DELETE (loses audit/refund semantics).

How do you make order creation safe to retry, and how do you prevent lost updates?

Model answer

  • Require Idempotency-Key on POST; store (client, key) → request fingerprint + response; replay the stored 201 on retries, 409 while in progress, 422 if the same key is reused with a different body; expire keys after 24–72 h.
  • Pass the same key to the payment provider.
  • Return ETag (version) on GET; require If-Match on PATCH; 412 on mismatch, 428 when missing.

Rubric

Look for: key scoping per client, fingerprinting, behavior for concurrent duplicates, expiry, downstream key propagation, ETag/If-Match with 412/428. Misses: relying on 'check if exists' races, last-write-wins updates.

Define the error format and three concrete error examples.

Model answer

application/problem+json (RFC 9457) with stable type URIs and request_id:

  • 422 validation: type=/problems/validation, errors[] with pointer, code, detail
  • 409 conflict: type=/problems/order-not-cancellable, detail="Order ord_1 already shipped"
  • 429: type=/problems/rate-limited, headers Retry-After, RateLimit / RateLimit-Policy

Rubric

Strong answers use one format everywhere, machine-readable codes, and correct status codes. Misses: 200-with-error bodies, leaking stack traces, clients forced to parse messages.

Open in the app to mark lessons complete, take notes and track your progress.