flaky.

Moving from JSONPlaceholder

Same resources, same field names, same nested routes. The migration is one string in your code. What you get for it is the thing JSONPlaceholder cannot do: ask for the failure you are trying to test against.

The change

- fetch('https://jsonplaceholder.typicode.com/posts')
+ fetch('https://flakyapi.dev/v1/posts')

Two differences in that line: the host, and a /v1 prefix. The prefix exists so the API can change later without breaking the URLs you have already published — the exact problem JSONPlaceholder has, having never versioned.

A find-and-replace across a project is usually enough:

sed -i '' 's|jsonplaceholder.typicode.com/|flakyapi.dev/v1/|g' src/**/*.js

What is identical

Every shared resource returns the same field names, so nothing downstream of the fetch has to change — not your types, not your components, not your tests.

ResourceFieldsJSONPlaceholderflaky
postsid, userId, title, body100100
commentsid, postId, name, email, body500500
albumsid, userId, title100100
photosid, albumId, title, url, thumbnailUrl5,0001,000
todosid, userId, title, completed200200
usersid, name, username, email, address, phone, website, company1010
productsid, title, price, rating, stock, brand, category…100

Nested routes work the same way — /v1/posts/1/comments, /v1/albums/1/photos, /v1/users/1/todos — and so does filtering by any field, /v1/todos?userId=3&completed=true.

Paging accepts both styles. _start and _limit, the json-server form JSONPlaceholder uses, work unchanged; _page and _limit also work if you prefer pages. So do _sort and _order.

What is different, honestly

photos has 1,000 rows, not 5,000

The dataset ships inside the Worker so a list request is an array slice rather than a database read. Five thousand photo rows would be two thirds of the bundle for a resource almost nobody paginates past the first page of. If you genuinely need 5,000, say so and it can go up.

There are rate limits

1,000 requests a day anonymously; 10,000 with a free key that takes one request to create and needs no confirmation email. JSONPlaceholder publishes no limit. Every response carries x-ratelimit-remaining so you are never guessing.

Writes are still fake — but they say so

POST, PUT, PATCH and DELETE are echoed and not stored, exactly as on JSONPlaceholder. The difference is the response carries x-mock-write: not-persisted, so nobody loses an afternoon discovering it. When you need writes that stick, take a sandbox.

An unknown _param is an error

JSONPlaceholder treats ?_typo=1 as a filter on a field named _typo, matches nothing, and returns []. Here it returns a 400 naming the parameters that exist. An empty array that means "you made a typo" costs more time than an error.

The reason to switch

Every fake API hands you data. None of them let you ask for the thing you are actually testing: the slow response, the 503, the endpoint that fails one time in three. That is normally a local mock server, a branch in your code, or a setTimeout you have to remember to delete.

# your loading state, with a real slow response
GET /v1/posts?_delay=3000

# your error boundary, with a real 503
GET /v1/posts?_status=503

# your retry logic, failing one request in three
GET /v1/posts?_fail_rate=0.3

# slow and unreliable at once, because that is what production is
GET /v1/products?_delay=1500&_fail_rate=0.2

It works on every endpoint, including nested routes. No install, no config, no branch in your code that has to be removed before you ship — and it works from a browser console, a CodePen, or a colleague's machine with nothing set up.

Out-of-range values are rejected rather than ignored: ?_status=999 returns a 400, not a silent 200. A tool for testing failures should not fail quietly itself.

Try it before you change anything

Paste either of these into a terminal — nothing to sign up for.

curl 'https://flakyapi.dev/v1/posts?_limit=2'

curl -i 'https://flakyapi.dev/v1/posts?_status=503'

Or use the live inspector on the home page, which lets you set the delay, status and failure rate and watch the response change.