How to Fetch with GraphQL and Codegen
Use GraphQL with a codegen step so the queries are type-checked at build time and the client gets typed results without manual interfaces.
On this page
Introduction
The hardest part of type-safe GraphQL is not writing the code — it is deciding what the code is allowed to reject. A GraphQL codegen setup that accepts everything is easy to ship and expensive to operate, because every malformed input becomes a production surprise. The discipline is to enumerate the shapes the system actually receives, encode them in GraphQL codegen, and let the library reject the rest loudly. This keeps the happy path fast and the failure path visible.
A GraphQL codegen guard for type-safe GraphQL should be proportional to the risk. A ten-field form does not need a hundred-line schema; it needs the five checks that catch the five mistakes the users actually make. Ship the five, measure the rejections, and add the sixth only when the traffic proves it exists. Speculative generality is the bug you write on purpose, and GraphQL codegen makes it easy to write, which is why discipline matters.
The sections below walk through the concrete decisions: where to put the graphql codegen check, how to keep it fast under load, what to do when the contract changes, and how to test the failure paths so they degrade on purpose instead of by accident.
Write the query
Write the query is the first decision because it determines what the rest of the GraphQL codegen pipeline can assume. A wrong choice here turns into type-safe GraphQL under load, and the invariant you thought GraphQL codegen guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Write the query is the one most teams skip: drive graphql codegen to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
const QUERY = gql`query User($id: ID!) { user(id: $id) { id name } }`;
const { data } = useUserQuery({ variables: { id } });
The typescript above is the shape to ship for type-safe GraphQL. It is small enough to audit and specific enough that the failure behavior is obvious. Adapt the constants to your traffic and latency budget, and resist the urge to handle cases the system has not seen yet.
Run codegen
The mechanism behind Run codegen is straightforward in GraphQL codegen, but the consequences ripple. A wrong choice here turns into type-safe GraphQL under load, and the invariant you thought GraphQL codegen guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Run codegen is the one most teams skip: drive graphql codegen to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Use the typed hook
Use the typed hook is where most GraphQL codegen implementations leak performance. A wrong choice here turns into type-safe GraphQL under load, and the invariant you thought GraphQL codegen guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Use the typed hook is the one most teams skip: drive graphql codegen to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
test('returns typed user', () => {
expect(data?.user.name).toBe('Ada');
});
Pin the behavior with this test. A GraphQL codegen setup that passes this test degrades on purpose instead of by accident, which is the difference between a managed failure and an incident.
Handle errors
The hard part of Handle errors is not the happy path — it is what happens when graphql codegen fails halfway. A wrong choice here turns into type-safe GraphQL under load, and the invariant you thought GraphQL codegen guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Handle errors is the one most teams skip: drive graphql codegen to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Fragment composition
Fragment composition should be observable without a debugger. A wrong choice here turns into type-safe GraphQL under load, and the invariant you thought GraphQL codegen guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Fragment composition is the one most teams skip: drive graphql codegen to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Test the query
Test the query is a trade, not a winner. A wrong choice here turns into type-safe GraphQL under load, and the invariant you thought GraphQL codegen guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Test the query is the one most teams skip: drive graphql codegen to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Common pitfalls
The common pitfalls in type-safe GraphQL all reduce to one mistake: treating GraphQL codegen as a formality instead of a contract. The contract is the thing the rest of the system relies on, and a weak contract propagates weakness everywhere.
- Skipping the graphql codegen boundary so type-safe GraphQL enters unvalidated.
- Assuming GraphQL codegen is fast without measuring the tail latency under load.
- Letting type-safe GraphQL fail silently instead of failing loudly with a metric.
- Testing only the happy path so the graphql codegen failure surfaces in production.
- Sharing mutable state across requests so one bad input poisons the next.
Conclusion
Do not conflate the conclusion with the implementation. They look similar from the outside but the failure modes differ: GraphQL codegen fails fast and the caller knows, while an unchecked path fails slow and the caller guesses. Choose the one whose failure mode your system can absorb, and document the choice at the call site.
Self-check: write a test that drives graphql codegen to the failing condition and assert the system degrades gracefully. Then type-safe GraphQL is a property you maintain, not a feature you hope for.
Sources: GraphQL Codegen, Apollo Client
FAQ
- Codegen vs manual?
- Codegen. Manual types drift from the schema. Codegen is the source of truth.
- Fragments?
- Use fragments for reusable fields. Codegen composes them into the query type.
Report a problem with this articleOpens a pre-filled issue on GitHub.