TypeScript and Magento GraphQL: Type-Safe Storefront Integration
AI generated
<T>
type
TypeScript · Magento GraphQL · Headless · Storefront
TypeScript and Magento GraphQL
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.

16 min read GraphQL Codegen · Nullability · Typed Queries Magento 2.4.8 · Apollo/urql · GraphQL Schema

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.

11. FAQ: TypeScript and Magento GraphQL

1What does graphql-code-generator actually do?
Reads the schema and the project's queries, then automatically generates TypeScript types and typed document objects for the GraphQL client.
2Where do I get Magento's GraphQL schema for codegen?
Via introspection against the running /graphql endpoint, or as an exported schema.graphql file, pulled again per environment because of custom modules.
3Why are so many fields in Magento's GraphQL schema nullable?
Defensive schema design against partial failures and disabled modules. Even practically always-present fields therefore count as nullable.
4Non-null assertion or optional chaining, which is better?
Non-null assertion is quick but risky. Optional chaining with a fallback is safer, a normalization layer is usually the most robust.
5What are TypedDocumentNode objects and what are they for?
They bundle the query, input type, and output type together, so clients like Apollo or urql derive the types automatically, without a manual cast.
6How do I handle user_errors in Magento GraphQL mutations in a type-safe way?
Check the length of user_errors first, then access the actual data field. Treat empty data despite no errors as a separate case.
7Should codegen be part of the CI pipeline?
Yes, a CI job with a codegen run, diff check, and tsc --noEmit makes schema drift visible before it reaches production.
8What are persisted queries, and are they worth it for Magento storefronts?
They send only a hash instead of the full query string, which reduces network load for large queries and combines well with generated types.
9What does fragment colocation mean in this context?
Each component defines its own fragment with exactly the fields it needs, the client merges them automatically and avoids over-fetching.
10Does this approach also work with PWA Studio instead of a custom frontend?
Yes, PWA Studio uses the same GraphQL endpoint and can be typed independently of the frontend framework with graphql-code-generator.