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
- API-first versus code-first
- The design process in seven steps
- Step 1: identify consumers and use cases
- Step 2: extract the nouns of the domain
- Step 3: decide what becomes a resource and what does not
- Guiding design principles
- Granularity: neither too fine nor too coarse
- Design for the consumer, not for the database
- Tolerance to change and the robustness principle
- The Aroma Store API style guide
- Map of module 2
- 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.
- 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.
- 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.
- 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.
- Step 3: decide what becomes a resource and what does not
Not every noun deserves a URI. Apply these filters:
- 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. - 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.
- 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.
- 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.
- 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.
- 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).
- 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).
- 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.
- 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:
- 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).
- Conventions are arbitrary, consistency is not.
camelCaseis not objectively better thansnake_case; what is objectively worse is using both.
- 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 callsAnd 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
- What Is an API?
- History and Evolution of APIs
- HTTP Fundamentals for APIs
- Basic Principles of REST
- The Richardson Maturity Model and HATEOAS
- REST vs. SOAP
- REST Compared with GraphQL, gRPC and Webhooks
Module 2: Designing RESTful APIs
- RESTful API Design Principles
- Resources and URIs
- HTTP Methods
- HTTP Status Codes
- Representations, Headers and Content Negotiation
- Filtering, Sorting, Pagination and Search
- API Versioning
- API Documentation
Module 3: Building RESTful APIs
- Setting Up the Development Environment
- Building a Basic Server
- Handling Requests and Responses
- Input Data Validation
- Persistence and the Data Access Layer
- Authentication and Authorisation
- Error Handling
- Testing and Validation
Module 4: Best Practices and Security
- API Design Best Practices
- Security in RESTful APIs
- OAuth 2.0 and OpenID Connect in Practice
- Rate Limiting and Throttling
- CORS and Security Policies
- HTTP Caching and Performance
- Observability: Logs, Metrics and Traces
Module 5: Tools and Frameworks
- Postman for API Testing
- Swagger and OpenAPI for Documentation
- Popular Frameworks for RESTful APIs
- Contracts, Mocks and Automated API Testing
- Continuous Integration and Deployment
- API Gateways and Developer Portals
