for TypeScript and PHP done right
Anyone who keeps GraphQL queries manually in sync with types builds up technical debt, every single time a schema field changes. Code generation from the schema and operations files turns types into automation instead of a maintenance chore.
Table of Contents
- 1. Why manual types are an anti-pattern in GraphQL
- 2. graphql-codegen: core principle and workflow
- 3. TypeScript plugins: typed-document-node, react-query and urql
- 4. PHP code generation: interfaces and value objects from the schema
- 5. Configuration: structuring codegen.ts cleanly
- 6. Using the Magento schema as a codegen source
- 7. CI integration: verifying generated code without checking it in
- 8. Codegen approaches compared
- 9. Common mistakes in code generation
- 10. Summary
- 11. FAQ
1. Why manual types are an anti-pattern in GraphQL
GraphQL ships with a typed schema: every field, every type and every operation is explicitly defined. Even so, it is common practice to maintain TypeScript interfaces for GraphQL responses manually, in parallel to a schema that keeps evolving independently of them. That creates a systematic desynchronization problem: as soon as a schema field is renamed or a type is extended, every manually maintained interface has to be updated by hand. In a team with several developers and an actively developed schema, that is not a rare edge case, it is a constant, ongoing task.
The problem is not limited to TypeScript. PHP code that processes GraphQL responses, whether in a headless frontend BFF, a CLI tool or an API integration layer, also works with implicit array structures when there is no code generation, and a typo in a field name only surfaces at runtime. Code generation from the schema turns type safety into the norm instead of a matter of manual discipline.
2. graphql-codegen: core principle and workflow
The tool @graphql-codegen/cli reads the GraphQL schema (as an SDL file, a URL, or an introspection result) and the operations files (queries, mutations, fragments) and generates code from them according to configurable plugins. The workflow is straightforward: schema and operations live in the repository, graphql-codegen runs as a build step and produces one or more output files. The generated code is either checked in or used exclusively as a verification step in CI.
Configuration lives in a codegen.ts file at the project root. It defines where the schema is located, which documents (operations files) are read, and which plugins run with which settings for which output files. The strength of the tool lies in its plugin architecture: for different frameworks, React, Vue, urql, react-query, there are specialized plugins that generate not just TypeScript types but fully typed hooks.
# Operations file: src/graphql/customer.graphql
# graphql-codegen reads this alongside the schema to generate typed hooks
query GetCustomer {
customer {
firstname
lastname
email
addresses {
id
street
city
postcode
country_code
}
}
}
mutation UpdateCustomerEmail($email: String!, $password: String!) {
updateCustomerEmail(email: $email, password: $password) {
customer {
email
}
}
}
# Fragment reuse (codegen generates fragment types automatically)
fragment CustomerBasic on Customer {
firstname
lastname
email
}
3. TypeScript plugins: typed-document-node, react-query and urql
The plugin @graphql-codegen/typed-document-node generates typed DocumentNode objects that can be passed directly to Apollo Client, urql or fetch-based clients. The result: the compiler knows the exact type of the response object, including every nested field and every optional, nullable type. Typos in field names are caught immediately at compile time, not at runtime in production.
For react-query, the plugin @graphql-codegen/typescript-react-query generates ready-made, typed useQuery and useMutation hooks that can be used directly in component code. The developer imports the generated hook, passes the variables, and gets back a fully typed response object, without writing a single TypeScript interface by hand. The urql plugin works the same way, producing useQuery and useMutation composables that match the urql API.
4. PHP code generation: interfaces and value objects from the schema
PHP has fewer established codegen tools than TypeScript, but the approach is the same. A PHP GraphQL client that processes responses benefits from generated value objects that mirror the schema. Tools like spawnia/sailor or softonic/graphql-client combined with a custom codegen layer generate PHP classes from SDL definitions that deserialize responses directly and hand back typed objects. That eliminates the typical $response['data']['customer']['email'] chain in favor of $response->customer->email with full IDE autocomplete.
Magento itself does not use code generation on the server side, its resolver architecture does not depend on an external schema that would need to be generated. But in a headless setup, where a PHP BFF consumes the Magento GraphQL schema, PHP codegen can add substantial value. The Magento schema is fetched via introspection, saved as an SDL file, and then used as the input for the PHP codegen process.
# codegen.ts: configuration for TypeScript + PHP generation
# TypeScript output: typed document nodes + react-query hooks
# generates: src/generated/graphql.ts
#
# documents: 'src/graphql/**/*.graphql'
# schema: https://shop.example.com/graphql
# plugins:
# - typescript
# - typescript-operations
# - typed-document-node
# - typescript-react-query
# Usage in React component (generated hook):
# import { useGetCustomerQuery } from '../generated/graphql'
#
# const { data, isLoading } = useGetCustomerQuery()
# data?.customer?.email // fully typed, nullable-aware
# PHP output (sailor pattern): generates PHP value objects
# CustomerResponse::class with ->customer->email typed as string|null
5. Configuration: structuring codegen.ts cleanly
A sensible codegen.ts configuration separates schema types from operations types and allows multiple output files for different layers. The most common mistake is writing every plugin into a single output file that then becomes a monolithic file containing hundreds of generated types. A better split is a base types file (from the schema, without operations) plus operations-specific files that contain fragment types and hooks for individual domains.
Scalars must be explicitly mapped in the configuration. The Magento schema contains custom scalars such as String for prices (which are handled internally as floats) and Boolean. Without an explicit scalar mapping, codegen generates the type any for unknown scalars, which cancels out the type-safety benefit. A good approach is to map every custom scalar to a concrete TypeScript type or a branded type that prevents accidental mix-ups between similar-looking fields.
6. Using the Magento schema as a codegen source
The Magento GraphQL schema can be exported as an SDL file via the introspection endpoint and used as a schema source for graphql-codegen. That makes sense for headless setups where the frontend is developed directly against the Magento schema. The workflow: fetch the schema via introspection in CI, save it as schema.graphql, and point codegen at it as the source. That way, the generated type system always stays in sync with the actual Magento schema.
One practical problem here: the Magento schema is very large and contains hundreds of types that an average frontend project does not need. The graphql-codegen option onlyOperationTypes: true limits the generated types to the ones actually used in the operations, which significantly reduces the output size and makes the generated file more maintainable. For Magento projects with many modules, it is also worth caching the schema locally instead of re-fetching it via introspection on every codegen run.
7. CI integration: verifying generated code without checking it in
Whether generated code should be checked into the repository is a team decision. For checked-in code, the rule is: the npm run codegen command must run in CI, and a git diff --exit-code afterward ensures that the checked-in code matches the freshly generated code. If the checked-in code diverges, the CI step fails and signals that operations were changed without regenerating the types.
Alternatively, generated code can be excluded from the repository entirely and produced only in CI. That avoids merge conflicts in generated files, but requires a build step before development can begin. In practice, the first approach (checked in plus CI check) is easier for teams, because IDE autocomplete works immediately and no local build infrastructure is required for new team members. Either way, it is important to run graphql-codegen with the --check flag in CI, which aborts the process with exit code 1 if any output file would change.
# CI pipeline step: verify generated types are up to date
# Run codegen in check mode: fails if output would change
# package.json scripts:
# "codegen": "graphql-codegen --config codegen.ts"
# "codegen:check": "graphql-codegen --config codegen.ts --check"
# CI YAML (GitHub Actions pattern):
# - name: Check GraphQL types are up to date
# run: npm run codegen:check
# # Fails with exit code 1 if schema or operations changed
# # without regenerating the typed files
# Schema introspection for Magento:
query IntrospectSchema {
__schema {
types {
name
kind
fields {
name
type { name kind }
}
}
}
}
# Export schema to SDL file (alternative to introspection at runtime):
# npx get-graphql-schema https://shop.example.com/graphql > schema.graphql
8. Codegen approaches compared
Different setups call for different codegen strategies. The right approach depends on the framework, the team size and the complexity of the schema.
| Approach | Framework | Output | Notable trait |
|---|---|---|---|
| typed-document-node | Apollo, urql, fetch | DocumentNode + types | Framework-agnostic, minimal |
| typescript-react-query | React + react-query | Typed useQuery hooks | No manual hook layer needed |
| typescript-urql | Vue, React + urql | Typed composables | Good fit for Vue setups |
| spawnia/sailor (PHP) | PHP BFF/CLI | PHP value objects | Type-safe PHP clients |
| Manual types | Any | Hand-written interfaces | Falls out of sync on schema changes |
In Magento headless projects with a React frontend, typed-document-node combined with a fetch-based client is often the most maintainable choice, because it requires no Apollo dependency. For teams already using react-query, the react-query plugin delivers the biggest productivity boost through fully ready-made hooks. PHP codegen is worthwhile as soon as a PHP layer consumes the Magento GraphQL schema.
9. Common mistakes in code generation
The most common mistake is ignoring nullable types in the generated code. GraphQL types are nullable by default, which leads to plenty of string | null | undefined types in the generated TypeScript. Teams that find this annoying and convert the types into manual interfaces lose the maintainability advantage of codegen entirely. The correct response is to consistently use optional chaining (?.) and model null checks explicitly, instead of casting nullability away.
A second common mistake is not using fragments. Without fragments, codegen generates separate, non-reusable types for identical field sets that appear in different queries. With fragments, a CustomerBasicFragment type is generated once and used as a shared base type in every query that references that fragment. That makes the generated code more consistent and makes refactoring easier, because a fragment change automatically updates every derived type.
# WRONG: duplicate field sets without fragment, generates separate types
query GetCustomerForHeader {
customer { firstname lastname email }
}
query GetCustomerForProfile {
customer { firstname lastname email addresses { city } }
}
# RIGHT: shared fragment, codegen generates reusable CustomerBasicFragment type
fragment CustomerBasic on Customer {
firstname
lastname
email
}
query GetCustomerForHeader {
customer { ...CustomerBasic }
}
query GetCustomerForProfile {
customer {
...CustomerBasic
addresses { city postcode }
}
}
# Generated TypeScript (simplified):
# type CustomerBasicFragment = { firstname: string; lastname: string; email: string }
# type GetCustomerForProfileQuery = { customer: CustomerBasicFragment & { addresses: ... } }
10. Summary
GraphQL code generation solves a systematic problem: manual types are always one step behind the schema. graphql-codegen turns that manual process into an automated build step that produces type-safe TypeScript hooks and PHP value objects directly from the schema and operations. The effort spent on the initial configuration pays for itself after the very first schema changes, because no developer has to manually verify whether the types are still accurate.
For Magento headless projects, using the Magento schema as a codegen source is especially valuable, because the schema is large and is frequently extended through module updates. CI integration with the --check flag ensures that teams do not accidentally ship stale types to production. The combination of typed-document-node outputs and consistent fragment usage produces the most compact and maintainable generated code.
GraphQL Code Generation: the essentials at a glance
graphql-codegen
Reads schema plus operations, generates typed TypeScript hooks and PHP value objects. Plugin architecture for framework-specific outputs.
CI integration
graphql-codegen --check as a CI step: fails if the schema or the operations changed without regenerating the types.
Use fragments
Fragments produce reusable base types instead of duplicated interfaces. A single fragment change automatically updates every dependent type.
Nullable types
Do not cast away GraphQL nullability in TypeScript. Use optional chaining (?.) consistently and model null checks explicitly.