Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

json-server User Guide

Generated April 2026 from typicode/json-server at 89a34a4

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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.json yet, 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-server will 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.json while 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.

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 npx invocation 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.node field in the package's package.json enforces this — older Node versions emit an EBADENGINE warning and may not run at all. Check with node --version before 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.15 line is a rewrite. Several heavyweight features from 0.x (custom routes, middleware files, --delay, the --routes option) are not present in this beta. If you depend on those, pin to 0.17.4 from the 0.x line 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 .json or .json5 file. If the file doesn't exist, the server exits with File <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 id field (string). On read, numeric ids are automatically coerced to strings, and items missing an id get 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 status 404. POSTing or PATCHing a body that isn't a JSON object returns {"error": "Body must be a JSON object"} with status 400.

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 _where JSON-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 _page returns the full filtered array, not a wrapped object. Pass _page=1 (with or without _per_page) to get the paginated shape.

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 in children whose <singular_parent>Id equals this item's id.
  • _embed=parent (singular) — finds the item in the parents resource whose id matches this item's parentId.

_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 up db.json before 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 bundled views/index.html Eta 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 -s to point at your frontend's dist/ folder during development — json-server serves 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:

  1. Drop a db.json next to your frontend project:

    {
      "users": [
        { "id": "1", "name": "Alice", "email": "[email removed]" }
      ],
      "todos": [
        { "id": "1", "userId": "1", "title": "Buy milk", "done": false }
      ]
    }
    
  2. Start the server bound to all interfaces and a non-conflicting port:

    npx json-server -h 0.0.0.0 -p 4000 db.json
    
  3. In your frontend, point your fetch base URL at http://localhost:4000.

  4. Develop. Every POST/PATCH/DELETE writes through to db.json so your data is sticky across server restarts.

Tip

Add db.json to .gitignore if 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=999 with 3 items returns the last available page (clamped to last); _per_page=0 is clamped to 1. You will never see a 400 from 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[*].postId works because inflection.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 → API
  • http://localhost:3000/ → unless you also have a views/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

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.0 exposes the API to anyone on your network. There is no auth — they can read and write your db.json freely. Use it on trusted networks only, or keep the default localhost. 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:

  1. Verify the path: ls db.json.
  2. If the file should be created from scratch, echo '{}' > db.json first.
  3. 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:

  1. Find the squatter: lsof -i :3000 (macOS/Linux) or netstat -ano | findstr :3000 (Windows).
  2. 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:

  1. Check the indexed routes printed at startup — that's the authoritative list.
  2. Confirm whether the resource is a collection (array, supports :id) or a singleton (object, only top-level methods).
  3. Check method/route compatibility — PUT /:name only works on singletons; PUT /:name/:id only 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:

  1. Rename the foreign key to <singular_parent>Id (camelCase, capital I).
  2. 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.