Sui exposes data through several surfaces: JSON-RPC with a fixed method vocabulary, a newer gRPC API used by the current TypeScript SDK, and an indexer-backed GraphQL schema that presents a relational view of objects, transactions, events and checkpoints. GraphQL lets the client specify the exact response shape in a single POST, so a transaction, its effects and the objects it touched can be retrieved in one round-trip instead of several JSON-RPC calls. Requests are sent as a single POST containing query, variables and operationName, and an introspection query returns the schema so tooling can autocomplete and validate fields. Because the GraphQL endpoint is usually indexer-backed, it can lag the chain tip, and schema fields and availability vary by provider and network version, so introspection before relying on a field is essential. GraphQL is read-only: transactions are still submitted over JSON-RPC or gRPC.
Sui's Data-Access Surfaces and Where GraphQL Sits
Sui exposes chain data through more than one interface, and each is optimized for a different style of consumption. JSON-RPC is the method-oriented surface: the client calls named methods such as sui_getObject or sui_getTransactionBlock, and each call returns a fixed payload defined by the method. The newer gRPC API is used by the current TypeScript SDK and provides a typed, streaming-friendly interface for many of the same operations. The indexer-backed GraphQL schema sits alongside these as a relational view over objects, transactions, events and checkpoints, with pagination and nested selection built into the schema itself.
The distinction matters because the surfaces are not interchangeable. JSON-RPC and gRPC are procedural: you ask for a specific thing and receive a specific shape. GraphQL is declarative: you describe the shape you want and the server returns exactly that, which is useful when a dashboard or indexer needs several related entities at once. The Sui API references document these surfaces as distinct data-access options, and the Sui RPC guide covers the JSON-RPC method vocabulary in detail.
For readers coming from the JSON-RPC world, the mental model shift is that GraphQL is not a replacement for transaction submission. It is a read surface. You still broadcast transactions over JSON-RPC or gRPC; GraphQL is where you assemble the read-side view of what happened.
- JSON-RPC: fixed method vocabulary, fixed response payloads, one concern per call.
- gRPC: typed interface used by the current TypeScript SDK, suited to streaming and SDK integration.
- GraphQL: indexer-backed relational schema over objects, transactions, events and checkpoints, with nested selection and cursor pagination.
What GraphQL Is and Why the Client Controls the Response Shape
GraphQL is a typed query language and runtime for APIs, served over a single endpoint. Rather than exposing many endpoints or methods, it exposes a schema of types and fields, and the client sends a query describing exactly which fields it wants. The server validates the query against the schema and returns a JSON response whose shape mirrors the query. The GraphQL learn documentation describes this as the core contract: one endpoint, a typed schema, and client-specified selection.
In a Sui JSON-RPC workflow, fetching a transaction, its effects and the objects it touched typically means one call for the transaction, then additional calls for each object or effect you need. Each call returns a fixed payload, so you often receive more fields than you use and still need more calls to assemble the full picture. GraphQL inverts this: you write one query that selects the transaction, its effects, and the objects it touched as nested fields, and the server returns a single response containing only those fields.
This is the mechanism behind the round-trip reduction. It is not that GraphQL is inherently faster per byte; it is that the client can express a multi-entity read as one operation instead of N operations. For indexers and dashboards that repeatedly assemble the same relational view, that difference compounds.
Why GraphQL Matters for Indexers and Dashboards
Indexers and dashboards share a common pattern: they need a transaction plus its effects plus the objects it touched, often for many transactions in sequence. In JSON-RPC that pattern becomes a fan-out of calls, and each call consumes request units and adds a round-trip. GraphQL collapses the fan-out into a single query with nested selection, which reduces both round-trips and request-unit consumption for the same logical read.
Cursor-based pagination is part of the schema rather than an afterthought. Instead of manually tracking offsets, you request a page and receive a cursor that you pass into the next query. This is the same pattern described in the Sui object reads and dynamic fields pagination guide for JSON-RPC, but expressed as schema fields. For a dashboard that pages through checkpoints or events, the cursor becomes the stable continuation token.
The relational view also helps with joins that are awkward in a method-oriented API. If you need an event and the transaction that emitted it, or a checkpoint and the transactions it contains, the schema can express that relationship directly. The Sui checkpoint stream and ledger service guide covers the checkpoint side of that picture, and the Sui transaction effects and object changes guide covers how effects are structured when you do need to parse them.
- One query can select a transaction, its effects, and the objects it touched.
- Cursor pagination is a schema field, not a manual offset calculation.
- Nested selection reduces the number of calls needed to assemble a relational view.
- Fewer calls generally means fewer request units consumed for the same read.
How a GraphQL Request Differs from a JSON-RPC Request
A GraphQL request is a single HTTP POST to the GraphQL endpoint with a JSON body containing query, variables and optionally operationName. The query field holds the GraphQL document, variables holds the values referenced by that document, and operationName disambiguates when a document contains more than one operation. The response is a JSON object with a data field and, when something goes wrong, an errors array.
A JSON-RPC request is also a POST, but its body is a JSON-RPC 2.0 envelope with jsonrpc, method, params and id. The JSON-RPC 2.0 specification defines this method-oriented model: the client names a method and passes positional or named parameters, and the server returns a result or an error keyed by the same id. There is no schema negotiation in the request itself; the method vocabulary is fixed by the server.
The practical consequence is that GraphQL requests are self-describing in a way JSON-RPC requests are not. A GraphQL query names the fields it wants, so the response shape is visible in the request. A JSON-RPC call names a method, and the response shape is defined elsewhere. This is why GraphQL tooling can autocomplete and validate against the schema, while JSON-RPC tooling relies on documentation or generated clients.
// JSON-RPC 2.0 request envelope (method-oriented)
{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_getObject",
"params": ["0xOBJECT_ID", { "showType": true }]
}
// GraphQL request body (client-specified selection)
{
"query": "query GetObject($id: SuiAddress!) { object(address: $id) { address version digest } }",
"variables": { "id": "0xOBJECT_ID" },
"operationName": "GetObject"
}Introspection: Confirming the Schema Before Relying on Fields
Introspection is the GraphQL mechanism that returns the schema itself as data. A client can ask which types exist, which fields each type has, and what arguments those fields accept. The GraphQL learn documentation describes introspection as the foundation for tooling such as autocomplete, validation and schema explorers. For Sui, introspection is the reliable way to confirm that a field you intend to use actually exists on the endpoint you are querying.
This matters because Sui's GraphQL schema and availability vary by provider and network version. A field that exists on one endpoint may be absent or renamed on another, and a query that works against a testnet endpoint may fail against mainnet. Introspecting first turns a runtime failure into a known constraint. It also tells you whether introspection is enabled at all: disabling introspection in production is common, and when it is disabled you must rely on a fixed query rather than generated tooling.
The example below sends an introspection query to confirm the schema is reachable. It is deliberately small: it asks for the query type's name and a few of its fields, which is enough to prove the endpoint responds with a schema. A full introspection query returns the entire type system and is much larger.
// introspection.mjs — confirm the GraphQL schema is reachable
const ENDPOINT = process.env.SUI_GRAPHQL_ENDPOINT;
const introspectionQuery = `
query IntrospectQueryType {
__schema {
queryType {
name
fields {
name
description
}
}
}
}
`;
const res = await fetch(ENDPOINT, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ query: introspectionQuery, operationName: "IntrospectQueryType" })
});
const json = await res.json();
if (json.errors) {
console.error("Introspection failed:", json.errors);
process.exit(1);
}
const queryType = json.data.__schema.queryType;
console.log("Query type:", queryType.name);
console.log("Fields:", queryType.fields.map((f) => f.name).join(", "));A Runnable Node.js Query for a Transaction and Its Effects
Once the schema is confirmed, a concrete query can select a transaction and its effects using variables. The example below uses fetch, which is available in current Node.js releases, and passes the transaction digest as a variable rather than interpolating it into the query string. Using variables is the recommended pattern because it keeps the query document stable and lets the server validate the value against the schema type.
The query selects a transaction by digest and asks for its effects and the objects it touched. The exact field names depend on the schema version exposed by your endpoint, which is why the introspection step comes first. If a field name differs, the server returns an error naming the unknown field, and you adjust the query against the introspected schema rather than guessing.
The response is a JSON object whose data shape mirrors the query. Because the client specified the selection, there is no need to filter out unused fields afterward. This is the round-trip reduction in practice: one POST returns the transaction, its effects and the related objects together.
// query-transaction.mjs — fetch a transaction and its effects in one round-trip
const ENDPOINT = process.env.SUI_GRAPHQL_ENDPOINT;
const DIGEST = process.env.SUI_TX_DIGEST;
const query = `
query TransactionWithEffects($digest: String!) {
transaction(digest: $digest) {
digest
effects {
status
timestamp
objectChanges {
address
inputState
outputState
}
}
}
}
`;
const res = await fetch(ENDPOINT, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
query,
variables: { digest: DIGEST },
operationName: "TransactionWithEffects"
})
});
const json = await res.json();
if (json.errors) {
console.error("Query errors:", json.errors);
process.exit(1);
}
console.log(JSON.stringify(json.data.transaction, null, 2));Measuring Round-Trips and Bytes Against Your Own Endpoint
The value of GraphQL for a given workload is an empirical question, and the honest way to answer it is to measure against the endpoint you actually use. The table below is a template: fill it in with your own observations rather than relying on numbers from another environment. Run the same logical task twice, once as a sequence of JSON-RPC calls and once as a single GraphQL query, and record the calls, bytes and round-trips for each.
Record the pagination cursor used for each page so the comparison is reproducible. If a task spans multiple pages, note the cursor value so another reader can replay the same sequence. Bytes can be measured from the response body length, and round-trips can be counted as the number of HTTP requests issued. Keep the task definition identical across both columns so the comparison is meaningful.
Because provider behavior varies, treat any single measurement as specific to that endpoint, network and time. The method is what transfers: define the task, run both surfaces, and record the numbers.
- Task: describe the logical read in one sentence, e.g. 'fetch transaction X with its effects and touched objects'.
- JSON-RPC calls: count the number of method calls issued.
- GraphQL calls: count the number of POST requests issued.
- Total bytes: sum the response body sizes for each surface.
- Round-trips: count the HTTP requests, including retries.
- Pagination cursor used: record the cursor value for each page so the run is replayable.
Limitations and Tradeoffs of the GraphQL Surface
The GraphQL endpoint is usually indexer-backed, which means it can lag the chain tip. A transaction that has just been finalized may not yet appear in the index, so a dashboard that reads immediately after submission can observe a gap. This is a property of the indexing pipeline, not a defect in GraphQL, but it changes how you design read-after-write flows. If you need the freshest possible view, JSON-RPC or gRPC may be the better surface for that specific read.
Schema fields and availability vary by provider and network version. A field present on one endpoint may be absent on another, and a query that works on testnet may need adjustment on mainnet. Introspecting before relying on a field is the practical mitigation, but it does not eliminate the variance. When introspection is disabled in production, which is common, generated tooling cannot discover the schema, and a fixed query must be maintained by hand.
GraphQL is read-only. It does not submit transactions; that remains the job of JSON-RPC or gRPC. A complete application therefore uses more than one surface: GraphQL for relational reads, and JSON-RPC or gRPC for submission and for reads that must not lag. The Sui RPC WebSocket subscriptions guide covers the streaming side for readers who need push-style updates rather than polling.
- Indexer-backed endpoints can lag the chain tip.
- Schema fields and availability vary by provider and network version.
- Introspection is often disabled in production, requiring fixed queries.
- GraphQL is read-only; transaction submission stays on JSON-RPC or gRPC.
- A complete application typically uses more than one data-access surface.
Troubleshooting Common GraphQL Query Failures
Most GraphQL failures fall into a small number of categories, and the error response usually names the cause. A validation error means the query references a field or argument that does not exist in the schema; the fix is to introspect and correct the field name or argument type. An execution error means the query was valid but the resolver failed, often because the requested entity does not exist or the indexer has not yet seen it.
A missing data field with an errors array is the standard GraphQL error shape. Read the errors entries first: they include a message and often a path pointing to the failing field. If the error mentions an unknown field, the schema on that endpoint differs from what you assumed. If the error mentions a type mismatch, check that your variables match the declared argument types.
If introspection itself fails, the endpoint may have introspection disabled, or the endpoint may not be a GraphQL endpoint at all. Confirm the URL and the HTTP method: GraphQL is a POST to a single endpoint, not a method call. If the response is HTML rather than JSON, the request likely hit a web server rather than the GraphQL handler.
- Validation error: field or argument not in schema — introspect and correct.
- Execution error: resolver failed — check entity existence and indexer freshness.
- Unknown field in errors: schema differs from your assumption.
- Type mismatch: variables do not match declared argument types.
- Introspection failure: introspection disabled or wrong endpoint.
- HTML response: request hit a web server, not the GraphQL handler.
Next Steps for Building on the Sui GraphQL Surface
The practical path is to introspect first, then write queries against the confirmed schema, then measure the workload against your own endpoint. Start with a small query that selects a single entity, confirm the response shape, and expand to nested selection once the basics work. Keep the query document stable and pass values as variables so the server can validate them against the schema.
For teams running indexers or dashboards, the OnFinality Learn hub collects related guides on Sui data access, and the Sui network page covers the network context. If you are evaluating endpoints for a production workload, the RPC pricing page and the API service page describe the commercial side, while the Sui RPC guide remains the reference for the JSON-RPC method vocabulary you will still need for submission.
A reasonable next experiment is to take one dashboard panel that currently fans out into several JSON-RPC calls and rewrite it as a single GraphQL query, then fill in the results table from the measurement section. That gives you a concrete, reproducible comparison for your own environment rather than a borrowed number.
- Introspect the endpoint before writing queries.
- Start with a single-entity query, then expand to nested selection.
- Pass values as variables so the server validates them.
- Measure one real dashboard panel against both surfaces.
- Keep JSON-RPC or gRPC for transaction submission and freshest reads.