Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/michael-tiger-2010/dragonjson/llms.txt

Use this file to discover all available pages before exploring further.

Any HTTP server that implements this protocol is compatible with the dragonJSON client. The spec is transport-agnostic — implement it in any language or framework you like. The client handles caching, event dispatching, and relay; the server only needs to handle data and mutations.

Base URL

All requests are sent to the single URL passed to dragonJSON(url, options). There is no routing by path beyond GET vs POST — the operation type is encoded in the query string or request body.
dragonJSON("https://mysite.com/api", { ... })
//                 ↑ every request goes here

Reading Data

Single-path read

The client issues a GET request with a path query parameter. The value is a dot-separated string that maps to the nested structure of your data store. An empty or absent path means the root.
GET /api?path=posts.page1
Return the JSON value at that path — any valid JSON type is acceptable:
{ "title": "Hello World", "body": "First post body." }
Primitives are equally valid responses:
"Hello World"

Batched read

When enableBatching: true (the default), the client coalesces rapid parallel requests and sends them together using the paths parameter — a JSON-encoded array of path strings.
GET /api?paths=["posts.page1","meta.title"]
Return a flat object keyed by path string:
{
  "posts.page1": { "title": "Hello World", "body": "..." },
  "meta.title": "dragonJSON Demo"
}
Every path from the request array must appear in the response object. If a path does not exist, set it to null — do not omit it. An absent key causes the client to reject that path’s promise with an error.

Freeform query ($get)

When the client calls .$get(command), the command object is JSON-encoded and appended as a command query parameter alongside the path. The result is returned directly to the caller and is never stored in the client cache.
GET /api?path=posts&command={"action":"paginate","cursor":0,"limit":10}
The shape of command is entirely up to your implementation. Return any valid JSON.

Special Response Fields

Object responses may include the following reserved fields to control how the client loads children:
__next
boolean
Signals that this object has children that have not been sent yet. The client stores a placeholder and re-fetches the real data the first time a child is accessed. Use this to defer loading of expensive sub-trees.
{ "__next": true }
__more
boolean
Signals that this object has unknown or dynamic children — keys the client does not know in advance. The client will fetch any key accessed under this node on demand rather than treating it as not found. Use this for open-ended collections such as user-generated content or dynamic routes.
{ "__more": true }
__batch
object
A hierarchical batch envelope. Allows the server to return multiple paths in a single response, saving round-trips. See the Hierarchical batch section below for the full shape.

Hierarchical Batch (Optional)

When enableHierarchicalBatch: true is set in client options, the client adds two extra query parameters to reads:
GET /api?path=posts&target=posts.page1.title&hierarchical=true
target tells the server which path the client is ultimately trying to reach. The server may respond with a __batch envelope containing multiple paths at once, saving the client extra round-trips:
{
  "__batch": {
    "posts": { "page1": { "__next": true } },
    "posts.page1": { "title": "First post", "body": "..." },
    "posts.page1.title": "First post"
  }
}
If you do not want to implement this optimisation, simply respond with the normal value for path and ignore target and hierarchical. The client detects the absence of __batch and wraps the response automatically.

Mutations

All mutations are POST requests. The path being mutated is encoded in the query string, exactly like a read. The Content-Type must be application/json.
POST /api?path=some.nested.key
Content-Type: application/json
The request body varies by operation type.

$set

The body is the new value at path. It can be any JSON type — no __op field is used.
POST /api?path=posts.page1
{ "title": "Updated title", "body": "New content" }
Response:
{ "invalidate": ["posts.page1"] }

$add

The body carries __op: "add" and a value. The client may suggest a key; the server may accept or override it. If key is omitted, the server assigns one.
POST /api?path=posts
{ "__op": "add", "key": "page5", "value": { "title": "New post", "body": "..." } }
Response — invalidate the parent collection so the client re-fetches the updated key list:
{ "invalidate": ["posts"] }

$remove

The path being deleted is in the query string. The body is always { "__op": "remove" }.
POST /api?path=posts.page1
{ "__op": "remove" }
Response — invalidate the parent so the client evicts the deleted entry:
{ "invalidate": ["posts", "posts.page1"] }

Mutation response contract

Every mutation endpoint must return { "invalidate": [...] }. An empty array is valid and tells the client to treat the mutation as a cache no-op. Missing or malformed responses cause the client to throw.
The client marks each listed path as stale and re-fetches it on next access. It also fires any .$on() listeners registered on those paths.

Authentication

If the client is initialised with auth: "Bearer my-token", every request includes:
Authorization: Bearer my-token
Validate this on the server side as you would any bearer token. Return 403 to deny access. Custom headers supplied via headers: {} in client options are also forwarded verbatim on every request, so you can use any header-based authentication or metadata scheme your stack requires.

Error Handling

Return a non-2xx HTTP status code for errors. The client rejects the corresponding promise with a message containing the status code and path. Error response bodies are not parsed by the client — they are available for your own logging.
For batch requests, avoid failing the entire batch for one bad path. Return the valid paths normally and set the failed path to null. This lets the client surface a per-path error rather than rejecting all promises at once.

Live Invalidation Relay

If the client is configured with liveInvalidation, it sends a relay message to the relay channel after every mutation:
{
  "invalidate": ["posts.page1"],
  "initiator": "set",
  "key": "*",
  "path": "posts"
}
Your server or relay (WebSocket, BroadcastChannel, SSE, etc.) should broadcast this message to all other connected clients. Receiving clients invalidate the listed paths and fire any matching .$on() listeners locally — exactly as if the mutation had happened on their own instance.
The relay message is not a mutation request. It is metadata about a mutation that has already been applied. Do not apply it again server-side.
You can also use the relay channel for server-initiated messages — for example, push notifications — by emitting relay-shaped messages from the server to any path your clients are listening on.

Path Conventions

  • Paths use . as a separator: posts.page1.title
  • An empty string or absent path refers to the root of the data store
  • Path segments must not contain . (dots within a segment are not supported)
  • Path segments must not start with $ (reserved for client methods)
  • The segment names then and exists are reserved and must not be used as data keys

Compliance Checklist

A compliant dragonJSON server must implement all of the following:
  • Accept GET ?path= and return the JSON value at that path
  • Accept GET ?paths= (JSON-encoded array) and return a flat { "path": value } object
  • Accept POST ?path= with a JSON body and return { "invalidate": [...] }
  • Distinguish $add (__op: "add") and $remove (__op: "remove") from plain $set (no __op field)
  • Accept GET ?path=&command= and return any JSON for freeform queries
  • Return null (not omit) for paths that do not exist in batch responses
  • Always return { "invalidate": [...] } from mutations, even when the array is empty
The following are optional but recommended:
  • Support __next: true on object responses to defer expensive sub-trees
  • Support __more: true on object responses with dynamic key sets
  • Support GET ?path=&hierarchical=true&target= with a __batch response envelope
  • Implement a relay channel for live invalidation across clients

Quick Reference

Client callMethodQuery paramsBody
await server.posts.page1GETpath=posts.page1
await Promise.all([server.posts.page1, server.meta])GETpaths=["posts.page1","meta"]
server.posts.$get({ action: "paginate" })GETpath=posts&command={...}
server.posts.page1.$set({ title: "Hi" })POSTpath=posts.page1{"title":"Hi"}
server.posts.$add({ title: "New" })POSTpath=posts{"__op":"add","value":{...}}
server.posts.page1.$remove()POSTpath=posts.page1{"__op":"remove"}

Build docs developers (and LLMs) love