BytePane

JSON Placeholder API: Free Fake REST API for Testing

API17 min read

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.

EndpointRecordsDescriptionNested Route
/posts100Blog post objects with title, body, and userId/posts/1/comments
/comments500Post comments with name, email, body, and postId/posts/1/comments
/albums100Photo album objects with title and userId/albums/1/photos
/photos5,000Photo objects with album ID, url, and thumbnailUrl/albums/1/photos
/todos200Todo items with title, completed boolean, and userId/users/1/todos
/users10User 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:

ToolPersistenceAuthCustom SchemaBest For
JSONPlaceholderNone (fake)NoneFixed 6 typesTutorials, demos, learning HTTP, quick prototypes
DummyJSONSession onlyYes (login, tokens)~20 preset typesAuth testing, e-commerce prototypes, richer data
ReqresNone (fake)Yes (login, register)Fixed (users only)Auth flow testing, login/register UI development
json-serverFull (local file)Manual setupAny (db.json)Offline dev, custom data, local CRUD testing
MockAPI.ioCloud (free tier)BasicCustom schemasCollaborative 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-40

Using 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?
JSONPlaceholder is a free online fake REST API hosted at jsonplaceholder.typicode.com. It provides 6 pre-populated resource endpoints (posts, comments, albums, photos, todos, users) with realistic JSON data. No authentication or API key required. Handles over 3 billion requests per month and is widely used for prototyping, learning HTTP, and README demonstrations.
Does JSONPlaceholder actually save data?
No. POST, PUT, PATCH, and DELETE return realistic responses but persist nothing. A POST to /posts always returns id: 101. The created resource will not appear in subsequent GET requests. This is by design — use json-server locally or MockAPI for actual persistence.
Is JSONPlaceholder free to use?
Yes, completely free. No API keys, no accounts, no rate limits, no usage restrictions for development, testing, or demonstrations. It is an open-source project by typicode; the source code is on GitHub and can be self-hosted using json-server.
What endpoints does JSONPlaceholder have?
Six resource endpoints: /posts (100 records), /comments (500), /albums (100), /photos (5,000), /todos (200), and /users (10). All support nested routes — /posts/1/comments returns all comments for post 1. All HTTP methods (GET, POST, PUT, PATCH, DELETE) are supported.
What are the best alternatives to JSONPlaceholder?
DummyJSON (dummyjson.com) for richer data types and real auth endpoints; Reqres (reqres.in) specifically for authentication flow testing; json-server (npm) for local development with full persistence and custom schemas; MockAPI.io for cloud-hosted fake APIs with persistent custom data.
Can I use JSONPlaceholder in production?
No. JSONPlaceholder has no SLA, no uptime guarantee, returns fake data, and persists nothing. Use it for testing and demonstrations only. For production, use a real backend API.

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:

Browse All Developer Tools

Related Articles