Your .graphql files, compiled into Sorbet types — and the fakes to test them.
GraphWeaver is graphql-codegen for Ruby. Write a query as a .graphql file and it
generates checked-in # typed: strict Ruby — nested T::Structs, casting, a typed
execute — so srb tc knows the exact shape of every result. The schema can be a
live graphql-ruby class, an introspection dump, SDL, or an Apollo supergraph;
at runtime the only dependencies are graphql and sorbet-runtime.
# app/graphql/queries/person.graphql
query($id: ID!) {
person(id: $id) {
id
name
birthday
pets { name }
}
}rake graph_weaver:generate turns that file into a PersonQuery module, and what
comes back is a struct rather than a Hash you have to trust:
result = PersonQuery.execute!(id: "1") # or #execute, for the Response envelope
result.person&.name # => "Daniel"
result.person&.birthday # => #<Date: 1990-06-15> custom scalars deserialize
result.person&.pets&.map(&:name) # => ["Shelby", "Brownie"]
result.person&.nmae
# srb tc: Method `nmae` does not exist on `PersonQuery::Result::Person`
# Did you mean `name`?person is T.nilable because the schema says the field is nullable — the &.
isn't defensive, it's the schema talking. Static Sorbet is optional:
sorbet-runtime is the only Sorbet gem this one needs, so in an app that doesn't
run srb tc the same typo surfaces as a NoMethodError the first time the line
runs, rather than in CI — Sorbet, with or
without.
Add gem "graph_weaver" to your Gemfile. In Rails, the rest is one command:
rails g graph_weaver:install https://api.example.com/graphqlwhich writes the initializer, the app/graphql layout, the editor config and the
schema dump. Getting started walks the production
setup end to end.
Or skip the build step and poke at an API from a console: GraphWeaver.new(url)
parses a query into a module on the spot, and run! executes one without a module
at all — against a real API. The
examples run that path for real, smallest first: a public API in
30 lines, a paginated search, the production path against GitHub, and the
federated graph below.
Generation makes result types exact, which makes them tedious to build by hand —
and most generators stop there and leave you the fixtures. GraphWeaver ships the
fakes. One line in the spec helper (require "graph_weaver/rspec"), then one tag
says what an example runs against:
it "shows the profile", graphql: :fake do
person = PersonQuery.execute!(id: "1").person
person.name # => "Shakita Stark" with faker installed; "name-1" without
person.birthday # => #<Date: 2024-12-16> custom scalars included
person.pets.size # => 2
endNo fixture, no stub, no HTTP — and the values are seeded from rspec's own seed, so
--seed 4242 hands back that same person and a failure reproduces.
Random data answers "does this render". When the example is about the data, pin
the fields it's about — graphql_fake("Person.name" => "Ada"), or a whole type
off your factory — and everything else in the selection stays fabricated. Pins are
schema names, checked and spellchecked, so a typo raises rather than leaving the
example green against random data.
The tag also picks a real client when you want one: :in_process runs your
resolvers, :router runs them across a federated graph, and :wire serves either
at your own endpoint, so the transport you ship runs too. Field-level failure
simulation and record/replay cassettes with anonymization are in
testing.
When your app is both a GraphQL client and a subgraph, the local router plans a
query across the composed supergraph and runs your real resolvers over the
boundary — no gateway process, no node, no sockets. It prints the plan as it
fetches, batching every node at a level into one _entities call, and anything it
can't answer faithfully it refuses at plan time rather than guessing, naming the
coordinate that stopped it. It is diffed against a real @apollo/gateway over the
same supergraph: currently 73 queries identical, 2 refused, 0 wrong
(spec/integration/router_parity_spec.rb).
examples/federation.rb runs the whole thing with no
network; see federation.
The lifecycle is rake tasks, not a CI pipeline you assemble yourself:
schema:refresh re-introspects the committed dump, schema:diff names what
changed when whatever that dump came from has moved past it, queries:check names
the queries that drift broke and where, unused names the selections your app
stopped reading, and verify fails when the checked-in Ruby is stale. Generation
is deterministic — same schema and queries, byte-identical files — so regenerating
never shows a diff you didn't earn. See
getting started.
Any release can change what codegen emits, patch releases included. So rake graph_weaver:generate is part of every upgrade, and verify is what tells you
when you've skipped it.
- Typed variable kwargs — enums as
T::Enums, input objects asT::Structs, required vs optional falling out of nullability and defaults - Fragments, unions and interfaces (member structs,
__typenamedispatch),@skip/@includenullability - Any transport — in-process, a zero-dependency HTTP client, or Faraday with your own middleware, plus a composable
Retry - Structured errors — an envelope that keeps partial data and extensions, a hierarchy split by failure site, field-level reports, stale-schema detection
- Getting started — the production path in Rails, step by step
- Generated modules — module anatomy, typed variables, fragments/unions/interfaces, naming, clients, dynamic mode
- Testing — fakes, failure simulation, the rspec tags
- Federation — supergraph vs API schema, the local router, what it refuses
- Transports — the execute contract, Faraday, retries and backoff
- Errors — the Response envelope, the error hierarchy, field-level reports
- Translating input errors — stable keys and data for bad input, on either side of the wire
- Custom scalars — what's registered already, codec inference, input coercion
- Cassettes — capture and replay real responses, anonymized
- Editor support — five lines of YAML for schema autocomplete in
.graphqlfiles, no JS project - Against a real API — introspecting a live endpoint, GitHub end to end
- Logging — point
GraphWeaver.loggerat any Logger - Upgrading — regenerate on every bump, and what each release moved
- Migrating — coming off graphql-client or a hand-rolled client, commit by commit
- Alternatives — graphql-client, graphlient, artemis, hand-rolled: what each is good at, and where this one loses
make check— regenerate spec fixtures, run specs, typecheckmake integration— one-off checks against live APIs (GitHub needs a token) and a federation gateway (needs node)