Getting started: the production path (Rails)
The setup that ships: queries live as .graphql files, generation writes
# typed: strict Ruby you check in, and CI fails when anything drifts.
Mostly copy/paste. (Exploring an API from a console instead? Start with
dynamic mode — no build step.)
Rails is assumed below; the non-Rails note at the bottom covers the one difference.
1. Install
# Gemfile
gem "graph_weaver"
2. Bootstrap the schema dump
Codegen reads a schema dump at app/graphql/schema.json
(GraphWeaver.schema_path). You never write this file by hand —
cache: true writes it on first introspection. Bootstrap once from a
console:
GraphWeaver.new("https://api.example.com/graphql", auth: ENV["API_TOKEN"], cache: true).schema
Skip this step and the generate task tells you exactly that — the error
message is the documentation. Prefer PR-reviewable diffs? cache: :graphql
writes SDL instead of introspection JSON; both generate identical code.
Note cache:/ttl: apply only to url clients — a schema source (a live
class or a dump) never introspects, so passing them raises.
3. Wire the client
# config/initializers/graph_weaver.rb
GraphWeaver.client = GraphWeaver.new(
"https://api.example.com/graphql",
auth: ENV["API_TOKEN"],
cache: true, # reuses the committed dump; delete the file to re-introspect
)
# custom scalars/enums/type helpers — register globally, so the rake
# tasks bake them into generated source
GraphWeaver.register_scalar("DateTime", Time, serialize: :iso8601, requires: "time")
GraphWeaver.client = is the load-bearing line: generated modules
without a baked transport resolve to it at execute time (the full
resolution order). The generated
modules themselves load at boot automatically (the Railtie requires
everything under generated_path, after your initializers run) —
outside Rails, call GraphWeaver.load_generated! wherever your app
boots.
4. Rake tasks — nothing to do
In Rails the graph_weaver:* tasks register themselves (a Railtie), and
they depend on :environment, so your initializer — and its
registrations, which are baked into generated source — runs first.
Outside Rails, add require "graph_weaver/tasks" to your Rakefile.
5. Write a query, generate, commit
# app/graphql/queries/person.graphql
query($id: ID!) {
person(id: $id) {
name
birthday
}
}
rake graph_weaver:generate # writes app/graphql/generated/person_query.rb
Commit the schema dump and the generated files. Generated code is reviewed like any other code — and never edited by hand.
PersonQuery.execute!(id: "1").person&.name # typed, via GraphWeaver.client
Shared fragments
Define reusable fragments once and spread them from any query:
# app/graphql/fragments/person_fields.graphql
fragment PersonFields on Person { name birthday }
# app/graphql/queries/person.graphql
query($id: ID!) { person(id: $id) { ...PersonFields } }
Each query inlines only the fragments it (transitively) spreads, so the sent
QUERY stays self-contained — the server never needs your fragment library.
Fragment files hold only fragments (no operations), and names are unique across
them. Point elsewhere with GraphWeaver.fragments_paths (an appendable list,
default app/graphql/fragments).
Shared unions
When a shared fragment is the whole selection on a union field, its type is
hoisted once into a GraphQLUnions module and every query that spreads it
aliases the same type — so a union selected across many queries becomes one
Ruby type family, and you write one exhaustive case … when … T.absurd that
works everywhere:
# app/graphql/fragments/feed_item.graphql
fragment FeedItemFields on FeedItem {
__typename
... on Post { title }
... on Photo { url }
}
# any query
query { feed { ...FeedItemFields } } # feed : T::Array[FeedItemFields::Type]
Hoisting is what the shared fragment buys you — there's no flag. It triggers
only when the union field's selection is exactly that one spread (mix in other
fields, or shadow the fragment with a query-local one of the same name, and the
union stays inlined in that query). Named like the inputs module from the output
path (GraphQLUnions, or GithubUnions in a multi-schema layout); override
with GraphWeaver.unions_module=.
6. Test against fakes
# spec/support/graph_weaver.rb
require "graph_weaver/rspec"
GraphWeaver::Testing.configure { |config| config.auto_fake = true }
The opt-in is deliberate (no surprise fakes); once on, the schema
auto-locates from the committed dump and every query in every example
executes against a seeded, schema-correct FakeClient — no server, no
stubs, and rspec --seed 1234 reproduces the fake data along with test
order. Pin values with overrides:, simulate failures with Failure.*
— see testing.
7. Verify in CI
rake graph_weaver:verify # generated code fresh? fails on any drift
rake graph_weaver:schema:verify # server drifted? re-introspects and compares
Two different questions. graph_weaver:verify checks that the committed
generated files match what the current schema + queries + registrations
would produce — run it in every CI build. graph_weaver:schema:verify
asks whether the server has moved since the dump was taken — it needs
network, a dump with a recorded source url (introspected dumps have one),
and GRAPHWEAVER_AUTH for private APIs; run it on a schedule and refresh
with rake graph_weaver:schema:refresh.
Sorbet, with or without
sorbet-runtime is a hard dependency, so generated T::Structs and sigs
enforce at runtime in every app — no Sorbet setup required on your end.
The static layer (srb tc flagging a typo'd field before anything
runs) applies only when your app runs Sorbet, and only to checked-in
generated files — dynamic parse is invisible to srb tc. Everything
works without Sorbet; codegen plus Sorbet is what moves type errors from
runtime to CI.
If your app globally injects T::Sig (class Module; include T::Sig), the
per-struct extend T::Sig in generated files is redundant — rubocop's
Sorbet/RedundantExtendTSig flags it. GraphWeaver auto-detects that at
generation time and skips the extend; override with
GraphWeaver.extend_t_sig = true/false. (Generated code is machine-generated
and marked "do not edit," so excluding generated/** from rubocop is also fine.)
Not Rails?
Everything above works the same, minus the Railtie conveniences: add
require "graph_weaver/tasks" to your Rakefile yourself, and — since
there's no :environment hook to run your registrations — require the
file that does them from the Rakefile too. GraphWeaver.load_generated!
goes wherever your app boots instead of an initializer.