GraphWeaver
A typed GraphQL client for Ruby, built for federation, extensibility, Sorbet, and testing.
GraphWeaver generates # typed: strict Ruby from your queries: nested T::Structs, casting code, and a typed execute — so srb tc sees the exact shape of every query result, and a typo'd field is a static error, not a runtime surprise.
# queries/person.graphql
query($id: ID!) {
person(id: $id) {
name
birthday
pets { name }
}
}
result = PersonQuery.execute!(id: "1") # typed result, or raises on errors (execute returns an envelope)
result.person&.name # => "Daniel" (typed String)
result.person&.birthday # => Date (custom scalars deserialize)
result.person&.nmae # => srb tc: Method `nmae` does not exist
New here? The getting started guide walks the
production setup end to end — initializer, codegen, fakes, CI. Or run the
examples, smallest first: examples/countries.rb (public
API, no auth, all dynamic), examples/rick_and_morty.rb (filtering,
pagination, a block-built type helper), and examples/github/run.rb
(auth + checked-in generated modules; it stars this repo ⭐ and introduces
you to your fellow stargazers).
Features
- Queries and mutations with typed variable kwargs — enums as
T::Enums, input objects asT::Structs, required vs optional falling out of nullability and defaults - Fragments (inline, named, type conditions), unions and interfaces (member structs,
__typenamedispatch), custom scalars (pluggable registry),@skip/@includenullability - Any schema source: live schema class, introspection JSON, or SDL — including Apollo Federation supergraph SDL; introspect live endpoints with caching
- Any transport: in-process schema execution, the zero-dependency HTTP executor, or Faraday with your own middleware — plus a composable
Retry(exponential/linear/custom backoff, jitter, retry-by-error-class or GraphQL code) — swap per call withexecutor: - Structured errors: a typed response envelope (partial data + extensions survive), an error hierarchy split by failure site, field-level reports with entity ids, and
schema_stale?detection — every error dual-surfaced as a human message plus JSON-ready#to_h - Testing built in: schema-correct fakes, failure simulation, record/replay cassettes with anonymization, rspec integration
- Dynamic mode for development:
GraphWeaver.parse(...)generates and evals on the fly, no build step
Usage
Three ways to run a query — pick by context:
| Context | Use |
|---|---|
| Production | checked-in codegen (rake graph_weaver:generate) — reviewed, srb tc-checked |
| Development, consoles | client.parse / client.load_queries! — no build step |
| Scripts, one-offs | client.execute! — no module at all |
The production path assembled is the getting started; the pieces:
require "graph_weaver"
# a client for one server: transport (Faraday when loaded), auth, and a
# lazily introspected schema. The first argument is a url or any schema
# source — a live schema class, or a .json/.graphql dump
api = GraphWeaver.new("https://api.example.com/graphql", auth: ENV["API_TOKEN"], cache: true)
# make it the app default — generated modules execute through it
GraphWeaver.client = api
# generate checked-in typed modules (rake graph_weaver:generate, or directly)
source = GraphWeaver::Codegen.generate(
schema: api.schema,
query: File.read("queries/person.graphql"),
module_name: "PersonQuery",
)
File.write("app/queries/person_query.rb", source)
# at runtime
PersonQuery.execute(id: "1") # via GraphWeaver.client
PersonQuery.execute(other_client, id: "1") # or per call
Module names derive from the operation name (query GetPerson →
GetPerson) or, for parse on a .graphql file, from the file name;
pass module_name:/name: to override. Pass client: (a constant) to
bake a default client into the generated module. Prefer Faraday? It's
opt-in (gem "faraday"), and the client picks it up when loaded —
middleware blocks and ready connections in transports.
In development, skip the build step entirely — modules from client.parse
carry the client's transport, no global wiring needed:
# parse a query into a typed module on the fly — a .graphql path or a raw string
PersonQuery = api.parse("queries/person.graphql")
PersonQuery.execute(id: "1")
# or every query file at once (queries_path convention), named like generation would
api.load_queries!
# or one-shot, no module at all — variables are plain kwargs
api.execute!("query($id: ID!) { person(id: $id) { name } }", id: "1")
Dig deeper
- Getting started — the production path in Rails, step by step: initializer, rake tasks, fakes, CI, Sorbet or not
- Generated modules — module anatomy, typed
variables (enums, input objects), fragments/unions/interfaces,
@skip/@include, naming, clients, dynamic mode - Against a real API — the exploratory tour: introspect a live endpoint (GitHub end to end), dynamic mode, schema caching
- Transports — clients, the execute contract, Faraday, retries and backoff
- Custom scalars — the registry: codec inference, requires, input coercion
- Errors — the Response envelope, the error hierarchy, field-level reports with entity ids, stale-schema detection
- Logging — point
GraphWeaver.loggerat any Logger: wire traffic at debug, introspection/cache/codegen at info, errors at warn - Testing — schema-correct fakes, failure simulation, rspec integration
- Cassettes — capture and replay real API
responses; anonymized recording (
GRAPHWEAVER_RECORD=1, rake tasks)
Installation
# Gemfile
gem "graph_weaver"
or
gem install graph_weaver
Development
make check— regenerate spec fixtures, run specs, typecheckmake integration— one-off checks against the live GitHub and Countries APIs