What this is
You give it a JSON document; it gives you a GraphQL endpoint. Every top-level key holding an array of objects becomes a type with a list field, a by-id field, a page envelope and four mutations. Every lone object becomes a type you can read and replace. Every foreign key becomes a relation you can follow from either end.
{ "posts": [ … ] } → type Post, fields posts and post, mutations createPost, updatePost, patchPost, deletePost
Nothing is configured and nothing is generated into a file — the schema is derived from the document on every request, so it is never out of step with the data. The JSON Server Studio serves the same document as REST; the two run side by side on the same file, and a document sorted through one comes back in the same order through the other.
How the schema is derived
A collection's fields come from reading every record, not a sample — a type inferred from the first fifty records that record four thousand contradicts is a schema its own data fails to validate against. A whole number inside GraphQL's 32-bit Int is Int; anything larger is Float, because serialising a bigger value through Int is an error at response time rather than a rounding. A field that is a string in one record and a number in another is JSON, not the wider of the two. A field absent or null anywhere is nullable. A nested object is JSON rather than a generated nested type, because a name invented from a field key would collide across collections and multiply the schema by the document's depth.
Names, and when they change
posts becomes the type Post, the fields posts and post, and the mutations createPost, updatePost, patchPost and deletePost. The inflector carries the irregulars — people becomes Person, categories becomes Category, series stays series — because a wrong guess here is a published type name that ends up in whatever your codegen wrote. It is not complete, and no inflector is, so the studio prints the derived name beside every key: a guess it got wrong is visible before anything depends on it. GraphQL has no hyphen in its name grammar, so blog-posts is published as blogPosts, and a key starting with a digit gains one underscore. Both are reported rather than done quietly.
Relations
A field named like the singular of a collection followed by Id — postId against a posts collection — publishes Comment.post and Post.comments in one go. That convention is json-server's, so a document that works there works here. Three rules keep it honest: the foreign key has to actually be present on the child, a relation never shadows a field the document already has, and a self-reference is skipped. Both sides take page arguments, and the many side is the reason to reach for GraphQL over the REST studio — you get both ends in one round trip and pick the fields on each.
Filtering, sorting and paging
Every list field takes where, orderBy, order, page and perPage. The filters are typed inputs rather than a convention, so an IDE completes them and a codegen types them: views: { gt: 100 }, title: { contains: "draft" }, id: { in: ["1", "2"] }. Clauses on different fields AND together, and AND, OR and NOT nest. Fields of type JSON and list fields have no filter, because there is no comparison over an arbitrary nested value that means anything and one that quietly matched nothing would be worse than none. perPage defaults to ten — a list field with no ceiling would make the cost estimate below impossible.
Arguments every list field takes
| Argument | What it does | Reach for it when |
|---|---|---|
| where | Filters the collection with typed comparisons. | You want a subset. It is the one argument an IDE can complete for you, because it is a real input type rather than a convention. |
| orderBy | Names the field to sort on, as an enum of that type's own fields. | Order matters. JSON and list fields are absent from the enum — there is no sort over an arbitrary nested value. |
| order | ASC or DESC. Defaults to ASC. | You passed orderBy. On its own it does nothing. |
| page | Which page, 1-based. Defaults to 1. | You are walking a collection. A page past the end is empty rather than an error, so a loop ends cleanly. |
| perPage | How many records per page. Defaults to 10, capped at 1,000. | Always worth thinking about — this is the number the cost estimate multiplies down your query tree. |
Two things a table cannot hold. orderBy disappears entirely from a type with no sortable field, so a schema that has it is telling you something. And perPage having a default is what makes the cost bound possible at all: without one, every list would have to be assumed at its maximum and ordinary two-level queries would be refused.
Mutations
create returns the record including the id it was given, which is the only way you learn what to ask for next. update replaces every field; patch merges. A record's id is absent from the update input rather than accepted and ignored, so you find out at validation instead of discovering your write went somewhere else. Mutations are refused over GET: the specification reserves GET for safe requests, and honouring that is what stops a link — in an email, in a crawler's queue, in a chat client's preview fetcher — from writing to your fixture. The input types hold the fields the collection already has, so adding a new field means editing the document rather than inventing one through the API.
What it refuses, and why
GraphQL moves the cost of a request from the server's route table to your query, which is the whole security difference from a REST fixture. A single document can ask for every collection, joined to itself, many times over, in two hundred bytes — and the derived relations are cyclic by construction, since a post has comments and every comment has a post. So three bounds run before any resolver: depth stops the cycle, an estimated node count stops breadth, and a root-field count stops aliasing one field two hundred times. Every response carries x-graphql-cost and x-graphql-depth so you can watch the estimate climb as you nest, rather than discovering the ceiling as a mysterious 400. Introspection is exempt from all of it, because its cost is bounded by the schema and an endpoint no IDE can read is most of the point gone.
Size, and the way back from full
A document may be up to 900 KB when it is pasted in, and may grow to 1,024 KB before writes stop. The gap is deliberate: a server created at its own ceiling would be full before its first mutation. Past 80% the usage bar changes tone and names the bytes left, so the lock is something you saw coming. When it does lock, queries and delete mutations keep working — deleting a record is the way out, and a ceiling that refused every write would be a trap whose only escape is discarding the whole document.
What is stored, and what is not
The document is stored, because serving it is the point. The last 50 operations are logged as a name, a type, a status, a duration and a cost — the query text is not kept, because a query is mostly your own field names against your own data and a second copy of that is more than this service should hold. A browser's claim on a server is a random secret in an HttpOnly cookie; the server stores only its hash. The recovery key is shown once and stored only as a hash, so it cannot be looked up or shown again. Deleting a server removes its document, its logs and every browser's access.
