TestingAPIs is still being built. If something breaks, email support@testingapis.com.

A JSONPlaceholder alternative where writes are saved

The same kinds of resources, but writes are saved, bad input is rejected and errors are available on demand.

Examples last run against the API on

JSONPlaceholder fakes its writes: a POST returns id: 101, but a GET for that id returns 404, and a DELETE leaves the record in place. If your app needs to read back what it just wrote, use https://api.testingapis.com/apisign in to see. It has the same kinds of resources (posts, comments, albums, photos, todos and users), and creates, updates and deletes are saved. It also rejects invalid input with a 400 and can fail any request on demand, so you can build and test the error states too. It's free and allows requests from any origin. Posts and todos need no key; the other resources need a free account's API key in the X-TestingApis-Key header.

Writes that are saved

This is what JSONPlaceholder does with a new post:

Terminal
$ curl -X POST https://jsonplaceholder.typicode.com/posts \
  -H "Content-Type: application/json" \
  -d '{"title":"hello","body":"world","userId":1}'
{ "title": "hello", "body": "world", "userId": 1, "id": 101 }

$ curl -i https://jsonplaceholder.typicode.com/posts/101
HTTP/1.1 404 Not Found

The same steps here: create a post, read it back, edit it, delete it and check it's gone.

Terminal
$ curl -X POST https://api.testingapis.com/api/Posts \
  -H "Content-Type: application/json" \
  -d '{"userId":101,"title":"Hello from the guide","body":"Checking that writes persist."}'
{"message":"Post created successfully","data":{"id":16,"userId":101,"title":"Hello from the guide","body":"Checking that writes persist.","tags":[],"publishedAt":"2026-10-07T02:58:58Z","likes":0}}

$ curl https://api.testingapis.com/api/Posts/16
{"id":16,"userId":101,"title":"Hello from the guide","body":"Checking that writes persist.","tags":[],"publishedAt":"2026-10-07T02:58:58Z","likes":0}

$ curl -X PUT https://api.testingapis.com/api/Posts/16 \
  -H "Content-Type: application/json" \
  -d '{"userId":101,"title":"Edited title","body":"Checking that writes persist."}'
{"message":"Post updated successfully","data":{"id":16,"userId":101,"title":"Edited title",...}}

$ curl -X DELETE https://api.testingapis.com/api/Posts/16
{"message":"Post deleted successfully"}

$ curl -i https://api.testingapis.com/api/Posts/16
HTTP/1.1 404 Not Found

{"status":404,"error":"Not Found","message":"Post with ID 16 does not exist."}

Route mapping

JSONPlaceholdertestingapis.comRecords
/posts/api/posts15 (JSONPlaceholder: 100)
/posts/1/comments/api/posts/1/commentsComments on post 1
/comments/api/commentsdocs50 (500)
/albums/api/albumsdocs10 (100)
/albums/1/photos/api/albums/1/photossign in to see5 per album
/photos/api/photosdocs50 (5,000)
/todos/api/todos30 (200)
/users/api/peopledocs20 (10), with address, geo and company
/users/1/posts/api/posts/user/101User ids start at 101

Paths are case-insensitive: /api/posts and /api/Posts are the same. There are fewer records than on JSONPlaceholder, but each one has more fields. Posts have tags, likes and a publish date, todos have a priority and a due date, and people have a nested address and company. The API documentation lists 13 more resource types, from products and carts to books, movies and countries.

GET/api/posts/1

Bad input gets an error

JSONPlaceholder accepts any body, even an empty one. Here, a missing required field returns 400 with a message, so the error branch of your form has something real to show:

Terminal
$ curl -i -X POST https://jsonplaceholder.typicode.com/posts -H "Content-Type: application/json" -d '{}'
HTTP/1.1 201 Created

{ "id": 101 }

$ curl -i -X POST https://api.testingapis.com/api/Posts -H "Content-Type: application/json" -d '{"title":""}'
HTTP/1.1 400 Bad Request

{"status":400,"error":"Bad Request","message":"Missing required field: title"}

Errors and slow responses on demand

Add ?error=500 to any request to make it fail with that status, or ?delay=3 to answer three seconds late. Together, ?delay=2&error=503 shows a loading state followed by an error. The error responses guide covers timeouts, retries and rate limits.

GET/api/todos/1?error=503

Filtering, sorting and paging

Both APIs filter lists by field, as in ?userId=101, and both sort, search and page. The parameter names differ:

JSONPlaceholdertestingapis.com
Filter?userId=1?userId=101
Page?_page=2&_limit=10?page=2&limit=10
Sort?_sort=id&_order=desc?sort=id&order=desc
Search?q=word?q=word
Total countX-Total-CountX-Total-Count, X-Total-Pages

Switching an app over

Reads return the record directly, as JSONPlaceholder does. Writes wrap it with a message, as { message, data }, and user ids start at 101. Those are the two changes most apps need:

JavaScript
// Before
const BASE = "https://jsonplaceholder.typicode.com";
const post = await (await fetch(`${BASE}/posts/1`)).json();

const created = await (await fetch(`${BASE}/posts`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ userId: 1, title: "Hi", body: "There" }),
})).json();

// After
const BASE = "https://api.testingapis.com/api";
const post = await (await fetch(`${BASE}/posts/1`)).json();

const { data: created } = await (await fetch(`${BASE}/posts`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ userId: 101, title: "Hi", body: "There" }),
})).json();
This example uses endpoints for signed-in members.Sign inCreate a free account

When JSONPlaceholder is the better choice

  • You need large lists, such as 5,000 photos to test virtual scrolling.
  • You're following a tutorial that uses JSONPlaceholder's exact ids and fields.
  • You only read data, and never need a write to show up afterwards.

To go further, the CRUD testing lesson walks through checking that each create, update and delete really happened.