The wire doesn't change. The same query text goes out and the same JSON comes back, so this is a sequence of small commits rather than a cutover: queries move across one at a time while the old client keeps serving, and most of the spec suite you already have keeps passing untouched.
Budget 3–4 weeks of one engineer for an app with ~40 queries and 25k lines of
code reading them — what it costs breaks that down. If the app
doesn't run Sorbet and has fewer than ten queries, don't migrate: the wins that
survive without srb tc are real, but they don't pay for a checked-in code
generator (alternatives).
Both gems stay in the Gemfile until the last commit. rails g graph_weaver:install boots the app to read your configuration, so an initializer
that still requires graphql-client has to keep resolving — remove the gem first
and the generator dies on cannot load such file -- graphql/client. Running the
two side by side is the right shape anyway.
- Install beside graphql-client, pointed at the dump you already have:
rails g graph_weaver:install db/petstore_schema.json(a schema dump you already have). Commit that and nothing else. - Adopt the dump. It records no source url, so
schema:diffandqueries:checkhave no server to ask.rake graph_weaver:schema:refreshrewrites it from the client the app points at (orURL=<your endpoint>once) and records the provenance. Do it now: until you do,queries:checkis re-reading the file it is meant to be checking against, and says so. It rewrites the file in place, in graph_weaver's format — pretty-printed introspection JSON with agraph_weaver:provenance key besidedata. graphql-client goes on reading it, sinceload_schemahands the parsed hash to graphql-ruby and graphql-ruby takesdataand ignores its siblings; a dump anything else reads is worth checking once. - Register scalars and enums, and regenerate — if the API declares any.
Plenty don't: one whose leaves are all
String,Int,IDandBooleanhas nothing to register and skips this step whole. Registrations are baked into generated source, so one added later reaches nothing until the nextrake graph_weaver:generate(scalars). - Port one query end to end — write the
.graphqlfile, generate, rewrite its call site — and leave the specs alone. They pass untouched. That single commit is the proof for every one after it. - Port the rest, one commit per query. Each
Client.parseconstant stays where it is until its last caller is gone. - Only now port the specs to
graphql: :fake(testing). Keep webmock until the last wire-level stub is gone, and keep one spec that really serves HTTP. - Delete graphql-client. Last commit.
- Sigs through your own app are a separate project, after all of the above — the types stop where your sigs do says what that work is.
Keep your own error classes. Translating GraphWeaver::QueryError,
TransportError and ServerError at one seam leaves a controller, its rescues
and their specs untouched (errors). Keep your presenters too, and
your fixtures until step 6.
Because the wire is the same, a webmock suite doesn't notice which client sent
the request. Swap the client under an untouched suite and most of it stays green
— 18 of 22 stubbed examples in one migrated Rails app, same stubs, same JSON
fixtures; the four that failed were unit specs feeding a hand-rolled Struct to
a presenter. That is the difference between a reviewable migration and a big-bang
one.
Same skeleton, three differences.
- One branch, no seam. There's no generator to keep bootable and a hand-rolled client is sixty lines, so running two of them side by side costs more than it saves.
- Set the load order first:
GraphWeaver.load_generated!goes before your own requires (not Rails?). Addraketo the Gemfile while you're there. - Keep the old client's transport specs, rewritten against a real
GraphWeaver.new(url, retries: 2)with webmock. Retries, timeouts and backoff are the one thing fakes can't cover.
- The retry loop. A hand-rolled one usually retries everything, mutations
included.
retries: 2excludes mutations by default, honoursRetry-After, and knows 408 and 429 (retries) — twenty lines gone and a correctness bug gone with them. - Scalar parsers.
register_scalar("Money", Money)is one line, and it deletes the"USD 12.50".splitsitting in every place that parsed one.DateandDateTimeneed no registration at all (scalars). - Fragment unwrapping. graphql-client masks a spread fragment's fields on the
parent, so every call site reads
Petstore::PetFields.new(data.pet).nameand carries the parent alongside for the fields the fragment didn't cover. Generated structs inline the fragment, so it'spet.name; a fragment that is a whole selection becomes one shared Ruby type (hoisting). - JSON fixtures and stub helpers.
graphql: :fakefabricates a schema-correct response and you pin the fields the example is about. Pins are schema vocabulary ("Pet.species" => "DOG"), so they survive query refactors, and they're spellchecked — a typo raises instead of leaving the example green against random data (testing).
Enum drift is fatal. A hand-rolled client hands you the raw string, so a value
the server added after you shipped falls through to whatever your code does with
an unknown one — commonly a humanize. A generated enum refuses instead: casting
raises and the whole response is lost, naming the value, the enum and the values
it knows about.
That is the right default for an API you own and a real risk for one you don't, because it breaks on a day nobody deployed. Before you port a query over an enum someone else can extend, register a fallback for it and unknown values land there instead — enums.
| how it scales | ~40 queries, 25k lines | |
|---|---|---|
.graphql files and generating |
sublinear — the second query costs a tenth of the first | 2–3 days |
| call sites | linear, and the bulk of it: every data["x"]["y"] becomes x.y, and every defensive nil guard either disappears or turns out to have been wrong |
~1 day per 3–4k lines of consuming code |
| specs | linear, and pleasant — the fixture-to-pin conversion is the same edit every time | ~2 days |
| sigs through your own app | the one that decides whether the migration paid off | weeks; a separate project |
Coming off graphql-client, run the new one behind the old for the first two weeks. The wire compatibility makes that free.