GuidesTEST APIS
Free fake APIs for testing a React app
We tested 13 free fake APIs from a browser page on 26 October 2026: 11 send CORS headers, so you call them with fetch() directly and no proxy, and 9 of those accept POST. JSONPlaceholder, DummyJSON and ReqRes answer 201 to a write and store nothing, while Platzi Fake Store API and restful-api.dev really save it. Postman Echo was the only one with no CORS headers.
Results by API
Every test ran from a page on http://127.0.0.1 in Chromium 156 and Firefox 157, with headers checked again in curl. Random User and PokeAPI are read only: Random User returns up to 5,000 fake users per request with ?results= (add seed= to get the same users every time), and PokeAPI is GET only with no key and no rate limit since November 2018, though its fair use policy asks you to cache. For real data instead of fake data, see the 70 free public APIs that work from the browser.
| API | Data | Writes persist | Key or headers | CORS |
|---|---|---|---|---|
| JSONPlaceholder | 100 posts, 500 comments, 100 albums, 5,000 photos, 200 todos, 10 users | No, faked | None | Yes, all methods |
| DummyJSON | 194 products, 208 users, 208 carts, 1,454 quotes, 50 recipes and more | No, faked | None, JWT login with a demo user | Yes, all methods and Authorization |
| Fake Store API | 20 products, 7 carts, 10 users | No, faked | None, token login | Yes, but headless Chromium got a 403 bot check |
| Platzi Fake Store API | Products, categories and users (22, 10 and 34 when tested) | Yes, shared with everyone | None, JWT login | Yes, all methods |
| ReqRes | 12 users, login and register | No, nothing is stored | None for the public demo | Yes, all methods |
| httpbin | Echoes your request, any status code, delays up to 10 seconds | Nothing to store | None | Yes, all methods |
| Postman Echo | Echoes your request, status codes, delays | Nothing to store | None | No header at all |
| restful-api.dev | 13 sample objects plus the ones you create | Yes, 50 requests per day | None, x-api-key for 100 per day | Yes, all methods |
| Random User | Up to 5,000 fake users per request | Read only | None | Yes |
| PokeAPI | 1,351 Pokemon and 1,025 species | Read only, GET only | None | Yes |
| MockAPI.io | Resources you define, 1 project with 2 resources free | Yes, in your project | Free account | Yes, no PATCH |
| Beeceptor | Your own mock endpoint, 50 requests per day free | Up to 10 objects on CRUD routes | None | Yes, all methods |
Two of the 13 did not work. Postman Echo (postman-echo.com) has the same routes as httpbin, but it sends no Access-Control-Allow-Origin header on any response, not even on OPTIONS. My JSON Server (my-json-server.typicode.com) answered HTTP 530 with Cloudflare error 1016 from two networks on the test day, home page included.
Fake writes or real writes
A React create form usually sends a POST, then refetches the list. With JSONPlaceholder the form gets a 201 and the new item never shows up. That is by design: its own guide says the resource is not really created and the response is faked.
Write, then read it back:
- JSONPlaceholder:
POST /postsreturned 201 with id 101, thenGET /posts/101returned 404.PUTandDELETEreturn success and change nothing. The preflight allows every method and theAuthorizationheader. - DummyJSON:
POST /products/addreturned 201 with id 195, thenGET /products/195was 404. - Fake Store API:
POST /productsreturned 201 with id 21, thenGET /products/21came back with an empty body. - ReqRes:
POST /api/usersreturned 201 with an id that 404s on read. Its home page says "Anonymous demo, rate-limited per IP. No data is stored." - Platzi Fake Store API (
api.escuelajs.co): aPOSTto/categoriescreated id 94, aGETreturned it and aDELETEremoved it. Everyone shares the same data, so other people's test entries named "string" and "New Ctg" sat in the list and the counts in the table will drift. - restful-api.dev:
POST /objectscreated an object,GETreturned it,DELETEremoved it. Ids 1 to 13 are sample objects you cannot change; aPUTon id 7 returns 405.
MockAPI.io and Beeceptor are built to store data too, but both need setup first, so we did not write to them. MockAPI's free plan is 1 project with 2 resources; $5 a month or $35 a year gets 20 projects and 50 resources. Its project URLs allow GET, PUT, POST and DELETE cross-origin, but PATCH is missing from the allowed methods. Beeceptor's free plan needs no signup and gives 50 requests per day per endpoint, 3 mock rules and CRUD routes that keep 10 small objects; unclaimed free endpoints are deleted after 30 days.
Faked writes are fine for testing loading and success states. When the UI must show the new item after a reload, use Platzi or restful-api.dev, or keep the created item in local state.
const api = 'https://jsonplaceholder.typicode.com';
const posts = await fetch(`${api}/posts?userId=1`).then((response) => response.json());
const response = await fetch(`${api}/posts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Hello', body: 'Sent from the browser', userId: 1 }),
});
const created = await response.json();
// Writes are faked: the new id answers 404 when you read it back
const readBack = await fetch(`${api}/posts/${created.id}`);
console.table(posts.slice(0, 3).map(({ id, title }) => ({ id, title })));
console.log(response.status, created, readBack.status);Login, tokens and pagination
DummyJSON covers login and paging on one host. POST /auth/login with the demo user emilys and password emilyspass returns a JWT accessToken. Send it as Authorization: Bearer TOKEN to GET /auth/me and you get the user back. A bad token returns 401 with {"message":"Invalid/Expired Token!"}, and the browser can read that body. The JSON body and the Authorization header both trigger a CORS preflight, which DummyJSON answers.
For paging, use limit and skip, and select to trim fields: /products?limit=10&skip=20&select=title,price returns products 21 to 30. limit=0 returns all 194 products in one call. Fake Store API (/auth/login) and Platzi also have token login.
const api = 'https://dummyjson.com';
const login = await fetch(`${api}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'emilys', password: 'emilyspass', expiresInMins: 30 }),
});
if (!login.ok) throw new Error(`Login failed: ${login.status}`);
const { accessToken } = await login.json();
const me = await fetch(`${api}/auth/me`, {
headers: { Authorization: `Bearer ${accessToken}` },
}).then((response) => response.json());
const page = 3;
const pageSize = 10;
const { products, total } = await fetch(
`${api}/products?limit=${pageSize}&skip=${(page - 1) * pageSize}&select=title,price`,
).then((response) => response.json());
console.log(me.username, `page ${page} of ${Math.ceil(total / pageSize)}`);
console.table(products);Error and loading states
httpbin returns any status you ask for: /status/500 gives a 500, and 503 and 429 behaved the same. It reflects your Origin in the CORS header, so fetch() can read the status instead of failing with a network error, which is what you need to test an error banner or a retry button.
For spinners and skeleton screens, slow the response down on purpose. httpbin's /delay/3 waits 3 seconds and caps at 10 (/delay/12 answered after 10.4 seconds). DummyJSON takes ?delay= in milliseconds on any route: delay=3000 answered in 3.3 seconds, and the maximum is 5000 (6000 returns a 400). Pair the slow calls with AbortSignal.timeout() so your component shows a timeout state instead of spinning forever.
async function probe(url, timeoutMs = 5000) {
const started = performance.now();
try {
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
return { url, status: response.status, ok: response.ok, ms: Math.round(performance.now() - started) };
} catch (error) {
return { url, status: error.name, ok: false, ms: Math.round(performance.now() - started) };
}
}
const results = await Promise.all([
probe('https://httpbin.org/status/500'),
probe('https://httpbin.org/status/429'),
probe('https://httpbin.org/delay/3'),
probe('https://httpbin.org/delay/8'),
probe('https://dummyjson.com/products/1?delay=2000&select=title'),
]);
console.table(results);When a test API blocks the browser
Three things blocked our browser tests:
- Postman Echo sends no CORS headers. Chromium throws
TypeError: Failed to fetch(see Failed to fetch) and Firefox throwsTypeError: NetworkError when attempting to fetch resource.AGETthroughhttps://proxy.cors.dev/https://postman-echo.com/getworked in both, since the free cors.dev proxy relaysGETandHEADtext responses up to 1 MiB with no key. To proxy a POST or custom headers you need cors.dev Pro at $5 a month, but httpbin and Beeceptor acceptPOSTdirectly, so use them instead. - Fake Store API has a Cloudflare bot check. It sends
Access-Control-Allow-Origin: *and works in Firefox, but headless Chromium, the browser Playwright and Puppeteer use in CI, got a 403 challenge page andfetch()failed. Run end-to-end tests against DummyJSON instead. - restful-api.dev limits are per IP. The public API allows 50 requests per day (100 with a free
x-api-key), reset every 24 hours. Through a shared proxy IP it had already answered 405 "daily request limit", so call it directly and each visitor gets their own 50.
Good questions.
Which free fake API is best for a React app?
DummyJSON, if you need login, paging and artificial delays on one host; JSONPlaceholder for simple lists like /posts and /users. If created items must show up after a refetch, use restful-api.dev or Platzi Fake Store API.
Does JSONPlaceholder save POST requests?
No. POST /posts returns 201 with id 101, but GET /posts/101 returns 404, and PUT and DELETE change nothing. The responses are faked by design.
Which free test APIs actually store data?
Platzi Fake Store API and restful-api.dev stored our writes and returned them on the next GET. Platzi shares one database with everyone; restful-api.dev allows 50 requests per day per IP. MockAPI.io and Beeceptor store data once you set up a project.
Do I need an API key for ReqRes?
No. The anonymous demo worked with no key and no signup and reported a limit of 40 requests per day, reset at midnight UTC. Its preflight still allows an x-api-key header, so old tutorials that send one keep working.
How do I test a 500 error or a slow response?
Call https://httpbin.org/status/500 for an error status the browser can read and https://httpbin.org/delay/3 for a slow response (10 seconds maximum), or add ?delay=3000 to a DummyJSON route (5000 ms maximum).
Why does a test API work in Postman but not in my React app?
Postman is not a browser, so it never checks CORS headers; Postman vs the browser covers the details. Postman Echo is the clear case: fine in Postman, blocked in every browser because it sends no Access-Control-Allow-Origin header.