json-server turns any JSON file into a full fake REST API in seconds. Point it at a file like db.json, and every top-level key becomes an endpoint that supports GET, POST, PUT, PATCH, and DELETE. It is the canonical zero-config mock backend for frontend developers, integration tests, demos, prototypes, and offline development.
This guide covers the 1.0.0-beta.15 release: a TypeScript rewrite built on @tinyhttp, lowdb, and Node's native HTTP. It runs as a single command, requires no global install, and persists every change you make through the API directly to the JSON file on disk.
1. Quick Start
The fastest possible path: one command, one file, a working REST API.
npx json-server db.json
npx downloads and runs json-server for you the first time. No global install, no package.json, no setup. As long as Node.js 22.12 or newer is on your machine, this works.
Tip
If you don't have a
db.jsonyet, create one in the same directory:{ "posts": [ { "id": "1", "title": "Hello world", "views": 100 } ], "comments": [ { "id": "1", "text": "Nice post", "postId": "1" } ], "profile": { "name": "Alice" } }The file is required —
json-serverwill not start without one. An empty file is fine; it is initialized to{}on first run.
You'll see output like:
JSON Server started on PORT :3000
Press CTRL-C to stop
Watching db.json...
♡⸜(˶˃ ᵕ ˂˶)⸝♡
Index:
http://localhost:3000/
Endpoints:
http://localhost:3000/posts
http://localhost:3000/comments
http://localhost:3000/profile
Open another terminal and exercise the API:
curl http://localhost:3000/posts
[
{ "id": "1", "title": "Hello world", "views": 100 }
]
curl http://localhost:3000/posts/1
{ "id": "1", "title": "Hello world", "views": 100 }
Create a record:
curl -X POST http://localhost:3000/posts \
-H "Content-Type: application/json" \
-d '{"title":"Second post","views":0}'
{ "title": "Second post", "views": 0, "id": "h7Ks93Lq2nE" }
That row is now persisted in db.json on disk. Stop the server, reopen the file, and you'll see the new post.
Note
The server auto-watches
db.jsonwhile running (in non-production mode). Edit the file in your editor, save it, and the API picks up the change instantly — no restart needed.
What you can do next
- Mock a paginated list (
?_page=1&_per_page=10) — see Section 5 - Filter records (
?title:eq=hello,?views:gt=100) — see Section 4 - Embed related records (
?_embed=comments) — see Section 4 - Move beyond port 3000 (
-p 8080) — see Section 3
2. Installation
json-server ships as a single npm package. Pick whichever method fits how your project is set up.
Method 1: One-shot via npx (recommended)
npx json-server db.json
This is the path the project tutorial leads with, and it's right for almost every use. npx fetches the package, runs it once against your db.json, and exits when you stop the server. No files added to your project, no global pollution.
Tip
The first
npxinvocation downloads and caches the package (a few seconds). Subsequent runs from the cache are near-instant.
Method 2: Global install
npm install -g json-server
json-server db.json
Useful if you reach for json-server constantly across many projects and prefer a permanent json-server command on your PATH.
Important
Global installs require Node.js 22.12.0 or newer. The
engines.nodefield in the package'spackage.jsonenforces this — older Node versions emit anEBADENGINEwarning and may not run at all. Check withnode --versionbefore installing.
Method 3: Project-local dependency
When json-server is part of your dev workflow (running tests, simulating an API in CI), add it as a devDependency:
npm install --save-dev json-server
Then call it from a script in your package.json:
{
"scripts": {
"mock": "json-server db.json"
}
}
npm run mock
This is the right method for a team — every developer gets the same version pinned in package-lock.json.
Method 4: Build from source
For contributors or anyone who wants to run a development checkout:
git clone https://github.com/typicode/json-server.git
cd json-server
npm install
npm run dev -- fixtures/db.json
The dev script runs the TypeScript source directly via node --watch --experimental-strip-types src/bin.ts. To produce the published JavaScript:
npm run build # rm -rf lib && tsc
node lib/bin.js fixtures/db.json
System requirements
| Requirement | Value | Notes |
|---|---|---|
| Node.js | >=22.12.0 |
Enforced by package.json engines.node. |
| OS | macOS, Linux, Windows | Pure JavaScript — no native bindings. |
| Disk | < 30 MB | Most of that is node_modules. |
| Memory | Modest | The whole DB is held in memory; size your DB accordingly. |
| Network | None at runtime | Only needed for the initial install. |
Verifying the install
json-server --version
1.0.0-beta.15
json-server --help
Usage: json-server [options] <file>
Options:
-p, --port <port> Port (default: 3000)
-h, --host <host> Host (default: localhost)
-s, --static <dir> Static files directory (multiple allowed)
--help Show this message
--version Show version number
Warning
The current
1.0.0-beta.15line is a rewrite. Several heavyweight features from0.x(custom routes, middleware files,--delay, the--routesoption) are not present in this beta. If you depend on those, pin to0.17.4from the0.xline until they reappear. See the FAQ for what's missing.
3. Configuration
json-server is intentionally minimal. There is no config file — everything is set via CLI flags or environment variables, and almost everything has a sensible default.
Command-line flags
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--port <port> |
-p |
number | 3000 (or $PORT) |
TCP port to listen on. |
--host <host> |
-h |
string | localhost (or $HOST) |
Host/interface to bind. |
--static <dir> |
-s |
string (repeatable) | [] |
Extra directory of static files to serve. May be passed multiple times. |
--help |
— | boolean | — | Print help and exit. |
--version |
— | boolean | — | Print version and exit. |
--watch |
-w |
boolean | — | Deprecated and ignored — file watching is on by default. Prints a yellow warning if used. |
Note
The positional argument (
<file>) is required. It must be a.jsonor.json5file. If the file doesn't exist, the server exits withFile <name> not found. If it exists but is empty, it is initialized with{}on startup.
Environment variables
| Variable | Purpose | Default |
|---|---|---|
PORT |
Default for -p. CLI -p wins if both are set. |
3000 |
HOST |
Default for -h. CLI -h wins if both are set. |
localhost |
NODE_ENV |
When set to production, disables file watching, enables view caching, and disables sirv dev mode for static files. |
(unset) |
PORT=8080 HOST=0.0.0.0 npx json-server db.json
Common option combinations
| Goal | Command |
|---|---|
| Default everything | npx json-server db.json |
| Custom port | npx json-server -p 8080 db.json |
| Bind to all interfaces (LAN) | npx json-server -h 0.0.0.0 db.json |
| Serve a custom static dir alongside the API | npx json-server -s ./assets db.json |
| Two static dirs | npx json-server -s ./public2 -s ./images db.json |
| Production-mode (no file watch, view caching) | NODE_ENV=production npx json-server db.json |
| JSON5 source file | npx json-server db.json5 |
Tip
See the Admin Guide's Configuration Reference for full details on every flag, environment variable, the JSON Schema enforced on
db.json, and the production-mode behavior changes.
The database file
Your JSON file IS your configuration of resources. Each top-level key becomes a route:
{
"posts": [ { "id": "1", "title": "..." } ],
"comments": [ { "id": "1", "postId": "1" } ],
"profile": { "name": "Alice" }
}
This produces:
| Key | Type | Routes generated |
|---|---|---|
posts |
array | /posts, /posts/:id (full CRUD) |
comments |
array | /comments, /comments/:id (full CRUD) |
profile |
object | /profile (singleton — only GET, PUT, PATCH) |
Important
Items in arrays must have an
idfield (string). On read, numericids are automatically coerced to strings, and items missing anidget a random base64url ID generated at load time and written back to the file. This is enforced by the normalizing adapter on every read.
json-server also auto-injects a $schema pointer into your file on every write, pointing at ./node_modules/json-server/schema.json. This lets editors with JSON Schema support autocomplete and validate the file shape. It is stripped on read so it never appears in API responses.
{
"$schema": "./node_modules/json-server/schema.json",
"posts": [ { "id": "1" } ]
}
JSON5 support
If your file ends in .json5, comments, trailing commas, and unquoted keys are parsed correctly — both on read and on write.
{
// posts table
posts: [
{ id: '1', title: 'Hello' },
],
}
npx json-server db.json5
4. Core Usage
Every top-level key in your JSON file becomes an HTTP resource. The behavior depends on whether the value is an array (collection) or a non-array object (singleton).
The HTTP surface
| Method | Path | Behavior on collection (posts: [...]) |
Behavior on singleton (profile: {...}) |
|---|---|---|---|
GET |
/:name |
Returns array, supports filter/sort/page/embed | Returns the object |
GET |
/:name/:id |
Returns one item or 404 |
404 (no :id on singletons) |
POST |
/:name |
Creates a new item, returns 201 with generated id |
404 |
PUT |
/:name |
404 (use :id form) |
Replaces the singleton, returns it |
PUT |
/:name/:id |
Replaces item by id (preserving id) |
404 |
PATCH |
/:name |
404 (use :id form) |
Merges into the singleton |
PATCH |
/:name/:id |
Merges fields into item by id | 404 |
DELETE |
/:name/:id |
Deletes item, nullifies foreign keys, optionally cascades | 404 |
OPTIONS |
* |
CORS pre-flight, returns 204 |
Same |
Note
Asking for a key that doesn't exist in the file returns
{"error": "Not Found"}with status404. POSTing or PATCHing a body that isn't a JSON object returns{"error": "Body must be a JSON object"}with status400.
Reading data
List a collection
curl http://localhost:3000/posts
[
{ "id": "1", "title": "a", "views": 100 },
{ "id": "2", "title": "b", "views": 200 }
]
Get one item
curl http://localhost:3000/posts/1
{ "id": "1", "title": "a", "views": 100 }
A non-existent :id returns 404:
curl -i http://localhost:3000/posts/-1
HTTP/1.1 404 Not Found
Content-Type: application/json
{"error":"Not Found"}
Filtering with where operators
Pass query parameters to filter a collection. The format is field:operator=value (preferred) or the legacy field_operator=value (kept for 0.17 compatibility). A bare field=value is treated as field:eq=value.
| Operator | Meaning | Example | Matches |
|---|---|---|---|
eq |
equals (default) | ?title:eq=hello or ?title=hello |
title === "hello" |
ne |
not equals | ?title:ne=hello |
title !== "hello" |
lt |
less than | ?views:lt=100 |
views < 100 |
lte |
less than or equal | ?views:lte=100 |
views <= 100 |
gt |
greater than | ?views:gt=100 |
views > 100 |
gte |
greater than or equal | ?views:gte=100 |
views >= 100 |
in |
in list (comma-separated) | ?id:in=1,2,3 |
id in ["1","2","3"] |
contains |
string includes (case-insensitive) | ?title:contains=ello |
title contains "ello" |
startsWith |
string starts with (case-insensitive) | ?title:startsWith=hel |
title starts with "hel" |
endsWith |
string ends with (case-insensitive) | ?title:endsWith=lo |
title ends with "lo" |
Values are auto-coerced: true/false become booleans, null becomes null, anything that parses as a finite number becomes a number, otherwise it stays a string.
Combine filters by repeating params:
curl 'http://localhost:3000/posts?views:gt=100&published:eq=true'
Filter on nested fields with dot notation:
curl 'http://localhost:3000/posts?author.name:eq=Alice'
For OR logic across predicates, use _where with a JSON object (see below).
Tip
For complex filters, the
_whereJSON-query parameter is much cleaner. Pass a URL-encoded JSON object and it overrides any other filter params:?_where={"author":{"name":{"eq":"Alice"}},"views":{"gt":100}}
Sorting
curl 'http://localhost:3000/posts?_sort=views' # ascending
curl 'http://localhost:3000/posts?_sort=-views' # descending
curl 'http://localhost:3000/posts?_sort=views,-title' # multi-key
The _sort parameter accepts a comma-separated list of fields; prefix with - for descending.
Pagination
curl 'http://localhost:3000/posts?_page=1&_per_page=10'
{
"first": 1,
"prev": null,
"next": 2,
"last": 3,
"pages": 3,
"items": 25,
"data": [
{ "id": "1", "title": "..." }
]
}
The response wraps the page slice in a navigation object: first, prev, next, last, total pages, total items, and the page slice in data.
| Param | Default | Notes |
|---|---|---|
_page |
(unset = no pagination) | Page number, 1-indexed. Clamped between 1 and last. |
_per_page |
10 (when _page is set) |
Per-page count. Clamped to a minimum of 1. |
Note
Pagination is opt-in: a request without
_pagereturns the full filtered array, not a wrapped object. Pass_page=1(with or without_per_page) to get the paginated shape.
Embedding related records
json-server infers relationships from foreign-key naming. If comments items have a postId field, then:
curl 'http://localhost:3000/posts/1?_embed=comments'
{
"id": "1",
"title": "a",
"comments": [
{ "id": "1", "postId": "1", "text": "Nice" }
]
}
And the reverse direction (singular form embeds the parent):
curl 'http://localhost:3000/comments/1?_embed=post'
{
"id": "1",
"postId": "1",
"post": { "id": "1", "title": "a" }
}
The rule:
_embed=children(plural) — finds items inchildrenwhose<singular_parent>Idequals this item'sid._embed=parent(singular) — finds the item in theparentsresource whoseidmatches this item'sparentId.
_embed may be repeated (?_embed=comments&_embed=author) to embed multiple relations.
Writing data
Create
curl -X POST http://localhost:3000/posts \
-H "Content-Type: application/json" \
-d '{"title":"New post","views":0}'
{ "title": "New post", "views": 0, "id": "Hk7Lq3nW" }
A new random id is always assigned — any id you submit is overwritten (preventing client-side id forgery). Status 201 Created on success.
Replace by id (PUT)
curl -X PUT http://localhost:3000/posts/1 \
-H "Content-Type: application/json" \
-d '{"title":"Replaced","views":0}'
The whole record is replaced, but the original id is preserved.
Patch by id (PATCH)
curl -X PATCH http://localhost:3000/posts/1 \
-H "Content-Type: application/json" \
-d '{"views":999}'
Merges the request body into the existing record, leaving unspecified fields untouched.
Delete by id
curl -X DELETE http://localhost:3000/posts/1
Returns the deleted item, or 404 if it doesn't exist. Side effect: any other items in the database that have a foreign key pointing to this id (e.g. comments with postId: "1") get that key set to null.
To cascade-delete dependents instead of nullifying:
curl -X DELETE 'http://localhost:3000/posts/1?_dependent=comments&_dependent=tags'
Each _dependent names a resource whose foreign-key-bearing rows should be deleted along with the post.
Warning
Every successful write (
POST/PUT/PATCH/DELETE) flushes the in-memory database back to your JSON file on disk. There is no undo. Back updb.jsonbefore running destructive scripts.
Error responses
| Status | Body | When |
|---|---|---|
200 OK |
The data | Successful read or update |
201 Created |
The new item | Successful POST to a collection |
204 No Content |
empty | CORS pre-flight (OPTIONS) |
400 Bad Request |
{"error":"Body must be a JSON object"} |
POST/PUT/PATCH with a non-object body (array, string, null) |
404 Not Found |
{"error":"Not Found"} |
Unknown resource, missing id, or operation not supported on that resource shape |
Static files & the home page
While json-server is running:
http://localhost:3000/renders an HTML index page (the bundledviews/index.htmlEta template) listing your endpoints with a friendly preview.http://localhost:3000/<file>serves anything in./public/(auto), and any directories you passed via-s.
mkdir public
echo "<h1>Hi</h1>" > public/test.html
npx json-server db.json
Then curl http://localhost:3000/test.html returns the HTML.
Tip
Use
-sto point at your frontend'sdist/folder during development —json-serverserves your built SPA on/and the API on/<resource>from the same origin (no CORS pain).
5. Common Tasks
Real-world recipes. Each one is self-contained — pick the one that matches your problem.
Task 1: Stand up a fake REST API for a frontend in 60 seconds
Goal: Your React/Vue/Svelte/Solid app needs a backend, and the real one isn't ready yet.
Steps:
-
Drop a
db.jsonnext to your frontend project:{ "users": [ { "id": "1", "name": "Alice", "email": "[email removed]" } ], "todos": [ { "id": "1", "userId": "1", "title": "Buy milk", "done": false } ] } -
Start the server bound to all interfaces and a non-conflicting port:
npx json-server -h 0.0.0.0 -p 4000 db.json -
In your frontend, point your fetch base URL at
http://localhost:4000. -
Develop. Every
POST/PATCH/DELETEwrites through todb.jsonso your data is sticky across server restarts.
Tip
Add
db.jsonto.gitignoreif it accumulates test data, or check it in if it's seed data your team needs.
Task 2: Mock a paginated list
Goal: You're building an infinite-scroll component and need realistic page metadata.
Seed enough rows:
{
"posts": [
{ "id": "1", "title": "Post 1" },
{ "id": "2", "title": "Post 2" },
{ "id": "3", "title": "Post 3" }
]
}
Request page 1 with 2 items per page:
curl 'http://localhost:3000/posts?_page=1&_per_page=2'
{
"first": 1, "prev": null, "next": 2, "last": 2,
"pages": 2, "items": 3,
"data": [ { "id":"1","title":"Post 1" }, { "id":"2","title":"Post 2" } ]
}
Use next to drive infinite scroll. When next is null, you've hit the end.
Note
Pagination clamps gracefully:
_page=999with 3 items returns the last available page (clamped tolast);_per_page=0is clamped to1. You will never see a400from out-of-range pagination params.
Task 3: Filter and sort like a real backend
Goal: Demonstrate search-as-you-type or a sortable table without writing a query DSL.
# All published posts with views over 100, newest first
curl 'http://localhost:3000/posts?published:eq=true&views:gt=100&_sort=-createdAt'
# Posts whose title contains "react", page 1
curl 'http://localhost:3000/posts?title:contains=react&_page=1&_per_page=20'
# Posts by Alice OR Bob
curl 'http://localhost:3000/posts?author.name:in=Alice,Bob'
For complex predicates, build a JSON object on the client and send it as _where:
const where = {
or: [
{ author: { name: { eq: 'Alice' } } },
{ tags: { contains: 'featured' } }
],
views: { gt: 100 }
}
const url = `http://localhost:3000/posts?_where=${encodeURIComponent(JSON.stringify(where))}`
Task 4: Embed parent and child relationships
Goal: Your UI shows posts with their comments inline, or comments with their author inline.
Seed:
{
"posts": [ { "id": "1", "title": "Hello", "userId": "10" } ],
"comments": [ { "id": "1", "postId": "1", "text": "Nice" } ],
"users": [ { "id": "10", "name": "Alice" } ]
}
Embed children:
curl 'http://localhost:3000/posts/1?_embed=comments'
Embed a parent:
curl 'http://localhost:3000/comments/1?_embed=post'
Stack relations:
curl 'http://localhost:3000/posts?_embed=comments&_embed=user'
Note
Embed relies on naming convention: a child resource's foreign key must be
<singular_parent>Id.comments[*].postIdworks becauseinflection.singularize("posts") === "post". If your foreign keys don't match this pattern, embed silently returns the item unchanged.
Task 5: Persist changes through to a JSON5 file with comments
Goal: You want your fixture file to keep human-readable comments and trailing commas.
Name your file db.json5:
{
// Top-level posts table
posts: [
{ id: '1', title: 'Hello' },
],
// Singleton — site-wide profile
profile: { name: 'Alice' },
}
npx json-server db.json5
JSON5 syntax is preserved on writes. The adapter uses JSON5.parse and JSON5.stringify automatically when the extension is .json5.
Task 6: Serve a frontend bundle alongside the API
Goal: Run a single command that hosts both your built SPA and its mock API.
npx json-server -s ./dist db.json
http://localhost:3000/index.html(and any other static files) → served from./dist/http://localhost:3000/posts→ APIhttp://localhost:3000/→ unless you also have aviews/index.html, the bundled landing page renders
You can pass -s multiple times to layer directories:
npx json-server -s ./dist -s ./assets -s ./uploads db.json
Task 7: Cascade-delete or orphan related records
Goal: Decide what happens to comments when their parent post is deleted.
By default, DELETE /posts/1 nullifies foreign keys: comments whose postId was "1" now have postId: null. The comments still exist, just orphaned.
To delete those dependents instead:
curl -X DELETE 'http://localhost:3000/posts/1?_dependent=comments'
Multiple dependents:
curl -X DELETE 'http://localhost:3000/posts/1?_dependent=comments&_dependent=likes'
This is the closest thing to a ON DELETE CASCADE policy json-server offers.
Task 8: Run on LAN so a phone or another machine can reach it
Goal: Test your mobile UI against the mock API from a real device.
HOST=0.0.0.0 PORT=4000 npx json-server db.json
Then point your phone's browser (or build) at http://<your-laptop-ip>:4000. CORS is enabled for any Origin, so cross-origin requests Just Work.
Caution
Binding to
0.0.0.0exposes the API to anyone on your network. There is no auth — they can read and write yourdb.jsonfreely. Use it on trusted networks only, or keep the defaultlocalhost. See the Admin Guide's Security section for hardening notes.
6. Troubleshooting
When something goes wrong, work the list top to bottom — most issues are install or configuration drift.
Decision tree
Server won't start? -> Node version, file path, port conflict (below)
Server starts, returns 404? -> Resource name typo, plural/singular mismatch
Returns 400 on POST/PUT? -> Body shape: must be a JSON object, not array/string/null
Watcher doesn't pick up edits? -> NODE_ENV=production disables watch
Embed returns nothing? -> Foreign-key naming mismatch (must be `<singular>Id`)
Filter returns wrong data? -> Operator name typo (silent ignore — see #1731)
Error: File db.json not found
Cause: The positional argument points at a path that doesn't exist on disk.
Solution:
- Verify the path:
ls db.json. - If the file should be created from scratch,
echo '{}' > db.jsonfirst. - Re-run with the correct path.
Prevention: Always pass an absolute or working-directory-relative path. A bare filename is resolved against the current shell directory, not the user's home.
Error: EBADENGINE Unsupported engine ... required: { node: '>=22.12.0' }
Cause: The installed Node.js version is older than 22.12.
Solution:
node --version # check current
# upgrade via nvm:
nvm install 22
nvm use 22
Prevention: Pin Node version in your project's .nvmrc or package.json engines block.
Error: EADDRINUSE: address already in use :::3000
Cause: Another process holds port 3000.
Solution:
- Find the squatter:
lsof -i :3000(macOS/Linux) ornetstat -ano | findstr :3000(Windows). - Either kill it, or pick another port:
npx json-server -p 4000 db.json.
Prevention: Standardize one port per project in your npm run mock script.
Error: {"error": "Body must be a JSON object"}
Cause: You sent an array, string, null, or no body to POST/PUT/PATCH. The endpoint validates with the internal isItem() check, which only accepts non-array objects.
Solution:
# Wrong:
curl -X POST http://localhost:3000/posts -d '[{"title":"x"}]' ...
# Right:
curl -X POST http://localhost:3000/posts \
-H "Content-Type: application/json" \
-d '{"title":"x"}'
Prevention: Always send Content-Type: application/json and a single JSON object, never an array. To bulk-insert, loop client-side.
Error: {"error":"Not Found"} on a resource you can see in db.json
Cause: Resource name typo, or you're hitting /posts/1 when posts is a singleton object (no :id route on singletons), or vice versa.
Solution:
- Check the indexed routes printed at startup — that's the authoritative list.
- Confirm whether the resource is a collection (array, supports
:id) or a singleton (object, only top-level methods). - Check method/route compatibility —
PUT /:nameonly works on singletons;PUT /:name/:idonly on collections.
Prevention: Prefer plural names for collection resources (posts, comments). Embed inference depends on it.
File-watcher not picking up edits (issue #286, behavior in 1.0.0-beta.15)
Cause: NODE_ENV=production disables the chokidar file watcher entirely. Or your editor is writing to a temp file and renaming, which some watchers miss.
Solution:
echo $NODE_ENV
# (Should be empty.)
unset NODE_ENV
npx json-server db.json
Prevention: Don't set NODE_ENV=production in your shell profile. Set it only in CI / deployment, where it belongs.
--delay does nothing (issues #1558, #1545)
Cause: The --delay flag does not exist in 1.0.0-beta.15. It was a 0.x feature, removed during the rewrite. No rate-limit-simulation flags exist.
Solution: If you need latency simulation, run a small wrapper script. The simplest pattern: a Node http proxy in front of json-server that adds a setTimeout before forwarding. Or pin to 0.17.4 from the 0.x line.
Prevention: Read the --help output before relying on a flag from older docs.
Embed returns the item unchanged (issue #1634)
Cause: Foreign-key naming doesn't match the convention. _embed=comments looks for posts[*].id and comments[*].postId. If your column is named post_id or parent, embed gives up silently.
Solution:
- Rename the foreign key to
<singular_parent>Id(camelCase, capitalI). - Or post-process on the client.
Prevention: Stick to the xxxId naming convention from day one.
Auto-IDs that look numeric break exact-match queries (issue #1734)
Cause: Random IDs are 8 random bytes encoded as base64url. Occasionally that produces an all-digit string like "40123456". When you query with a numeric string, the value coercer turns it into a Number, and === against the stored string mismatches.
Solution: Be aware of the type when filtering — ?id:eq=40123456 compares a coerced Number against a stored string. Filtering by a different field (e.g. a slug) is more reliable.
Prevention: Use a string field that's known to be non-numeric (a slug, UUID, or human-readable key) as your stable lookup column.
Pagination at _per_page=0 (issue #1721)
Cause: _per_page=0 is clamped to 1 (not an error), which can be unexpected if you're relying on 0 for "no items".
Solution: Don't pass _per_page=0. Omit _page entirely if you want the unwrapped full array.
Prevention: Validate pagination params on the client before sending.
Watcher doesn't trigger when running in a dev container
Cause: Some bind-mount filesystems (Docker on macOS, WSL2) don't propagate inotify events.
Solution: Edit the file from inside the container, or run json-server outside the container and expose its port.
Prevention: For containerized workflows, prefer running json-server outside the container.
7. FAQ
General
Q: What versions does this guide cover?
1.0.0-beta.15 of typicode/json-server (commit 89a34a4). This is the post-rewrite line. Behavior here may differ substantially from 0.17.x and earlier — those versions had custom routes, middleware, --delay, and other features that aren't in the beta yet.
Q: Does json-server need a database (Postgres, MySQL, etc.)?
No. The "database" is your JSON file, loaded into memory at startup via lowdb. Every write flushes back to disk through steno (atomic-ish writes).
Q: How big can my db.json get before performance suffers?
Practical limit: tens of MB. The whole file lives in memory, every write rewrites it, and filtering is a linear scan. For demos and prototyping this is invisible. For production-volume datasets it is the wrong tool.
Routing & shape
Q: Why does POST /posts/1 return 404?
POST only exists on collections (POST /:name), never on individual items. Use PATCH /posts/1 to merge fields, or PUT /posts/1 to replace the record.
Q: Why does PUT /posts return 404?
PUT on a name without an id is reserved for singleton resources (a top-level non-array object). For arrays, you must specify which item via PUT /posts/:id.
Q: How do I get a list of all my endpoints?
The startup banner prints them. Or GET / renders the bundled HTML index page.
Filtering
Q: How do I do an OR filter?
Use _where with a JSON object containing an or key: ?_where={"or":[{"a":{"eq":1}},{"b":{"eq":2}}]} (URL-encoded). The query-param syntax has no OR; only AND between params.
Q: Are filters case-sensitive?
eq/ne/in use strict JavaScript ===/!== — case-sensitive for strings. contains/startsWith/endsWith are explicitly case-insensitive (the field and the search value are lowercased before comparison).
Q: What happens if I use an unknown operator like :foo?
The filter is silently dropped. ?title:foo=bar&title:eq=hello behaves as ?title:eq=hello. This can hide bugs — see issue #1731. Stick to the documented operator list.
IDs and writes
Q: Can I assign my own id on POST?
No — any id you submit is overwritten with a randomly generated one. This is by design (prevents id collisions and forgery in the mock environment). To seat a row at a specific id, pre-populate db.json directly.
Q: How are IDs generated?
crypto.randomBytes(8).toString('base64url') — 11-character base64url strings.
Q: What happens to my data if the server crashes mid-write?
lowdb uses steno for atomic writes (write to temp file, then rename). A crash typically leaves either the old or new file intact, not a half-written one. You should still keep backups.
Watching & dev mode
Q: Do I still need -w / --watch?
No — file watching is on by default in 1.0.0-beta.15. The -w flag is preserved as a no-op for backwards compatibility and prints a yellow warning if used.
Q: How do I disable watching?
Set NODE_ENV=production in the environment before starting the server. This also enables the Eta template cache and disables sirv dev-mode behavior for static files.
Missing features
Q: Where's --delay?
Not in 1.0.0-beta.15. The community has been asking for it back (#1545, #1558). For now, run a wrapper proxy or pin to the 0.x line.
Q: Where are custom routes (rewrites like /api/posts → /posts)?
Not present in this beta. The route shape is fixed: /<resource> and /<resource>/<id>. To mount under a prefix, use a reverse proxy (Caddy, nginx, Vite's proxy, the http-proxy-middleware package, etc.) in front.
Q: Where is middleware support?
Not present in this beta. Middleware-based use cases (auth simulation, custom response shaping, per-route delays) need an external proxy or a thin wrapper that mounts json-server into a larger Express/Fastify app via its programmatic createApp export. See issue #1617.
Q: Where is X-Total-Count?
Not present. The pagination response includes items (total) and pages in the body wrapper instead. Tracked at issue #1727.
Q: Can POST return a custom JSON shape?
Not via configuration. The response is always the created item with its generated id. To customize, run json-server behind a wrapping server you control. Tracked at issue #1619.
Static files
Q: Where do static files come from by default?
./public/ relative to the working directory — automatically served if it exists. Add more with -s <dir>. Multiple -s flags layer in the order they were passed.
Q: Can I serve files from inside my JSON file?
No — only static files on disk. Binary uploads are not supported (issue #528 is a long-standing request).
Production
Q: Should I deploy this to production?
No. json-server is a development and prototyping tool. There is no auth, no rate limiting, no input validation beyond "is this a JSON object", and any client can read or destroy your data. See the Admin Guide's Security section for the full reasoning and mitigations.