1. Collection limitations and the contract map
The collection already stores runnable requests. What is missing now is a second artifact: a short record of what exactly we learned about the contract while running those requests.
Collection is super useful, but it has one small “human” problem: it stores requests as executable artifacts, not as a readable record of facts. A week later you will open Postman, see 12 requests, and think: “So... what matters here?” The contract map is a short, textual record: which scenarios exist, what their method/path/params/status are, and what response shape we actually need for ReadLater Starter.
It is important to get the scale right. The contract map is not “full provider documentation on 40 pages”, and even less “a copy-paste of the entire JSON”. It is more like a passport for two or three main scenarios. When you start writing code, the map will be your anchor: you will not be “remembering by feel” where the query parameter is, which status counts as success, or what the field with the external identifier is called.
You can think of it like this: a collection is a set of buttons you can press. A contract map is a short instruction describing what exactly happens when you press each button, and what you need to notice as a result. And yes, if you named the request test 1 and left it in history, then you do not have a map yet — you have archaeology.
2. Contract map template
For a contract map not to turn into an essay titled “How I Spent My Summer in Postman”, it needs a template. The good news: the template is very short, and it is almost entirely made of familiar words: method, path, query, status, headers, body. The bad news (joke): you will need to record the same thing for every scenario in a disciplined way, like a grown-up engineer, not like a romantic who says “I’ll remember it anyway”.
Below is a convenient format for the contract map. I am making it two-layered on purpose: first transport facts, then facts about data shape. This way you will not fall into the favorite beginner trap of “I saw JSON and forgot that there was also a status code and Content-Type”.
Mini template
## Scenario: <Scenario name>
Transport:
- Method: <GET/POST/...>
- Path: <for example, /search or /books/{externalId}>
- Query params: <which ones exist and which are required>
- Success status: <for example, 200>
- Response Content-Type: <for example, application/json>
Body shape:
- Top-level: <object | array>
- Required fields we use: <what matters for ReadLater>
- Optional fields we use: <what may be absent>
- Notes: <1-2 observations about oddities/nuances>
If you like recording this as structured data, you can also keep the following “minimal model” in mind — just so you do not forget the field set.
// Minimal contract snapshot: enough to navigate scenarios
record ContractSnapshot(String scenario, String method, String path, int successStatus) {}
// Example: capture the search scenario as a fact (what we call and which status counts as success)
ContractSnapshot search = new ContractSnapshot(
"Search books by query",
"GET",
"/search",
200
);
Notice a useful detail: it is better to record path as a template, not as a concrete URL with a concrete value. That is, /books/{externalId}, not /books/OL12345M. You can keep the concrete example too, but separately, as a “run example”.
3. Scenario “Search”: search
Search is the scenario where the life of any “catalog” usually begins: the user does not yet know the book’s external identifier, they only know clean code, harry potter, or “something about microservices, but without microservices, please”. That is why for ReadLater Starter search is the basic entry point: we get a list of candidates and choose which book to inspect in detail next.
When you capture the search contract, your goal is not “to list all 200 fields the provider graciously sent”, but to understand the response shape and choose the fields that are actually needed by our project. For the bridge course this is especially important: we are not building an Amazon-level product, we are building a clear educational artifact. So we take the minimum: external id, title, author.
Transport example
Imagine the scenario looks like this:
Method:
GET
Path:
/search
Query:
q — required search parameter
Success status:
200
Response Content-Type:
application/json
Example URL that you can save as the “reference run” in Postman:
GET https://catalog.example/search?q=clean+code
Here it is important to capture one thought: q is the control handle of the scenario. So in the contract map it is useful to honestly write: “without q, search makes no sense”.
Response shape example
{
"items": [
{
"externalId": "OL12345M",
"title": "Clean Code",
"author": "Robert C. Martin"
}
],
"count": 1
}
Even if your provider uses different names (docs, results, total, id), the principle is the same: we have a list and some meta-information about the count. For the contract map it is important to record the shape: a top-level object, inside it an array of elements, and separately a number count/total.
Here it is also useful to note and record in Notes: count and items.size() do not have to match. The provider may return count = 1000, while items contains only the first 10 results. This is not an API “error”; it is normal practice.
Fields for ReadLater Starter
For search, three things are usually enough: externalId, title, author. And here the first realistic pain point of the contract appears: the author may be a string, an array of strings, an object, or may live somewhere else entirely. And that is fine. In the contract map it is convenient to record not only the field name, but also its “path” in JSON and a note about whether it is required.
import java.util.List;
// Description of one field that we "take" from the response.
record FieldNote(String jsonPath, boolean optional) {}
// Minimal set of what we critically need from the search response.
List<FieldNote> searchFields = List.of(
new FieldNote("items[].externalId", false), // external id — the basis for the details request
new FieldNote("items[].title", false), // we show the title to the user
new FieldNote("items[].author", true) // may sometimes be absent / be an array
);
The trick is that you do not pretend the world is perfect. If you notice that author is sometimes missing or looks different, you do not “blame the API”; you simply record the reality of the contract. That is backend thinking: not getting offended by the network and external services, but working with facts.
4. Scenario “Details”: details
The details scenario is usually simpler in shape (one object instead of a list), but trickier in content: in details the provider often sends many more fields, more nesting, and more “optionality”. And at this moment the beginner very much wants to “write down everything”. That desire is understandable, but it usually ends with the contract map swelling to the size of a novel, while usefulness drops to the size of a bookmark.
The purpose of the details scenario for ReadLater Starter at this stage is simple: given a book’s external identifier, get a “card” that can be shown to the user and used as a data source for adding to the reading list. So we capture the transport part and the minimal set of fields for the card: external id, title, author, and, if we are lucky, a short description.
Transport example
Typical form:
Method: GET Path: /books/{externalId} Path variable: externalId — required
Success status: 200 Response Content-Type: application/json
Reference run example:
GET https://catalog.example/books/OL12345M
And here is an important contract observation: externalId lives in the path, not in query. That is, it is not “search”, it is “give me a specific resource by id”. It is worth recording this in the map with at least one sentence so you do not confuse it later during implementation.
Response shape example
{
"externalId": "OL12345M",
"title": "Clean Code",
"author": "Robert C. Martin",
"description": "A Handbook of Agile Software Craftsmanship"
}
In the real world description may be null, may be a nested object, or may be absent. And again, this is not a reason to dramatize. We simply record that the field may be optional, and therefore it should not be the only pillar of your logic.
For the contract map, it is useful to split fields into “required for our project” and “nice bonuses” right away. This is not a list of “what the provider returns”, but a list of “what we need to build the next part of the project”.
For example, in the notes for details you can honestly write a thought like: “externalId sometimes does not come in the response body, but it is always known from the request.” This is a very useful engineering note: you do not spend an hour wondering “why is externalId null?”, and instead calmly add it to the normalized card from the request context.
And yes, if author comes as an array in the response, we record that too as an observation. That is not a “bad provider”; it is just the reality of transport contracts.
5. Minimal smoke suite
If the contract map is a “passport”, then the smoke suite is a “quick test that the passport is not forged”. And the word smoke here is not about everything burning down (though that happens), but about the old engineering idea: run the most basic thing, and if it works, then the system is at least alive. For an educational project, the smoke suite is especially important: it lets you avoid relying on perfect memory and prevents every new attempt from turning into “a fresh investigation”.
At the level of this lecture, the smoke suite is exactly a manual minimal baseline: a couple of saved requests that you can run and visually check for status, Content-Type, and response shape. No scripts and no attempt to automate everything at once.
Example of a minimal smoke suite
Below is a convenient table that you can insert directly into your contract map or README next to the Postman collection:
| Request name (in collection) | Method | URL (example) | Expected | What we check by eye |
|---|---|---|---|---|
| Search books by query | GET | /search?q=clean+code | 200 + JSON | There is a top-level object, there is an array of results (items/results), and inside at least one element there is a title and some id |
| Get book details by external id | GET | /books/OL12345M | 200 + JSON | There is a top-level object, there is a title, there is an author (or a clear author structure), and the response format has not turned into HTML |
There is a subtle point here. For the details request you need one specific externalId that you treat as an “example”. You can take it from the search result and simply choose one stable example once and insert it into the saved request. This is not an automatic request chain; we are just choosing one stable example so smoke can be run without a quest.
And one more simple discipline: for the smoke suite it is better to choose requests that are not tied to rare or unstable data. If you search for asdkjashdkjashd, you are creating a situation where tomorrow count=0, and you will not understand whether the contract changed or you were just searching for nonsense. In an educational context, clean code usually behaves more calmly.
6. Storing the result
There is a classic trap in any tool: “if it lives only inside the tool, it does not live long”. Today you remember everything, tomorrow Postman updates, the collection ends up in another workspace, and in a month you want to inspect the diff in Git and suddenly discover that “the contract is not written down anywhere in words”. So a good end-of-day result is two artifacts: the collection itself and the text contract map in the repository next to the ReadLater Starter code.
A convenient repository structure looks like this: the collection lives in postman/, and the contract map lives in docs/ (or also in postman/, if you want to keep all Postman artifacts in one place). This is not “more correct by the canons”; it is simply convenient for a human: code separately, integration artifacts separately.
Example file docs/catalog-contract.md
# External Catalog Contract
## Scenario: Search books by query
Transport: GET /search?q=<query>, success=200, response=application/json
Body shape: object with array results + count
We use fields: externalId, title, author
Notes: count may be > returned items count
## Scenario: Get book details by external id
Transport: GET /books/{externalId}, success=200, response=application/json
Body shape: object
We use fields: title, author, (optional) description
Notes: externalId may be missing in response body; take from request path
Yes, it looks a bit boring. And that is its strength. Good engineering documentation is almost always more boring than a chaotic “adventure story”. But that boredom saves hours later, when you implement the client and start catching mismatches between “what I thought” and “what is actually happening”.
The point is not bureaucracy. When the collection, the contract map, and two manual smoke requests live next to the project, the contract stops living only in memory and on the Send button. On that basis you can calmly build up variables and request chains, because the basic scenarios are already fixed.
7. Typical mistakes when working with the contract map
Mistake #1: record only the body and forget about method/path/status/headers.
A beginner often takes a “JSON screenshot” and thinks the contract is captured. Then it turns out that you got that JSON only with GET, only with the correct query, and only with Accept: application/json, and you never wrote any of that down. As a result, you have a pretty response body, but no contract. A contract always starts with transport facts.
Mistake #2: mix the scenario template and a concrete example into one mess.
If you wrote GET /books/OL12345M as the “path” and forgot that it is actually the template /books/{externalId}, then you have placed a mine for yourself. The contract map must separate “shape” and “run example”. The shape is needed for code; the example is needed for smoke checks and visual orientation.
Mistake #3: try to “save everything that came back” and turn the contract map into a JSON dump.
This is psychologically understandable: it is scary to lose information. But engineering-wise it is harmful: you drown in details and lose the main point. The map exists to highlight key fields and the response shape. Everything else lives in Postman, where you can always open the response body and look again.
Mistake #4: do not mark optional fields and act as if everything is always present.
External APIs are almost never perfect: sometimes there is no author, sometimes the description is null, sometimes a field is unexpectedly an array. If you do not record the observation “may be absent”, you are pre-programming a future NullPointerException in your head. Even if we are not writing code right now, a proper map should already be honest about optionality.
Mistake #5: make the smoke suite “creative” instead of reproducible.
The smoke suite is not the place for experiments. It should be simple and repeatable. When you search a different string each time or take a new random id, you cannot tell “the contract changed” from “the catalog data is different”. Smoke is about stability and a fast signal.
GO TO FULL VERSION