Credit first: json-graphql-server is one of the nicest ideas in the mock-API space โ hand it a single file of JSON and it derives a full GraphQL schema: typed fields, all<Type> lists with pagination and sorting, per-field filter arguments (views_gt, views_lte, _neqโฆ), relationship traversal from user_id-style foreign keys, working CRUD mutations, and GraphiQL in the browser. It comes from marmelab (the react-admin folks) and โ unlike much of this category โ it is actively maintained: v3.3.1 shipped in June 2026, the repo saw commits this month, ~1,950 stars, MIT, ~3,250 downloads/week. If your whole need is a GraphQL fixture inside a Node test process or a webpack dev server, keep it. It's good.
The walls only appear when the mock needs to outlive a process or be reached by anything that isn't your laptop. We ran v3.3.1 ourselves in August 2026; everything below is from that transcript.
1. Writes evaporate on restart. Mutations genuinely work โ createPost returned {"id":"3"} and subsequent queries saw it โ but the store is process memory. We restarted the server and post 3 was gone. Fine for unit tests; a wall the moment the mock is a shared dev sandbox, a demo backend, or anything CI spins up in one job and asserts on in another.
2. It binds to localhost. The entire CLI surface is -p/--port and -h/--host โ we read the 30-line bin script. Default host is literally localhost; a teammate, a phone on the same Wi-Fi, a deployed preview build, or a CI browser job gets nothing unless you rebind and self-host the process somewhere.
3. GraphQL-only โ and the failure mode is sneaky. There is no REST view of the data. Worse: every path serves the GraphiQL HTML page with HTTP 200 โ we curled GET /posts and got back the GraphiQL app shell. A component that does fetch('/posts').then(r => r.json()) doesn't get a 404 it can handle; it gets 200 OK and then throws on parse.
4. No failure simulation. No flag for latency, no error injection, no rate-limit rehearsal. The happy path is the only path. (Also a small assertion gotcha: ids are typed GraphQLID, so the number 1 in your file comes back as the string "1" in every response.)
One fix-first tip while you're here: the CLI require()s your data file, so a db.js written as an ES module (export default {โฆ}) crashes with the baffling TypeError: e.reduce is not a function. Use module.exports = {โฆ} (or a .cjs extension) and it starts fine.
Mockbird accepts the exact same file shape โ top-level keys become collections, records are kept verbatim โ and gives back a hosted project that speaks both GraphQL and REST over the same store, with writes in a real database:
BASE=https://mockbird.mockbird.workers.dev
curl -s -X POST $BASE/api/projects/import -H 'content-type: application/json' -d '{
"name": "jgs-migrate",
"db": {
"posts": [
{ "id": 1, "title": "Lorem Ipsum", "views": 254, "user_id": 123 },
{ "id": 2, "title": "Sic Dolor amet", "views": 65, "user_id": 456 }
],
"users": [
{ "id": 123, "name": "John Doe" },
{ "id": 456, "name": "Jane Doe" }
]
}}'
# โ {"id":"<PID>","adminKey":"โฆ","baseUrl":"https://mockbird.mockbird.workers.dev/m/<PID>", โฆ}
That's the dataset from json-graphql-server's own README. Now both protocols work โ note the user_id foreign key joins in each of them, snake_case and all, and ids stay integers:
# GraphQL (GraphiQL lives at this URL in a browser):
curl -s $BASE/m/<PID>/graphql -H 'content-type: application/json' \
-d '{"query":"{ post(id:1) { title views user { name } } }"}'
# โ {"data":{"post":{"title":"Lorem Ipsum","views":254,"user":{"name":"John Doe"}}}}
# REST twin of the same records:
curl -s '$BASE/m/<PID>/posts?views_gte=100' # range filters
curl -s '$BASE/m/<PID>/posts/1?_expand=user' # same join, REST-style
Mutations write to D1 (SQLite at the edge), so they're still there after every restart we don't have โ and the REST side sees them instantly:
curl -s $BASE/m/<PID>/graphql -H 'content-type: application/json' \
-d '{"query":"mutation{ createPost(input:{title:\"New one\",views:1,user_id:123}) { id } }"}'
# โ {"data":{"createPost":{"id":3}}}
curl -s $BASE/m/<PID>/posts/3
# โ {"id":3,"title":"New one","views":1,"user_id":123} โ same write, other protocol
And the failure drills json-graphql-server has no flags for:
# 2s of latency on the GraphQL endpoint (we measured 2.13s):
curl '$BASE/m/<PID>/graphql?mock_delay=2000' -X POST -H 'content-type: application/json' \
-d '{"query":"{ postsCount }"}'
# chaos injection โ this fraction of requests fails with a 5xx + errors[] payload:
curl '$BASE/m/<PID>/graphql?mock_chaos=0.3' -X POST -H 'content-type: application/json' \
-d '{"query":"{ postsCount }"}'
# deterministic error sequences on the REST twin (exactly two 500s, then success):
curl '$BASE/m/<PID>/posts?mock_seq=500,500,200&mock_seq_key=ci-run-1' # โ 500 500 200
| json-graphql-server | Mockbird GraphQL | Mockbird REST twin |
|---|---|---|
allPosts | posts | GET /posts |
Post(id: 1) | post(id: 1) | GET /posts/1 |
_allPostsMeta { count } | postsCount | X-Total-Count header |
page / perPage | page / limit | ?page=&limit= (also _page/_limit) |
sortField / sortOrder | sortBy / order | ?sortBy=&order= |
filter: { views_gt: 100 } | where (exact-match, typed) | ?views_gt=100 (+ _gte/_lte/_lt/_ne/_like) |
filter: { q: "dolor" } | q argument | ?q=dolor |
Post { User { name } } via user_id | post { user { name } } โ same FK convention | ?_expand=user / ?_embed=posts |
createPost(title: โฆ) (flat args) | createPost(input: {โฆ}) | POST /posts |
updatePost / deletePost | updatePost / deletePost | PATCH / DELETE /posts/:id |
GraphiQL at / | GraphiQL at /m/<PID>/graphql | โ |
One honest asymmetry in that table: json-graphql-server generates range filter arguments (_lt/_lte/_gt/_gte/_neq) inside GraphQL itself, which is genuinely nice โ our GraphQL where is typed exact-match, and the range operators live on the REST twin instead. If your queries lean hard on GraphQL-side range filters, that's a real difference, not a spin.
| Need | Mockbird | Notes |
|---|---|---|
| URL a teammate/phone/CI can hit | hosted, CORS on | no tunnel, no self-hosting, no signup |
| Writes that survive | D1-backed persistence | your QA's bug-repro data is still there tomorrow |
| Known data states per test | snapshots + per-request pinning | parallel workers each pin their own scenario |
| Failure rehearsal | mock_delay / mock_chaos / mock_jitter (GraphQL too), mock_status / mock_seq / mock_ratelimit (REST) | test retry logic against deterministic sequences |
| See what your app actually sent | request inspector | last 50 requests, headers + bodies |
| Contract artifacts | openapi.json, types.ts, ?format=zod, Postman collection | generated from the same schema |
| Leave anytime | /db.json eject | round-trips straight back into json-graphql-server |
# GraphiQL in your browser:
# https://mockbird.mockbird.workers.dev/m/demo/graphql
curl https://mockbird.mockbird.workers.dev/m/demo/graphql \
-H 'content-type: application/json' \
-d '{"query":"{ products(sortBy: \"price\", order: \"desc\", limit: 2) { id name price } }"}'
# same data over REST โ one store, two protocols:
curl 'https://mockbird.mockbird.workers.dev/m/demo/products?sortBy=price&order=desc&limit=2'
| json-graphql-server | Mockbird | |
|---|---|---|
| Price | free, open source (MIT) | free while in beta |
| Input | db.json / db.js | same db.json โ plus OpenAPI, CSV, Postman, HAR |
| Protocols | GraphQL only (other paths return GraphiQL HTML, HTTP 200) | GraphQL + REST over one store |
| Writes | in-memory, lost on restart | persistent (D1/SQLite) |
| Reachable from | localhost (rebind + self-host to share) | any device, hosted URL |
| GraphQL range filters | yes โ generated _lt/_gt/โฆ args | exact-match where; range ops on REST |
| Failure simulation | none | delay, jitter, chaos, status, sequences, rate-limit |
| Record ids | stringified ("1") | stay integers |
| In-browser client mock (XHR) | yes โ unique | no |
| Works offline | yes | no |
| Request caps | none | 10k/project/day |
| Maintenance | active (v3.3.1 Jun 2026, marmelab) | actively developed |
Written by the Mockbird maker โ bias disclosed. Where json-graphql-server genuinely wins: it runs offline with zero latency and no request caps; its generated GraphQL-side range-filter arguments are richer than our where; and its browser/XHR-mock build โ mocking a GraphQL endpoint inside the page for client unit tests โ has no Mockbird equivalent at all. It is also actively maintained by a serious shop; nothing here is an abandonment story. Every claim above was verified by us in August 2026 on v3.3.1: the restart-wipe, the localhost bind and 30-line CLI, the GraphiQL-on-every-path behavior, the stringified ids, and the full Mockbird transcript (snake_case user_id joins in both protocols, mutation โ REST read-back, measured 2.13s delay) ran against the live service before publishing. If any of this changes, we'll update the page.
Full GraphQL reference in the docs. More guides: free GraphQL mock API ยท @graphql-tools/mock alternative ยท graphql-faker alternative ยท json-server, hosted ยท host a JSON file as an API ยท deterministic test data. Create your API โ