Your screen is too small. Please widen your browser window or use a device with a screen width of at least 300px to use this app.
Generating the endpoint takes a minute. Deciding what it returns is what determines whether your screen survives its first real row. Here is where a mock belongs, what to put in it, and the values that find bugs instead of hiding them.
Most frontend teams reach for one of three things while the API is still being written. They hardcode a fixture into the component, they stand up a small Express or json-server process, or they intercept requests in the client with a library. Each one works, and each one carries the same cost: the mock lives inside the codebase, so it has to be written, reviewed, merged, and eventually removed.
That cost is paid again every time the shape of the response changes. It is also paid by the next person, who finds a fixture file and cannot tell whether it still matches production. Mocks that live in the repository tend to rot quietly, and the rot only shows up when someone trusts them.
The fixture carries a second problem that is easier to miss. Importing a JSON file into a component gets data on the screen, but it skips everything that makes a network call a network call. There is no loading state, because the data is already there. There is no error branch, because an import cannot fail at runtime. There is no race, no abort, no stale response arriving after the user has navigated away. Those are precisely the paths that break in production, and a mock served over HTTP puts every one of them back in play.
These are the six places a mock usually ends up, and they are not ranked. A fixture is the right answer for a snapshot test, Faker is the right answer when the data has to be generated inside a test run, and Mock Service Worker is the strongest choice when the mock belongs to the test suite, because the handlers are code you can override inside a single test and review in the same pull request as the feature. What separates them is where the data comes from and who else can see it.
| Approach | Where it lives | Who can reach it | Varies per call | Best at |
|---|---|---|---|---|
| Hand written fixture | A file in your repo | Only people with the repo | Never, it is the same every run | A snapshot test that must not move |
| Faker in your test setup | Your build or test process | Only people with the repo | Every run, unless you pin a seed | Generating data inside a test run |
| MSW | In your repo, as code |
Here is the mock data almost everyone writes first. It is not wrong, and it will get a table onto the screen. It will also pass every visual check you make, right up until the first real row arrives.
[
{ "id": 1, "name": "John Doe", "email": "john@example.com", "total": 100.00 },
{ "id": 2, "name": "Jane Roe", "email": "jane@example.com", "total": 200.00 },
{ "id": 3, "name": "Bob Smith", "email": "bob@example.com", "total": 300.00 }
]Look at what it quietly guarantees. Every name is about the same width, so no cell ever wraps. Every total has two decimal places, so you never notice the formatter is missing. Every field is present and non null, so the optional chaining you forgot never throws. Every list has exactly three items, so pagination, overflow and the empty state are all untested. The ids are small, so the rounding bug stays asleep.
Placeholder data is uniform in a way real data never is, and a UI built against it is tested against the one dataset guaranteed not to break it. The fix is not more rows. It is rows that disagree with each other.
This is the same list, written to be difficult. Two rows is enough, because the point is the contrast between them rather than the volume.
[
{
"id": 9007199254740993,
"name": "Bartholomew Featherstonehaugh-Wetherby III",
"email": "b.featherstonehaugh@a-rather-long-company-name.co.uk",
"nickname": "",
"avatar_url": null,
"tags": [],
"total": 1299.5,
"note": "実験用のデータ 🎉"
},
{ "id": 2, "name": "Bo Ng", "email": "bo@x.io", "nickname": "Bo",
"avatar_url": "https://example.com/a.png", "tags": ["vip"], "total": 8, "note": "" }
]Four things go wrong when JSON travels over HTTP, and all four are invisible until they are expensive. The first is the long id from the list above. JavaScript holds numbers as doubles, so any integer past 9,007,199,254,740,991 loses precision the moment it passes through JSON.parse. Snowflake ids and database bigints land squarely in that range, and the symptom is not an error, it is two different records comparing as equal.
// the id you put in the mock
{ "id": 9007199254740993 }
// the id your component compares against, after JSON.parse
{ "id": 9007199254740992 }The second is key order. It carries no meaning in the JSON spec, but a JavaScript object reorders integer-like keys ahead of the rest, so a body keyed by id can come back in an order nobody wrote. If anything downstream hashes or diffs the raw response, that reordering is a silent mismatch. Any tool that round-trips your mock through a parser before sending it will do this to you.
The third is null against absent. A key holding null and a key that is not there at all are different states, and they reach your component differently: one overwrites a default, the other lets it stand. Mock data that never sends an explicit null cannot tell you which branch you wrote.
The fourth is the charset. A response served without a utf-8 charset can mangle accented characters and emoji on the way to a client that guesses wrong, which is why the Japanese string in the sample above is worth keeping in your test data. All four are decided on the wire rather than in your component, so whatever serves your mock has to get them right before your test data can mean anything.
A list of three proves a fetch works and nothing else. The interesting counts are zero, one and more than fits. Zero is the empty state, which is usually the least designed screen in any product. One is the layout that looks broken because a grid built for a dozen cards is holding a single one. More than fits is where pagination, virtualisation, sticky headers and overflow all get their first real test.
The practical move is to keep all three alive at once rather than editing one dataset back and forth, and that is really a question about endpoints rather than rows. Point three paths at the same shape with different contents and switching between them costs a base URL change instead of a code change. Do the same for the unhappy paths and a 404, a 500 and a deliberately slow response stop being things you simulate by hand.
There is a failure mode on the other side of tidy data. Randomise every field independently and you get a customer named Xq Zzt whose email belongs to someone else, living in a city that is not in the country beside it, with an order total that has no relationship to the lines above it. That data is varied and still useless, because nobody can tell a rendering bug from the noise.
What separates useful variation from noise is that the relationships survive it. The shape stays fixed. The fields that move are the ones whose value genuinely differs row to row, and the ones that depend on each other move together: the email follows the name, the city follows the country, the total follows the lines.
Variation is also not always what you want. A snapshot test and a demo script both need the same answer every time, so whatever produces your data should be able to sit still on request. Data that changes when you did not ask it to is its own kind of bug.
Variety is only useful while the payload still makes sense.
An email, username and phone that match the name beside them, and a city that sits in the right country. Independently random fields read as noise, and nobody can tell a rendering bug from noise.
A sum that matches the lines above it, and a delivery date that falls after the order date. A screen doing its own arithmetic should never disagree with the payload it was handed.
Write the response you actually want and call it over HTTPS a minute later.
| Whoever runs the project |
| Only if your handler makes it |
| Mocks versioned with the code, and per test overrides |
| json-server | In your repo, as a JSON file | Your machine only | No, it serves the file | A full REST shape with writes that stick for the session |
| Postman mock server | In a Postman collection | Your Postman team | Only from saved examples | Teams already running their contracts through Postman |
| Fake API | Behind an HTTPS URL | Anyone holding the URL | Every request, if you ask it to | Sharing one state with a designer, a tester or a CI job |
The bottom row is the one that changes who the data is for. Everything above it lives inside the project, so a designer checking a state, a QA engineer reproducing a bug, or a CI job on another machine cannot reach it without running your code. A URL can be pasted into a ticket. That is the whole of the difference, and it is why the two halves of this table are not really competing.
None of these are exotic. Every one of them is a value a production API will send you eventually, and each one has a matching bug that is cheap to fix now and expensive to find later.
Names of different lengths, emails that are not all the same domain, prices that are not all round. A column only wraps when something in it is long enough to wrap.
A count that moves between calls is what exercises a table's pagination, its overflow and its sticky header. A list fixed at three items exercises none of them.
A value that only makes sense sometimes should be absent the rest of the time, such as a shipping date on an order that has not shipped. That is the branch your optional chaining is for.
A sample of one row proves the fetch worked. A page of results is what shows you the layout, and the two tell you completely different things about the same component.