Type-Safe Storefront Integration With Codegen
Magento ships its GraphQL endpoint without type information for the client, which leaves storefront code trusting field names and nullability on faith. graphql-code-generator automatically generates TypeScript types and typed query documents from the real schema, so broken field access and forgotten null checks show up at compile time instead of in a real customer's checkout.
Table of Contents
- 1. Why Type-Safe GraphQL Integration Matters for Headless Magento Storefronts
- 2. Magento's GraphQL Schema as the Source of Truth: Introspection and schema.graphql
- 3. Setting Up graphql-code-generator: codegen.yml Against the Magento Endpoint
- 4. Typing a Product List Query End to End, From the Query String to the Result
- 5. Typing Cart and Checkout: createEmptyCart, addProductsToCart, Cart Query
- 6. Handling Magento's Nullable-Heavy Schema Design in Everyday TypeScript
- 7. Integrating Codegen Into the Build Pipeline: Catching Schema Changes Early
- 8. Caching and Performance With Typed GraphQL Clients
- 9. Untyped Fetch Calls vs. Codegen-Typed Queries Compared
- 10. Summary
- 11. FAQ
1. Why Type-Safe GraphQL Integration Matters for Headless Magento Storefronts
Whether it's PWA Studio, a custom Next.js or Nuxt frontend, or a Hyva-adjacent JavaScript widget talking directly to the Magento GraphQL endpoint: as soon as a frontend stops rendering inside Magento templates, the implicit consistency check that a PHP block automatically gets against its own data model disappears. A fetch() call with a raw GraphQL string knows neither the field names of the response nor their types, so every access like data.products.items[0].price_range.minimum_price.regular_price.value is pure convention that the compiler never verifies.
This is exactly where TypeScript combined with generated types comes in: instead of copying field names from memory or from outdated documentation, a codegen tool derives the exact types directly from the live schema. Typos in field names, wrong assumptions about return types, and forgotten nullable cases turn into compile errors instead of undefined exceptions in a real customer's browser. For teams running Magento as a backend with a separate JavaScript frontend as their storefront, this is not a nice-to-have, it's the foundation for reliable releases.
2. Magento's GraphQL Schema as the Source of Truth: Introspection and schema.graphql
Every typed storefront client starts with the same question: where does the truth about fields, types, and nullability come from? The answer is GraphQL introspection, a standard mechanism that lets any GraphQL endpoint describe itself. Against the Magento endpoint at /graphql, an introspection query or a CLI tool like get-graphql-schema can export a complete schema.graphql file that maps every type, field, and nullability declaration of the installed Magento system, including custom extensions from third-party modules.
This exported schema file is the actual contract between backend and frontend, not the official Magento documentation, which quickly falls out of date on custom installations with their own schema.graphqls extensions. It's important to re-export the schema per environment, because a custom module, a Magento upgrade, or a disabled B2B module genuinely changes which fields are available. The exported schema then serves as input for the codegen run and should be versioned in the repository, so schema drift becomes visible in the diff.
3. Setting Up graphql-code-generator: codegen.yml Against the Magento Endpoint
graphql-code-generator is the established tool for automatically producing TypeScript types from a GraphQL schema and the queries used in a project. Configuration lives in a codegen.yml that points either directly at the Magento endpoint or at the previously exported schema.graphql, followed by a glob pattern that collects every .graphql file or template-literal-embedded query in the project. With the typescript-operations plugin, this produces types for every single query and mutation; with typed-document-node, it additionally produces typed document objects that GraphQL clients like Apollo or urql can consume without a manual type annotation.
The generated code usually ends up in a generated.ts file that is never edited by hand, but regenerated on every schema or query change. In practice, graphql-codegen runs as an npm script, often in --watch mode during development, so newly written queries immediately produce typed results without a developer having to trigger a manual build step.
# codegen.yml: points at the live Magento GraphQL endpoint
overwrite: true
schema:
- "https://shop.mironsoft.de/graphql":
headers:
Store: "default"
documents: "src/**/*.graphql"
generates:
src/generated/graphql.ts:
plugins:
- "typescript"
- "typescript-operations"
- "typed-document-node"
config:
# Magento's schema is nullable-heavy; keep that information explicit
avoidOptionals: false
skipTypename: false
scalars:
# Magento's Money/SortEnum scalars map cleanly to primitives here
SortEnum: "'ASC' | 'DESC'"
hooks:
afterOneFileWrite:
- "prettier --write"
4. Typing a Product List Query End to End, From the Query String to the Result
A typical product list query against products combines filtering, sorting, and pagination in a single request and returns nested price information along with page_info. Instead of maintaining this query as a raw string, it's stored in the project as a named .graphql document, with fragments for reusable parts like ProductCardFields. Fragments avoid duplication between list and detail pages, and ensure that a new field only needs to be added in one place to become typed at every point of use.
After the codegen run, this query automatically produces a pair of types: an input type for the variables, such as ProductsQueryVariables, and an output type for the response, such as ProductsQuery, both derived exactly from the schema. The GraphQL client no longer calls the query with a generic gql tag and a manual as ProductsQuery cast, but with the generated TypedDocumentNode, which automatically passes the input and output types to the client method. A field access like result.data.products.items is thus fully typed, including every nullable step along the way.
# products.graphql: reusable fragment plus the actual list query
fragment ProductCardFields on ProductInterface {
id
sku
name
small_image {
url
label
}
price_range {
minimum_price {
regular_price {
value
currency
}
}
}
}
query Products($search: String!, $pageSize: Int!, $currentPage: Int!) {
products(search: $search, pageSize: $pageSize, currentPage: $currentPage) {
total_count
items {
...ProductCardFields
}
page_info {
current_page
page_size
total_pages
}
}
}
// generated.ts: excerpt of what graphql-code-generator produces for the query above
// Note how almost every field is nullable, mirroring Magento's schema exactly
export type ProductCardFieldsFragment = {
__typename?: 'SimpleProduct' | 'ConfigurableProduct';
id?: number | null;
sku?: string | null;
name?: string | null;
small_image?: { url?: string | null; label?: string | null } | null;
price_range?: {
minimum_price?: {
regular_price?: { value?: number | null; currency?: CurrencyEnum | null } | null;
} | null;
} | null;
};
export type ProductsQueryVariables = {
search: string;
pageSize: number;
currentPage: number;
};
export type ProductsQuery = {
products?: {
total_count?: number | null;
items?: Array<ProductCardFieldsFragment | null> | null;
page_info?: { current_page?: number | null; total_pages?: number | null } | null;
} | null;
};
// Typed document node: client methods infer variables and result automatically
export declare const ProductsDocument: TypedDocumentNode<ProductsQuery, ProductsQueryVariables>;
5. Typing Cart and Checkout: createEmptyCart, addProductsToCart, Cart Query
The cart flow is where Magento's nullable-heavy schema shows up most, because several mutations are called in sequence here, and every response can theoretically return user_errors instead of the expected data. createEmptyCart returns a cart_id as a string, addProductsToCart takes that ID plus an array of CartItemInput objects and, on success, returns the updated cart type, but on failure returns a user_errors array that exists alongside the cart data and must be checked too.
Type safety here concretely means: the generated type for AddProductsToCartMutation marks both cart and its individual items as nullable, because the schema allows exactly that, even though in practice the mutation almost always returns either data or errors, never both empty. Clean consuming code therefore checks user_errors.length first, before touching cart at all, and treats an empty cart despite the absence of errors as its own explicit error case instead of glossing over it with a non-null assertion.
// Typed cart mutation with explicit null-safe handling of Magento's response shape
import { useMutation } from '@apollo/client';
import { AddProductsToCartDocument, type AddProductsToCartMutation } from '../generated/graphql';
interface AddToCartResult {
success: boolean;
itemCount: number;
errorMessage?: string;
}
async function addProductToCart(
cartId: string,
sku: string,
quantity: number,
): Promise<AddToCartResult> {
const { data, errors } = await client.mutate<AddProductsToCartMutation>({
mutation: AddProductsToCartDocument,
variables: { cartId, cartItems: [{ sku, quantity }] },
});
// Network/GraphQL-level errors first, separate from Magento's business errors
if (errors?.length) {
return { success: false, itemCount: 0, errorMessage: errors[0].message };
}
// Magento models domain errors as data, not as GraphQL errors
const userErrors = data?.addProductsToCart?.user_errors ?? [];
if (userErrors.length > 0) {
return { success: false, itemCount: 0, errorMessage: userErrors[0]?.message ?? 'Unknown cart error' };
}
// Even without user_errors, cart can technically be null per the schema
const cart = data?.addProductsToCart?.cart;
if (!cart) {
return { success: false, itemCount: 0, errorMessage: 'Cart response was empty' };
}
return { success: true, itemCount: cart.total_quantity ?? 0 };
}
6. Handling Magento's Nullable-Heavy Schema Design in Everyday TypeScript
A central culture shock when moving from hand-written interfaces to codegen-generated types: Magento's GraphQL schema marks most fields as nullable, even where they're practically always present, such as product.name or cart.items. This isn't a weakness of codegen, it's an accurate reflection of a schema deliberately designed to be defensive against partial failures, disabled modules, and asynchronous data availability. The generated type string | null | undefined forces every consumer to deliberately handle that case instead of ignoring it.
Three strategies have become established in practice. The non-null assertion product!.name! is quickest to write, but shifts the risk unchanged into runtime, and should only appear where a missing value genuinely indicates a programming error. Optional chaining with sensible fallbacks, such as product?.name ?? 'Unnamed', is safer, but scatters fallback logic across the entire codebase. The most robust approach is a normalization layer that converts raw GraphQL data into strictly non-nullable domain types right after the query call, explicitly deciding what happens when data is missing instead of delegating that decision to every individual component.
7. Integrating Codegen Into the Build Pipeline: Catching Schema Changes Early
A Magento upgrade, a newly installed third-party module, or a custom schema.graphqls extension can rename fields, change types, or tighten nullability declarations without the storefront code automatically finding out. If graphql-codegen only runs locally and irregularly, such schema drift often goes undetected for weeks, until a component hits a suddenly missing field at runtime. The solution is to anchor the codegen run as a fixed CI step that runs against a current schema reference on every pull request.
A simple but effective check: the CI job runs graphql-codegen and then compares the diff of the generated graphql.ts against the checked-in version. An unexpected diff means either a deliberate schema change that needs to be versioned along with it, or an unwanted drift that surfaces before the merge instead of in production. In addition, tsc --noEmit in the same job checks whether existing query usages still compile after a schema change, reliably surfacing breaking changes in referenced fields long before a customer sees a broken product page.
8. Caching and Performance With Typed GraphQL Clients
Typed queries solve a correctness problem, but performance remains its own dimension that codegen alone doesn't cover. Persisted queries reduce network load by having the client send only a hash to the Magento endpoint instead of the full query string, which the server resolves against a previously registered query. This saves several kilobytes per request, especially for large, fragment-heavy product list queries, and combines well with generated TypedDocumentNode objects, since their hash can be derived deterministically from the query document.
Fragment colocation is the second important lever: instead of maintaining one large, central query with every field ever needed, each component defines its own fragment with exactly the fields it actually renders. A GraphQL client like Apollo automatically combines colocated fragments into a single network request, which eliminates over-fetching without components having to manually coordinate which fields have already been requested elsewhere. Codegen generates its own type for each fragment, so components still only see the fields they declared themselves.
9. Untyped Fetch Calls vs. Codegen-Typed Queries Compared
A direct comparison between untyped fetch calls and codegen-typed queries makes the practical difference tangible, especially in teams with changing membership and regular Magento upgrades.
| Aspect | Untyped Fetch Call | Codegen-Typed Query | Advantage |
|---|---|---|---|
| Error detection | Only at runtime in the browser | Compile error before deploy | Catch broken fields before production |
| IDE autocomplete | No knowledge of response shape | Exact, from the generated type | Faster, correct query writing |
| Refactoring safety | Manual search for field names | tsc finds every affected spot | Safe schema migrations |
| Handling nullability | Silently assumed | Explicit and visible in the type | Fewer undefined exceptions |
| Onboarding new developers | Response shape only from docs/DevTools | Types as living documentation | Faster ramp-up for the team |
Mironsoft
Headless Magento integrations with type-safe GraphQL connectivity
Type-safe GraphQL integration for your storefront?
We set up graphql-code-generator for your Magento endpoint, type product, cart, and checkout flows end to end, and anchor type checking firmly in your CI pipeline, so schema changes never land in production unnoticed.
Codegen Setup
Setting up codegen.yml, fragments, and TypedDocumentNode for Apollo or urql
Typing Cart & Checkout
Building clean nullable handling, user_errors checks, and a normalization layer
CI Integration
Codegen diff checks and tsc --noEmit against schema drift in the pipeline
10. Summary
TypeScript and Magento GraphQL together solve a problem that plain fetch calls structurally cannot: the GraphQL endpoint knows its own types, but the client doesn't, unless someone explicitly pulls that information into the project. graphql-code-generator closes exactly this gap by deriving types and typed document objects directly from the real schema, including every nullable declaration that Magento's schema deliberately hands out generously. Product queries with fragments, cart mutations with explicit user_errors handling, and a normalization layer for consistent domain types together form a solid foundation for headless and hybrid storefronts.
The effort of setup and CI integration pays off above all when several developers work on the storefront at once and Magento is updated regularly, because that's exactly when schema drift happens most often. Anyone who anchors codegen as a CI step instead of only running it locally turns an entire class of silent runtime errors into visible, early-catchable compile errors, and gains reliability that translates directly into fewer production incidents.
TypeScript and Magento GraphQL - The Essentials at a Glance
Schema as Contract
Introspection or an exported schema.graphql as a reliable source instead of outdated documentation.
Codegen Instead of Manual Work
graphql-code-generator produces types and TypedDocumentNode objects directly from query and schema.
Take Nullability Seriously
Choose non-null assertion, optional chaining, or a normalization layer deliberately, based on risk.
CI Protection Against Drift
Anchor a codegen diff check and tsc --noEmit per pull request, catch schema changes early.