JSON Placeholder API: Free Fake REST API for Testing
Key Takeaways
- ▸JSONPlaceholder (
jsonplaceholder.typicode.com) handles 3 billion requests per month. It is the go-to fake REST API for tutorials, demos, learning HTTP concepts, and prototyping — no API key, no account, no rate limits. - ▸Six resource endpoints:
/posts(100),/comments(500),/albums(100),/photos(5,000),/todos(200),/users(10). All support GET, POST, PUT, PATCH, and DELETE. - ▸Critical caveat: POST, PUT, PATCH, and DELETE operations are simulated — they return a realistic response but nothing is actually stored. The data is read-only. Do not build anything real on top of JSONPlaceholder.
- ▸For persistent fake data, use json-server (npm, local) or DummyJSON (hosted, auth support). JSONPlaceholder is the right choice for learning and demos; those tools are better for active prototype development.
The Problem JSONPlaceholder Solves
You are building a React component that renders a list of posts. You need real HTTP calls — not hardcoded arrays — so the loading state works, the error handling is exercised, and the code structure matches production. But you do not want to spin up a backend for a prototype.
You need three things: a real server that returns real JSON, no authentication overhead, and data structured like what you will actually use. JSONPlaceholder provides all three in a single URL:
curl https://jsonplaceholder.typicode.com/posts/1
# Response in ~50ms:
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita..."
}That is the entire setup. No API key. No CORS configuration. No account. This is why JSONPlaceholder handles 3 billion requests per month — it is the path of least resistance when you need a working API endpoint in under 30 seconds.
All Endpoints at a Glance
JSONPlaceholder provides six resource types. Each supports full CRUD operations at both the collection level (/posts) and the individual resource level (/posts/1). Nested routes let you fetch related resources in a single call.
| Endpoint | Records | Description | Nested Route |
|---|---|---|---|
| /posts | 100 | Blog post objects with title, body, and userId | /posts/1/comments |
| /comments | 500 | Post comments with name, email, body, and postId | /posts/1/comments |
| /albums | 100 | Photo album objects with title and userId | /albums/1/photos |
| /photos | 5,000 | Photo objects with album ID, url, and thumbnailUrl | /albums/1/photos |
| /todos | 200 | Todo items with title, completed boolean, and userId | /users/1/todos |
| /users | 10 | User objects with name, email, address, phone, and company | /users/1/posts |
The total dataset: 6,110 records across 6 resource types. The /photos endpoint at 5,000 records is useful for testing pagination and virtualized list components. Filtering is available via query parameters: /posts?userId=1 returns only posts by user 1.
All HTTP Methods With curl Examples
JSONPlaceholder supports all five HTTP methods. Write operations (POST, PUT, PATCH, DELETE) return realistic responses but are not persisted — this is by design.
# GET a single resource
curl https://jsonplaceholder.typicode.com/posts/1
# GET all resources
curl https://jsonplaceholder.typicode.com/posts
# GET with filtering (query params)
curl "https://jsonplaceholder.typicode.com/posts?userId=1"
# GET nested resources (all comments for post 1)
curl https://jsonplaceholder.typicode.com/posts/1/comments
# POST (create) — returns the new resource with a fake ID
curl -s -X POST https://jsonplaceholder.typicode.com/posts -H "Content-Type: application/json" -d '{"title": "My New Post", "body": "Hello world", "userId": 1}'
# Response: { "id": 101, "title": "My New Post", "body": "Hello world", "userId": 1 }
# PUT (full replace)
curl -s -X PUT https://jsonplaceholder.typicode.com/posts/1 -H "Content-Type: application/json" -d '{"id": 1, "title": "Updated Title", "body": "Updated body", "userId": 1}'
# PATCH (partial update)
curl -s -X PATCH https://jsonplaceholder.typicode.com/posts/1 -H "Content-Type: application/json" -d '{"title": "Just the title changed"}'
# DELETE
curl -s -X DELETE https://jsonplaceholder.typicode.com/posts/1
# Response: {} (empty object — resource "deleted")Note on POST response IDs: the fake server always returns id 101 for a new post (the next ID after the 100 existing ones) regardless of what you submit. This is a common gotcha when testing create workflows — do not rely on the returned ID to actually retrieve the resource.
Using JSONPlaceholder with JavaScript and TypeScript
The Fetch API is the standard way to call JSONPlaceholder in modern JavaScript. For TypeScript projects, define interfaces for the resource types — JSONPlaceholder’s data shapes are stable and well-documented.
// TypeScript interfaces for JSONPlaceholder resources
interface Post {
userId: number
id: number
title: string
body: string
}
interface Comment {
postId: number
id: number
name: string
email: string
body: string
}
interface Todo {
userId: number
id: number
title: string
completed: boolean
}
interface User {
id: number
name: string
username: string
email: string
address: {
street: string
suite: string
city: string
zipcode: string
geo: { lat: string; lng: string }
}
phone: string
website: string
company: {
name: string
catchPhrase: string
bs: string
}
}
// Typed fetch utility
const BASE = 'https://jsonplaceholder.typicode.com'
async function get<T>(path: string): Promise<T> {
const res = await fetch(BASE + path)
if (!res.ok) throw new Error(`HTTP ${res.status}: ${path}`)
return res.json() as Promise<T>
}
// Usage examples
const post = await get<Post>('/posts/1')
const posts = await get<Post[]>('/posts?userId=1')
const comments = await get<Comment[]>('/posts/1/comments')
const todos = await get<Todo[]>('/users/1/todos')
const user = await get<User>('/users/1')
// POST with TypeScript
async function createPost(data: Omit<Post, 'id'>): Promise<Post> {
const res = await fetch(BASE + '/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!res.ok) throw new Error(`Failed to create post: HTTP ${res.status}`)
return res.json()
}
const newPost = await createPost({
title: 'Test Post',
body: 'This is a test',
userId: 1,
})
console.log(newPost.id) // 101 (always, not persisted)JSONPlaceholder in React: Two Approaches
Most tutorials show JSONPlaceholder with a raw useEffect + fetch pattern. That is fine for understanding how data fetching works mechanically, but the production approach uses TanStack Query (React Query) which adds caching, deduplication, and automatic refetching. Both patterns are shown below so you understand the difference.
// Approach 1: Raw useEffect (educational — shows the mechanics)
import { useState, useEffect } from 'react'
interface Post { userId: number; id: number; title: string; body: string }
function PostListBasic() {
const [posts, setPosts] = useState<Post[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let cancelled = false // prevent state update on unmounted component
fetch('https://jsonplaceholder.typicode.com/posts?_limit=10')
.then((res) => {
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
})
.then((data: Post[]) => {
if (!cancelled) {
setPosts(data)
setLoading(false)
}
})
.catch((err: Error) => {
if (!cancelled) {
setError(err.message)
setLoading(false)
}
})
return () => { cancelled = true }
}, [])
if (loading) return <p>Loading...</p>
if (error) return <p>Error: {error}</p>
return (
<ul>
{posts.map((post) => (
<li key={post.id}><strong>{post.title}</strong></li>
))}
</ul>
)
}
// Approach 2: TanStack Query (production approach)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
function PostListProduction() {
const { data: posts, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: () =>
fetch('https://jsonplaceholder.typicode.com/posts?_limit=10')
.then((res) => {
if (!res.ok) throw new Error('Failed to fetch')
return res.json() as Promise<Post[]>
}),
staleTime: 5 * 60 * 1000, // 5 minutes — no refetch if data is fresh
})
const queryClient = useQueryClient()
const deleteMutation = useMutation({
mutationFn: (postId: number) =>
fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`, {
method: 'DELETE',
}).then((res) => {
if (!res.ok) throw new Error('Delete failed')
// Note: JSONPlaceholder doesn't actually delete — this is for demo only
}),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
})
if (isLoading) return <p>Loading...</p>
if (error) return <p>Error: {(error as Error).message}</p>
return (
<ul>
{posts?.map((post) => (
<li key={post.id} className="flex justify-between items-center">
<strong>{post.title}</strong>
<button onClick={() => deleteMutation.mutate(post.id)}>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</button>
</li>
))}
</ul>
)
}The Approach 1 code has three bugs that are easy to introduce: missing the cancelled cleanup (state update on unmounted component), not handling non-2xx responses, and calling setLoading in both the success and error paths instead of using a finally block. Approach 2 avoids all three by delegating lifecycle management to TanStack Query. This is why the tutorial pattern is fine for learning and the library pattern is required for production.
Using JSONPlaceholder with Python
For backend scripting, test fixtures, or learning HTTP with Python, JSONPlaceholder works equally well. The requests library is the standard for synchronous HTTP in Python; httpx is the modern alternative with async support.
import requests
from typing import TypedDict
BASE = "https://jsonplaceholder.typicode.com"
class Post(TypedDict):
userId: int
id: int
title: str
body: str
# GET a list of posts
def get_posts(user_id: int | None = None) -> list[Post]:
params = {"userId": user_id} if user_id else {}
response = requests.get(f"{BASE}/posts", params=params, timeout=10)
response.raise_for_status()
return response.json()
# GET a single post
def get_post(post_id: int) -> Post:
response = requests.get(f"{BASE}/posts/{post_id}", timeout=10)
response.raise_for_status()
return response.json()
# POST (create) — fake, not persisted
def create_post(title: str, body: str, user_id: int) -> Post:
payload = {"title": title, "body": body, "userId": user_id}
response = requests.post(
f"{BASE}/posts",
json=payload, # sets Content-Type: application/json automatically
timeout=10,
)
response.raise_for_status()
return response.json()
# PATCH (partial update)
def update_post_title(post_id: int, title: str) -> Post:
response = requests.patch(
f"{BASE}/posts/{post_id}",
json={"title": title},
timeout=10,
)
response.raise_for_status()
return response.json()
# Usage
posts = get_posts(user_id=1)
print(f"User 1 has {len(posts)} posts")
new_post = create_post("Test", "Hello from Python", user_id=1)
print(f"Created post with id: {new_post['id']}") # always 101
# Async version with httpx (for FastAPI or asyncio contexts)
import httpx
import asyncio
async def get_todos_async(user_id: int) -> list[dict]:
async with httpx.AsyncClient(base_url=BASE) as client:
response = await client.get(f"/users/{user_id}/todos", timeout=10)
response.raise_for_status()
return response.json()
todos = asyncio.run(get_todos_async(1))
completed = [t for t in todos if t["completed"]]
print(f"User 1 completed {len(completed)}/{len(todos)} todos")What JSONPlaceholder Cannot Do (Important Limitations)
JSONPlaceholder is excellent within its intended scope. Outside that scope, you will hit walls quickly:
No data persistence
POST/PUT/PATCH/DELETE return realistic responses but store nothing. A POST to /posts always returns id: 101 regardless of previous calls. You cannot use JSONPlaceholder to test workflows that depend on reading back data you just wrote.
No authentication
There are no login endpoints, no tokens, no API keys. If you are building authentication flows (login, protected routes, token refresh), use Reqres or DummyJSON which have auth endpoints.
No custom schemas or data types
You get 6 fixed resource types. If your project needs products, orders, events, or any domain-specific data, JSONPlaceholder cannot help — use DummyJSON (products, recipes, carts) or json-server with a custom db.json file.
No file uploads or binary data
JSONPlaceholder is JSON-only. It cannot accept multipart form data, binary uploads, or serve actual images (the photo URLs in /photos point to real placeholder image services, not JSONPlaceholder itself).
No SLA or uptime guarantee
JSONPlaceholder is a free community service. It is highly reliable in practice but has no SLA. Do not use it in production, automated test suites that run in CI, or demos where downtime would be embarrassing.
JSONPlaceholder vs Alternatives
The choice depends on what you are building. JSONPlaceholder is the fastest to start with; the alternatives add capabilities at the cost of setup overhead:
| Tool | Persistence | Auth | Custom Schema | Best For |
|---|---|---|---|---|
| JSONPlaceholder | None (fake) | None | Fixed 6 types | Tutorials, demos, learning HTTP, quick prototypes |
| DummyJSON | Session only | Yes (login, tokens) | ~20 preset types | Auth testing, e-commerce prototypes, richer data |
| Reqres | None (fake) | Yes (login, register) | Fixed (users only) | Auth flow testing, login/register UI development |
| json-server | Full (local file) | Manual setup | Any (db.json) | Offline dev, custom data, local CRUD testing |
| MockAPI.io | Cloud (free tier) | Basic | Custom schemas | Collaborative prototyping, longer-running demos |
Decision guide: Use JSONPlaceholder if you are learning HTTP, building a tutorial, writing a README example, or testing a data-fetching component in under 5 minutes. Switch to json-server (local) if you need persistence during active development. Switch to DummyJSON if you need auth endpoints or more realistic domain data. Use MockAPI if you need a cloud-hosted API with custom schemas that persists across sessions.
Advanced Usage Patterns
Pagination and Filtering
JSONPlaceholder supports query parameters for basic filtering and pagination via _limit, _start, and field-value filters:
// Pagination parameters (powered by json-server under the hood)
const BASE = 'https://jsonplaceholder.typicode.com'
// Get page 2 of posts (10 per page)
await fetch(`${BASE}/posts?_start=10&_limit=10`)
// Filter by field value
await fetch(`${BASE}/posts?userId=1`) // posts by user 1
await fetch(`${BASE}/todos?completed=false`) // incomplete todos
await fetch(`${BASE}/todos?userId=1&completed=true`) // user 1's done todos
// Get specific fields only (_limit works too)
await fetch(`${BASE}/posts?_limit=5`) // first 5 posts
// Nested resource access
await fetch(`${BASE}/posts/1/comments`) // comments for post 1
await fetch(`${BASE}/users/1/posts`) // posts by user 1
await fetch(`${BASE}/users/1/todos`) // todos for user 1
await fetch(`${BASE}/users/1/albums`) // albums by user 1
await fetch(`${BASE}/albums/1/photos`) // photos in album 1
// Useful for testing infinite scroll / virtual lists
async function loadPhotosPage(page: number, limit = 20) {
const start = (page - 1) * limit
const res = await fetch(`${BASE}/photos?_start=${start}&_limit=${limit}`)
if (!res.ok) throw new Error('Failed to load photos')
return res.json()
}
const page1 = await loadPhotosPage(1) // photos 1-20
const page2 = await loadPhotosPage(2) // photos 21-40Using with axios
If your codebase uses axios (common in older React and Vue codebases), JSONPlaceholder works identically:
import axios from 'axios'
// Create a configured instance (good practice in real apps too)
const api = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com',
timeout: 10_000,
headers: { 'Content-Type': 'application/json' },
})
// GET with automatic JSON parsing (no .json() needed)
const { data: posts } = await api.get<Post[]>('/posts', {
params: { userId: 1, _limit: 10 },
})
// POST
const { data: newPost } = await api.post<Post>('/posts', {
title: 'My Post',
body: 'Content here',
userId: 1,
})
// axios error handling: non-2xx throws AxiosError
try {
await api.get('/posts/999') // 404 — throws
} catch (err) {
if (axios.isAxiosError(err)) {
console.error(`HTTP ${err.response?.status}`) // 404
}
}Self-Hosting with json-server
JSONPlaceholder is built on json-server, a zero-configuration REST API from a JSON file. If you need real persistence, custom data, or offline development, json-server gives you the same API with full CRUD semantics:
# Install json-server
npm install -g json-server
# Create a db.json file with your data
cat > db.json << 'EOF'
{
"products": [
{ "id": 1, "name": "Widget A", "price": 29.99, "inStock": true },
{ "id": 2, "name": "Widget B", "price": 49.99, "inStock": false }
],
"orders": [
{ "id": 1, "productId": 1, "quantity": 3, "userId": 1 }
],
"users": [
{ "id": 1, "name": "Alice", "email": "[email protected]" }
]
}
EOF
# Start the server (default port 3001, watches for db.json changes)
json-server --watch db.json --port 3001
# Now you have a FULL REST API with real persistence:
# GET /products → all products
# GET /products/1 → product 1
# POST /products → create (adds to db.json)
# PUT /products/1 → full replace (updates db.json)
# PATCH /products/1 → partial update (updates db.json)
# DELETE /products/1 → delete (removes from db.json)
# GET /products?inStock=true → filter
# GET /products?_limit=10&_page=1 → pagination
# In package.json scripts for a project:
# "mock-api": "json-server --watch src/mock/db.json --port 3001"For understanding the REST principles that make APIs like JSONPlaceholder work, our guide to building a REST API covers HTTP semantics, status codes, and the design decisions behind RESTful resource URLs.
Frequently Asked Questions
What is JSONPlaceholder?▾
Does JSONPlaceholder actually save data?▾
Is JSONPlaceholder free to use?▾
What endpoints does JSONPlaceholder have?▾
What are the best alternatives to JSONPlaceholder?▾
Can I use JSONPlaceholder in production?▾
Working with JSON? BytePane Has You Covered
When you are exploring JSONPlaceholder responses or building JSON-heavy applications, these tools help you inspect, validate, and transform data instantly:
- JSON Formatter & Validator — pretty-print and validate any JSON response from JSONPlaceholder or your API
- JSON Minifier — minify JSON payloads before benchmarking request sizes
- Base64 Encoder/Decoder — decode JWT tokens returned by auth APIs like DummyJSON or Reqres
- Epoch Converter — convert Unix timestamps found in API response metadata
Related Articles
Free Public APIs 2026
100+ free APIs for your next project — weather, government data, AI inference, finance, and developer utilities with rate limits and auth info.
How to Build a REST API
Build a production-ready REST API with Node.js and Express — covering the same HTTP semantics that JSONPlaceholder is designed to teach.
What Is JSON?
JSON syntax, the 6 data types, RFC 8259 spec, parsing in JavaScript/Python/Go, and performance comparisons with MessagePack and Protobuf.
HTTP Methods Explained
GET, POST, PUT, PATCH, DELETE — what safe and idempotent mean, correct status codes per method, and the design mistakes that break API clients.