0.0
The project is in a healthy, maintained state
A typed GraphQL client for Ruby — generate Sorbet T::Structs from queries, with federation, extensibility, and testing in mind
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

Runtime

 Project Readme

GraphWeaver

Gem codecov

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 as T::Structs, required vs optional falling out of nullability and defaults
  • Fragments (inline, named, type conditions), unions and interfaces (member structs, __typename dispatch), custom scalars (pluggable registry), @skip/@include nullability
  • 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 with executor:
  • 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 GetPersonGetPerson) 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
  • Federation — Apollo Federation: supergraph vs API schema, feeding weaver a composed graph, the @inaccessible caveat
  • 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.logger at 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, typecheck
  • make integration — one-off checks against the live GitHub and Countries APIs