We closed module 1 by saying it was time to move from understanding to building. Before writing the first line of server code there is a stage that far too many people skip, and it is exactly where API projects are won or lost: designing the contract. This is the umbrella lesson of module 2. It does not yet cover how URIs are named or which status code to return —those are the lessons that follow— but rather the working method: how you decide which API needs building, with which guiding principles and with which artefacts. By the end of the lesson you will be holding the Aroma Store API style guide, a living document that the next seven lessons will fill in and that module 3 will implement to the letter.

Contents

  1. API-first versus code-first
  2. The design process in seven steps
  3. Step 1: identify consumers and use cases
  4. Step 2: extract the nouns of the domain
  5. Step 3: decide what becomes a resource and what does not
  6. Guiding design principles
  7. Granularity: neither too fine nor too coarse
  8. Design for the consumer, not for the database
  9. Tolerance to change and the robustness principle
  10. The Aroma Store API style guide
  11. Map of module 2

  1. API-first versus code-first

There are two ways of ending up with an API, and they do not produce the same result.

In the code-first approach you write the application first and the API appears afterwards, almost as a by-product: you take the services that already exist, hang an HTTP layer off them and generate the documentation from the code. It is fast at the start and works reasonably well when the only consumer is your own frontend.

In the API-first approach you do the opposite: the contract is designed, reviewed and agreed before anything is implemented. The specification is the primary artefact; the code is its realisation. Consumers can start working against a mock generated from the contract while the server team implements it.

Criterion Code-first API-first
Starting point The existing code The agreed contract
Who decides the shape of the API The implementation and the ORM The consumers and the domain
When clients can start When there is a working server Day one, against a mock
Cost of a design change High: code has been written Low: you edit a document
Risk of leaking internal details High Low
Documentation Generated at the end, always trailing It is the source, always up to date
Good for Prototypes, an internal API with a single client APIs with several consumers or public ones

Aroma Store has four different consumers and one of them is an external company. Redesigning the contract after SwiftShip has integrated its systems costs meetings, versions and money. That is why this course adopts API-first: the whole of module 2 designs the contract on paper and module 3 implements it.

An honest caveat: API-first does not mean "design everything perfectly before touching code". It means that the contract goes first and gets reviewed the way code gets reviewed. You can iterate, but you iterate on the document, not on an API that is already published.

  1. The design process in seven steps

graph TD
    A["1. Identify consumers<br/>and use cases"] --> B["2. Extract the nouns<br/>of the domain"]
    B --> C["3. Decide what is a resource<br/>and what is not"]
    C --> D["4. Name URIs and<br/>define hierarchies"]
    D --> E["5. Assign methods, codes<br/>and representations"]
    E --> F["6. Write the contract<br/>(OpenAPI) and review it"]
    F --> G["7. Publish mocks and<br/>validate with consumers"]
    G -.->|"findings"| A

Steps 1 to 3 are this lesson. Steps 4 and 5 are lessons 02-02 to 02-06. Step 6 lands in 02-07 and 02-08. Step 7 is worked through in depth in 05-04. The cycle closes: what you learn validating with consumers goes back to the beginning.

  1. Step 1: identify consumers and use cases

An API is not designed "for the domain": it is designed for someone who is going to call it. The first deliverable is not a list of endpoints but a list of consumers with their real needs.

Consumer Who it is What it needs Constraints
Web shop SPA Application in the browser Catalogue, coffee detail page, cart, checkout Browser: CORS, visible latency, no secrets
Aroma Mobile Native iOS/Android app The same, on small screens Variable mobile networks, old versions coexisting for months
Internal panel Back-office tool Stock management, review moderation, orders Large volumes, listings with filters, near real time
SwiftShip Courier partner Receive paid orders, report shipments External: stable contract, retries, HMAC signature

Out of that come the use cases, written as user sentences rather than endpoints:

  • "As a visitor I want to see the available coffees filtered by origin and roast."
  • "As a customer I want to add two bags of Ethiopia Yirgacheffe to my cart and pay for it."
  • "As a customer I want to check the status of my order and download the invoice."
  • "As a moderator I want to approve or reject a pending review."
  • "As SwiftShip I want to find out that an order has been paid without having to ask every minute."

That last use case is the one that made webhooks appear in 01-07: the list of use cases also decides the architecture, not just the endpoints.

One important detail: consumers have different and sometimes conflicting needs. Aroma Mobile wants small responses because it pays for the network; the internal panel wants rich responses because it paints tables with many columns. That tension is not resolved by creating two parallel APIs but with contract mechanisms: sparse fieldsets and expansion, designed in 02-05.

  1. Step 2: extract the nouns of the domain

The technique is deliberately simple: write in prose what the business does and underline the nouns.

"A customer browses the catalogue of coffees, each one with its origin, its roast and its tasting notes. They add items to their cart and confirm an order, which has a total and a status. The order is paid and generates an invoice. When it is paid, SwiftShip creates a shipment. Afterwards, the customer can write a review of a coffee, which a moderator approves or rejects, and to which the shop can publish a reply."

Candidate nouns: customer, catalogue, coffee, origin, roast, tasting notes, item, cart, order, total, status, payment, invoice, shipment, review, reply, moderator.

And the verbs, which we note down separately because they are the source of the most interesting problems: browse, add, confirm, pay, generate, approve, reject, reply.

  1. Step 3: decide what becomes a resource and what does not

Not every noun deserves a URI. Apply these filters:

  1. Does it have its own identity? Can you point at it and say "that one there"? An order does (ord_5001); a total does not: it is an attribute of an order.
  2. Does anyone need to address it separately? A review does: it is moderated one at a time. A roast does not: it is a value of an enumeration.
  3. Does it have its own lifecycle? A shipment is born, changes state and ends. A tasting note does not: it lives and dies with its coffee.
  4. Is it manipulated independently of its parent? A cart item is, because you change its quantity without touching the rest.

Applied to Aroma Store:

Noun Resource? Decision
Coffee Yes Collection /coffees
Customer Yes Collection /customers
Order Yes Collection /orders
Review Yes Collection /reviews, also nested under its coffee
Cart Yes Collection /carts
Cart item Yes, sub-resource /carts/{id}/items/{coffeeId}
Payment Yes, sub-resource /orders/{id}/payment
Invoice Yes, sub-resource /orders/{id}/invoice
Shipment Yes, sub-resource /orders/{id}/shipment
Catalogue No It is the /coffees collection, not a separate resource
Origin, roast, tasting notes No Attributes of a coffee
Total, status No Attributes of an order
Moderator Not in v1 It is a user role, not a public resource

The right-hand column is justified in 02-02, which is where naming rules, nesting and singletons are explained. What matters here is the criterion: a resource is something that has identity, a lifecycle and a need to be addressed.

  1. Guiding design principles

These six principles are the ones we will apply, lesson after lesson, every time something has to be decided.

6.1. Consistency over local elegance

If /coffees accepts ?limit=20, then /orders accepts ?limit=20, even if for orders you would have preferred to call it ?size. An API with twenty good but mutually different decisions is worse than an API with twenty acceptable and identical ones: the consumer learns the first and infers the other nineteen.

6.2. Predictability ("guessability")

A developer who has already used GET /v1/coffees/cof_001 should be able to write GET /v1/orders/ord_5001 without opening the documentation and get it right. A practical test: show someone three endpoints and ask them to write the fourth. If they get it right, the API is predictable.

# If this works like this...
curl https://api.aromastore.example/v1/coffees/cof_001
curl "https://api.aromastore.example/v1/coffees?roast=medium&limit=10"

# ...this should work the same way, without consulting the documentation
curl https://api.aromastore.example/v1/orders/ord_5001
curl "https://api.aromastore.example/v1/orders?status=paid&limit=10"

6.3. Resource orientation rather than action orientation

The API exposes things you operate on with the HTTP methods, not remote functions. POST /v1/orders/ord_5001/payment instead of POST /v1/payOrder. We already justified this in 01-04 and 01-05; the hard case —actions that do not fit into CRUD— is solved in 02-02.

6.4. Symmetry between operations

If the GET of a coffee returns priceEuros and stock, the POST that creates it should accept those same field names. If POST /coffees returns the created resource, PUT /coffees/{id} should also return the updated resource. Gratuitous asymmetries force people to memorise exceptions.

6.5. An explicit and stable contract

Everything the consumer can observe is part of the contract: field names, types, status codes, headers, error messages, a collection's default ordering. Whatever you do not want to guarantee, do not expose. And whatever you do expose, do not change without versioning (02-07).

6.6. Errors that teach

An error is just another response and is designed just as carefully as a success. It must say what failed, why and what the client can do about it. The format is fixed in 02-04.

  1. Granularity: neither too fine nor too coarse

Granularity is how much work a single call does. It is one of the decisions with the widest consequences and it has no universal answer.

An API that is too fine. Every minimal resource has its endpoint and the client composes. Painting a coffee's detail page forces you to: fetch the coffee, fetch its reviews, fetch the customer of each review... This is chattiness: lots of round trips. On a mobile network with 150 ms of latency, eight chained calls are more than a second lost purely in travel.

An API that is too coarse. A single endpoint returns the coffee with its reviews, the customers who wrote them, the stock per warehouse and the recommendations. One single call, but: enormous responses, almost all of it unused (over-fetching), useless caching (any change invalidates everything) and a contract coupled to one particular screen that will break when the screen changes.

Symptom Diagnosis Design remedy
The client makes 5+ calls for one screen Too fine Optional expansion (expand=), sub-resources with embedded data
Fields nobody uses get downloaded Too coarse Sparse fieldsets (fields=), links instead of embedding
An endpoint is used by only one screen Coupled to the interface Redesign around the resource, not the view
Changing a screen forces you to touch the API Coupled to the interface Go back to resource orientation

Aroma Store's stance: a medium-granularity, resource-oriented API, with two controlled escape valves designed in 02-05 —expansion (expand) to reduce calls and sparse fieldsets (fields) to reduce weight— plus data already embedded where real usage demands it (the coffee's name inside an order item, so that the client does not have to resolve every coffeeId).

  1. Design for the consumer, not for the database

The most frequent and most expensive mistake: publishing your tables. You take the relational schema, generate one endpoint per table and call that a REST API.

Here is what happens when we expose the coffees table as it stands:

{
  "coffee_id": 1,
  "coffee_name": "Ethiopia Yirgacheffe",
  "origin_fk": 12,
  "roast_code": 1,
  "price_cents": 1450,
  "current_stock": 120,
  "soft_deleted": 0,
  "created_ts": "2026-01-15 08:30:00",
  "created_by": "admin",
  "row_version": 7
}

Problems: the consumer has to translate roast_code: 1 into "light" using a table they do not have; origin_fk: 12 tells them nothing; price_cents forces them to know a storage decision; soft_deleted, created_by and row_version are internal plumbing that is now part of the contract and cannot be removed without breaking clients. And if the table gets normalised tomorrow, the API breaks.

The representation designed for the consumer:

{
  "id": "cof_001",
  "name": "Ethiopia Yirgacheffe",
  "origin": "Ethiopia",
  "roast": "light",
  "priceEuros": 14.50,
  "stock": 120,
  "tastingNotes": ["citrus", "floral", "black tea"],
  "createdAt": "2026-01-15T08:30:00Z"
}

The rule is: the representation is a projection designed for whoever reads it, not a dump of the row. The database can carry on storing cents, foreign keys and deletion flags; that is the persistence layer's business (03-05).

  1. Tolerance to change and the robustness principle

Jon Postel's robustness principle says: "be conservative in what you send, liberal in what you accept". Translated into an API:

  • As a server: emit exactly what the contract promises. No fields that appear only sometimes and no types that change.
  • As a client: do not break because a field you were not expecting turns up. This is the tolerant reader, and it is what allows the server to add fields without publishing a new version.

Design consequences we adopt right away:

  • Enumerations can grow. If roast: "very_dark" shows up tomorrow, clients must ignore it gracefully rather than blow up. This is documented from day one.
  • New fields are optional and additive.
  • A field name is never reused with a different meaning.
  • Collections are wrapped in an object ({"data": [...], "total": n}) precisely so that metadata can be added without changing the type of the response. The argument is made in 02-05.

Exactly what counts as a breaking change and what does not, and how deprecation is managed, is lesson 02-07.

  1. The Aroma Store API style guide

This is the central artefact of the module. A style guide is a short document, versioned in the repository, where the conventions that the whole API respects are written down. It serves three purposes: deciding quickly, reviewing in pull requests and onboarding new people.

We start with the decisions already taken (module 1) and the ones this module will close out:

Area Aroma Store convention Example Detailed in
Base URL https://api.aromastore.example/v1 02-07
Collection names Noun in the plural, lowercase /coffees, /orders 02-02
Compound words in URIs kebab-case /tasting-notes 02-02
Identifiers Opaque, with a type prefix cof_001, ord_5001 02-02
Non-CRUD actions Sub-resource + POST POST /orders/{id}/payment 02-02
Methods GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS 02-03
Partial update PATCH with JSON Merge Patch application/merge-patch+json 02-03
Safe retries Idempotency-Key header on sensitive POSTs paying for an order 02-03
Status codes The standard ones, nothing invented 201 + Location on creation 02-04
Error format {"error": {"code", "message", "details"}} coffee_not_found 02-04
Names in JSON camelCase priceEuros, createdAt 02-05
Dates and times ISO-8601 in UTC with Z 2026-03-14T10:32:00Z 02-05
Monetary amounts Number in euros with two decimals, Euros suffix 14.50 02-05
Enumerations Lowercase snake_case, extensible pending_payment 02-05
Collections Envelope {"data": [...], "total": n} 02-05
Links Richardson level 2 with selective hypermedia _links.self 02-05
Content language Accept-Language for tastingNotes en, es, ca 02-05
Pagination limit + offset, plus the Link header ?limit=20 02-06
Sorting ?sort=field / ?sort=-field ?sort=-priceEuros 02-06
Search ?q= over the collection ?q=yirgacheffe 02-06
Versioning In the path: /v1 02-07
Deprecation Deprecation and Sunset headers 02-07
Documentation OpenAPI 3.1 in the repository openapi.yaml 02-08
Code language Identifiers and comments in English getCoffees Module 3

Two warnings about style guides:

  1. They are written to be complied with. A guide nobody checks in pull requests is decoration. In 05-05 we will see how to automate part of the checking (OpenAPI linters).
  2. Conventions are arbitrary, consistency is not. camelCase is not objectively better than snake_case; what is objectively worse is using both.

  1. Map of module 2

graph LR
    L1["02-01<br/>Principles<br/><i>the method</i>"] --> L2["02-02<br/>Resources and URIs<br/><i>the what and the where</i>"]
    L2 --> L3["02-03<br/>HTTP methods<br/><i>the how</i>"]
    L3 --> L4["02-04<br/>Status codes<br/><i>the outcome</i>"]
    L4 --> L5["02-05<br/>Representations<br/><i>the body</i>"]
    L5 --> L6["02-06<br/>Collections<br/><i>filter and paginate</i>"]
    L6 --> L7["02-07<br/>Versioning<br/><i>time</i>"]
    L7 --> L8["02-08<br/>Documentation<br/><i>the published contract</i>"]

By the end of the module you will have the complete contract of the Aroma Store API: its URIs, its methods, its codes, its representations, its paginated collections, its versioning policy and its documentation. Module 3 implements it with Node.js and Express without inventing anything new.

Common Mistakes and Tips

  • Starting with the endpoints. If your first design sheet is a list of URLs, you have skipped the consumers and the domain. Start with use cases written in business language.
  • Designing the API while looking at the ORM. Column names, foreign keys and internal flags are not contract. Project, do not dump.
  • Designing the API while looking at the screen. The opposite extreme and equally damaging: endpoints that only serve one particular view age with that view. Design resources and give the client tools (fields, expand) to adapt them.
  • Optimising before you have the problem. Do not add expansion, exotic filters or business caching "just in case". Every mechanism in the contract has to be documented, tested and maintained forever.
  • Confusing consistency with rigidity. There will be legitimate exceptions (the PDF invoice, for example). What matters is that they are few, deliberate and written down in the style guide, not accidents.
  • Tip: write the response first. Before deciding the URL, write out by hand the JSON you would like to receive in the main use case. Many design decisions clear themselves up as soon as you see it.
  • Tip: the new developer test. If someone who did not take part in the design needs to ask what the pagination parameter is called, the API is not predictable yet.

Exercises

Exercise 1: separate resources from attributes

Aroma Store wants to add subscriptions: a customer receives a bag of coffee every month, with a frequency, a payment method, a delivery address and a history of deliveries already made. On top of that, each subscription can be paused.

Decide, justifying it with the four criteria in section 5, which of these nouns are resources and which are attributes: subscription, frequency, payment method, delivery address, delivery, pause.

Exercise 2: diagnose the granularity

The "my orders" screen in Aroma Mobile currently makes these calls:

GET /v1/customers/cus_842/orders          # 12 orders
GET /v1/orders/ord_5001                   # one per order, 12 calls
GET /v1/coffees/cof_001                   # one per item, ~25 calls

And the screen only shows, per order: date, status, total and the name of the first coffee. Diagnose the problem and propose two different design solutions, stating the drawback of each one.

Exercise 3: extend the style guide

Add three new rows to the table in section 10 that are not there today and that you know will be needed, covering these three matters: (a) what Aroma Store's own headers are called, (b) which time zone is used in the input dates the client sends, (c) what happens to unknown fields that a client sends in the body of a POST. Write the convention in one sentence per row.

Solutions

Solution 1

Noun Resource? Justification
Subscription Yes Its own identity (sub_310), a lifecycle (active → paused → cancelled), addressed on its own. Collection /subscriptions.
Frequency No An attribute of the subscription (frequency: "monthly"). It has neither identity nor lifecycle.
Payment method Yes, but not as a sub-resource of the subscription It has identity and is reused across orders and subscriptions: its own collection /payment-methods, referenced by id from the subscription.
Delivery address It depends If the customer stores several, it is a resource (/customers/{id}/addresses). If there is only one per subscription, it is a nested object in its representation. Decide according to the use case, not according to the table.
Delivery Yes, sub-resource Each delivery has a date, a status and tracking: /subscriptions/{id}/deliveries. It makes no sense outside its subscription.
Pause Yes, as an action modelled as a sub-resource It is not data, it is a transition: POST /subscriptions/{id}/pause, consistent with /approval and /cancellation. Detailed in 02-02.

Solution 2

Diagnosis: an API that is too fine for this use case. The screen needs four pieces of data per order and triggers something like 38 calls. That is pure chattiness, made worse by the fact that Aroma Mobile suffers mobile network latency. There is also over-fetching in the other direction: for each coffee everything gets downloaded just to read name.

Solution A — have the collection return what gets painted. GET /v1/customers/cus_842/orders returns each order with createdAt, status, totalEuros and the items with the coffee's name already embedded. A single call. Drawback: the coffee's name is duplicated across many responses and that copy has to be kept consistent; on top of that the response grows for every consumer, including those that do not need the items.

Solution B — expansion and sparse fieldsets. GET /v1/customers/cus_842/orders?expand=items.coffee&fields=id,createdAt,status,totalEuros,items. One call, and each consumer asks for what it needs. Drawback: mechanisms that have to be documented, validated and tested; they open the door to expensive queries and complicate caching, because every combination of parameters is a different URL.

(Aroma Store's decision combines the two: it embeds the coffee's name in the items —stable data that is always needed— and offers expand/fields as an escape valve. This is settled in 02-05.)

Solution 3

Area Aroma Store convention Example
Own headers Aroma- prefix in PascalCase with hyphens; never X- (obsolete per RFC 6648) Aroma-Event-Id, Aroma-Signature
Input dates Accepted only in ISO-8601 with an explicit time zone; the server normalises them and stores them in UTC 2026-03-14T11:32:00+01:00
Unknown fields in the body Rejected with 400 and the code invalid_data, naming the field in details, so typos are caught early {"naem": "..."} → error

About the last one: it is a debatable decision and worth understanding. Rejecting unknown fields (strict) catches client typos instantly; ignoring them (tolerant) makes it easier for a new client to talk to an old server. Aroma Store is strict on input and tolerant on output, which is exactly the robustness principle from section 9.

Conclusion

Designing a RESTful API does not start by drawing URLs: it starts by knowing who is going to use it and what for, extracting the nouns of the domain and deciding with judgement which of them deserve to be resources. From there, a handful of guiding principles —consistency, predictability, resource orientation, symmetry, a stable contract and useful errors— resolve most day-to-day decisions, while granularity and the refusal to expose the database avoid the two most expensive structural mistakes. All of that materialises in one concrete artefact: the style guide, which we have opened with Aroma Store's already firm decisions and which we will fill in lesson by lesson.

With the method clear and the consumers identified, it is time for the first concrete decision of the contract: which resources exist and what their URIs are called. In the next lesson, 02-02 Resources and URIs, we will turn the list of nouns into a complete map of addresses —collections, elements, nested sub-resources and singletons—, we will fix the naming rules, distinguish what goes in the path from what goes in the query string, choose the type of identifier and solve the problem that no CRUD solves on its own: how to model actions such as paying for an order or moderating a review.

REST API Course: Principles of Designing and Developing RESTful APIs

Module 1: Introduction to RESTful APIs

Module 2: Designing RESTful APIs

Module 3: Building RESTful APIs

Module 4: Best Practices and Security

Module 5: Tools and Frameworks

Module 6: Case Studies and Projects

© Copyright 2026. All rights reserved