Skip to content

GraphQL

GraphQL requests have their own editor — Query, Variables, Schema, Headers, Auth — and send through the ordinary toolbar.

Underneath, the engine executes them as an HTTP POST. That is not a limitation to work around; it is why GraphQL gets auth injection, {{variable}} resolution, the cookie jar, layered send settings, gateway rewriting, tests, and history without any of it being reimplemented.

  1. Create a request with protocol GraphQL.
  2. Set the endpoint URL.
  3. Write the query or mutation in the Query tab.
  4. Put variables in the Variables tab as a JSON object.
  5. Set Operation name when the document contains more than one operation.
query GetUser($id: ID!) {
user(id: $id) {
id
email
roles
}
}
{ "id": "{{userId}}" }

{{variables}} resolve inside the query text and inside the variables JSON, so "{{userId}}" above is filled from the active environment at send time.

A GraphQL endpoint can describe itself. Press Introspect in the Schema tab and Tinspec POSTs the standard introspection query to the endpoint — with this request’s headers, active environment and auth — then renders what comes back.

Two things follow from it:

  • A schema explorer. Root operations and their fields first, then the declared types, with a filter box over both. Deprecated fields carry a badge. Clicking a root field inserts a starter document for it, turning its required arguments into query variables; nested field rows are documentation rather than buttons, because a nested field is not an operation on its own.
  • Completion in the Query tab. Once a schema is loaded the header says schema loaded · ⌃Space suggests, and the query editor completes field names at any depth, argument names inside a field’s parentheses, and input type names after $name: in a variable definition. Aliases, inline fragments and fragment definitions all resolve.

Introspection is manual and never automatic. It is a real request to somebody’s server, so it happens when you ask for it. It is also a pure side channel: it changes what the editor can offer and nothing about what Send transmits.

The completer is a small scanner, not a GraphQL parser — carrying a full grammar to answer “what can I type here” about a document that is invalid mid-keystroke anyway was not worth the bundle. So it deliberately returns nothing for fragment spreads, a union’s selection set, argument and enum values, variable names, and anything after a syntax error.

An empty popup is the signal that the cursor is somewhere the scanner cannot reason about. A confidently wrong completion would cost more.

Many servers disable introspection outside development, and the standard way they do it is to answer 200 with an errors array. Tinspec quotes that error verbatim and says plainly that the explorer and autocomplete are unavailable for this endpoint — sending is unaffected, and a query you write by hand works exactly as before.

The query itself is the conservative, widely supported form — no specifiedByURL, no isDeprecated on arguments — because a query that fails validation against an older server tells you nothing about your API.

The response renders in the normal response pane — same JSON folding, same click-to-copy, same Tests tab for results.

Remember that GraphQL reports errors in the body with HTTP 200. A test asserting status eq 200 will pass on a failed query. Assert on the payload instead:

tests:
- source: body
path: data.user.id
op: exists
- source: body
path: errors
op: isEmpty

isEmpty is the right operator for the second one: absent, "", [] and {} all count as empty, which is exactly the set of shapes a healthy GraphQL response uses for errors.

GraphQL requests are chainable — they produce exactly one response. Extract from the body with a dotted path as usual:

extract:
- var: userId
from: body
path: data.user.id

Add API source → GraphQL API binds the endpoint itself: the introspection query becomes a polled spec source, and its root fields materialize as endpoints in the sidebar, grouped by Query / Mutation / Subscription.

One root field is one endpoint, not one type. That is forced by the protocol: every root field is a POST to the same URL, so identifying endpoints by URL would collapse the entire API onto a single row, a single overlay key and a single tab. The operation id (query.user) is the identity instead.

Being a spec source, it inherits Spec Changes, the breaking-change classification and endpoint overlays — a field that lost an argument reads as a change rather than as a stale SDL file on disk.

  • Subscriptions. GraphQL over WebSocket is not supported; the engine sends every GraphQL request as an HTTP POST. A subscription root field still appears in the explorer, and in a bound source’s endpoint list, because the server declares it — but sending one only POSTs the document. For a real socket, use the WebSocket protocol directly.
- name: Get user
protocol: graphql
method: POST
url: "{{baseUrl}}/graphql"
graphql:
query: |
query GetUser($id: ID!) { user(id: $id) { id email } }
variables: '{"id":"{{userId}}"}'
operationName: GetUser
auth:
provider: users-api

variables is a JSON-encoded string, not a nested mapping. Nothing about introspection is stored on the request — the poll belongs to the session, and the schema is the server’s.