Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

GraphQL

A GraphQL client library is readable source. Its operations are documented Rust structs — the fields are the operation’s variables — beside the types the response decodes into, and a schema module holds the service’s vocabulary: the scope markers selections are written against, its enums, its input objects. That is the library’s product, and it is the thing a user reads, greps, and jumps into.

capi_graphql provides the runtime those definitions plug into — a small family of generic endpoints (GraphqlCall for the ordinary POST, Get, Persisted, Uploads, Subscribe, GraphqlBatchCall, and GraphqlWsConnect), riding the ordinary Http capability and the stock body decoder — plus the graphql! macro, which is the consumer’s power tool rather than the library’s generator.

impl GraphqlOperation for GetUser {
    type ApiConfig = MyConfig;
    type Scope = schema::Query;
    type Root = get_user::Root;
    type Data = Option<get_user::User>;

    const KIND: OperationType = OperationType::Query;
    const NAME: &'static str = "GetUser";

    fn variable_definitions() -> &'static [VariableDefinition] {
        &[VariableDefinition { name: "id", graphql_type: "ID!" }]
    }

    fn selection() -> Selection<Self::Scope> { … }

    fn extract(root: get_user::Root) -> Self::Data { root.user }
}

let user = api.query(GetUser { id: Id::new("1") }).await?;

What a client crate adds is small. Its config implements GraphqlApiConfig — two methods, graphql_url() (the one absolute URL every operation posts to) and json_lib() — and its root invokes graphql_schema! once:

capi_graphql::graphql_schema!(
    schema = "schema.graphql",         // the SDL, resolved against CARGO_MANIFEST_DIR
    config = GraphqlZeroConfig,
    schema_module = declared,          // the crate hand-writes `pub mod schema`; `emitted` has the macro write it
);

scalars(DateTime = crate::scalars::DateTime) maps custom scalars (unmapped ones fall back to String), and upload = Upload names the SDL scalar whose variables carry files. That one invocation embeds the schema and exports the crate’s own graphql!, which a consumer invokes from outside the crate to author an operation the library never shipped, validated against the same schema at compile time:

graphqlzero_capi_rs::graphql! {
    query AlbumTitle($id: ID!) {
        album(id: $id) { title user { username } }
    }
}

Whether a GraphQL service authenticates is a property of the deployment, not of an operation, so every runtime endpoint declares Auth::DEFAULT.optional(): a config that holds credentials installs a store and every call carries them, while an open service sends bare, with no store at all. Every request sends Accept: application/graphql-response+json, application/json.

The pieces, briefly:

  • An operation value is its variables. The struct’s wire encoding is the request’s variables object, and the struct converts into GraphqlCall through IntoEndpoint, so query() takes it directly. Nullable variables and input-object fields are Tristate<T>, keeping GraphQL’s absent/null/value distinction; a schema’s enums and input objects reach a document through ArgumentLiteral.
  • Decode types are named for the schema. They sit in a module named after the operation, where Root is the decoded data object and every other type carries the name of the schema type it projects — get_user::User, get_user::Company — so the Rust reads like the document. Where one operation projects the same type twice, every projection of it takes the selection path that reached it instead. Libraries write this by hand; graphql! emits it.
  • The response envelope is decoded once, centrally: { data, errors } splits into the operation’s typed output or a GraphqlErrorResponse that preserves any partial data alongside the spec’s error objects (message, locations, path, extensions). That envelope is every operation’s Endpoint::ErrorGraphqlErrorResponse<Op::Data>, so a library writes no domain error enum — and a caller recovers it typed with err.api_error::<GraphqlErrorResponse<Option<get_user::User>>>(), then reads .errors() and .data(). A non-success status without an envelope is UnexpectedStatus and a 2xx that isn’t one is Decoding: the standard ladder, with no per-library decoder code.
  • graphql! is the downstream feature. graphql_schema! reads the SDL once per crate and exports the crate’s graphql! wrapper with the schema embedded, so a consumer writes a selection the library never shipped — validated against that schema at compile time, with no code-generation step anywhere. Under schema_module = declared the macro emits no vocabulary of its own and resolves against the library’s hand-written schema module.
  • Schema drift is checked at test time, not compiled against. A library pins an API vintage deliberately, so conformance belongs beside the fixtures: projection::<Op>() reads back the requirements an operation’s code places on the schema — its selection, its variable declarations, the wire shapes of its types — and check_operations verifies the committed SDL still satisfies them, reporting in the schema’s own language. The checker is the conformance feature, a host-only test surface (the schema IR lives in capi_graphql_schema) that a client enables in its dev-dependencies and runs from a test. This is the consumer-driven-contract idea in GraphQL clothing: project what the code needs, check it against what the provider publishes.
  • Render equality holds the source to its intent. Each operation is declared a second time through the crate’s own graphql!, and operation_document proves the two send byte-identical documents — so a selection, alias, or argument that drifts from what was meant fails a test.
  • Queries can ride GETapi.query(op.via_get()) moves the document and variables into the percent-encoded query string and drops the body, so CDNs and browsers can cache the response. The QueryOperation marker is what keeps mutations off the GET transport at compile time.
  • Persisted queries (apq feature) — api.persisted().query(op) sends a SHA-256 of the document instead of the document, registering it exactly once on a cache miss: smaller requests on every call after the first, with the register-on-miss dance handled by a runner, above decoding, where the error envelope is already typed.
  • File uploads (uploads feature) — Upload-typed variables travel as multipart file parts per the GraphQL multipart request spec, streamed on native transports; the request JSON rides beside them with null at each file position, and api.query(op) works unchanged.
  • Subscriptionssubscription operations run over GraphQL-over-SSE in distinct connections mode: api.query(op) returns a stream of the operation’s data type, each next event decoding through the same envelope semantics as every other response, until the server completes. One root field per subscription. For graphql-transport-ws servers the ws feature adds a WebSocket session, shaped by lane: on the async lane GraphqlWsSession::handshake then split() yields a caller-spawned GraphqlWsDriver and a GraphqlWsHandle whose subscribe returns per-subscription typed streams demultiplexed by id over one connection; on the blocking lane GraphqlWsSubscription::open runs one subscription per connection with a blocking pump. The config’s GraphqlWsConfig supplies connection_params (the connection_init payload, where a token rides) and a graphql_ws_url that defaults to graphql_url.
  • Fragments and spreads compose selections, in the library’s source and across graphql! invocations alike; the Scope association is what proves a fragment belongs where it is spread. An interface selection without type conditions decodes as a plain struct of the interface’s declared fields; with conditions, common fields land in every variant of a __typename-tagged enum and uncovered concrete types decode into a typed common-fields fallback. Unions take inline fragments only.
  • @batch repeats one root field with per-entry inlined arguments under b0, b1, … aliases and restores entry order on decode — one request answering a whole set of lookups.

The reference client is graphqlzero_capi_rs; the authoring recipe lives in the capi-rs-author skill (references/graphql.md).