- Automatically Generate TypeScript Types for Your Shopify Storefront Queries
Automatically Generate TypeScript Types for Your Shopify Storefront Queries
Say goodbye to manual typing — build a fully type-safe headless Shopify storefront using GraphQL Code Generator.

Building a headless Shopify storefront with Next.js should be exciting, but I found myself trapped in a frustrating cycle that was draining my productivity and confidence. Every time I needed to fetch products, manage cart operations, or query collections, I faced the same tedious process: write a GraphQL query, then manually craft TypeScript types that I hoped would match Shopify's ever-evolving schema.
The reality was far from ideal. I'd spend hours carefully typing out interfaces, second-guessing whether Shopify returned amount
as a string or number, whether fields were nullable, or what the exact structure of nested objects looked like. Worse yet, when I updated queries to add new fields or optimize performance, I'd inevitably forget to update the corresponding types. This led to a cascade of silent failures, mysterious runtime errors, and late-night debugging sessions trying to figure out why my perfectly valid-looking code was breaking in production.
What started as a simple e-commerce project became a maintenance nightmare. I was spending more time managing types than building features, and every schema change from Shopify felt like a potential disaster waiting to happen. There had to be a better way.
Before: Manual types that get outdated, are prone to errors, and require constant maintenance - turning feature development into a frustrating guessing game.
After: Automatically generated types that are always in sync with your GraphQL operations and Shopify's schema - letting you focus on building great user experiences instead of wrestling with type definitions.
What We'll Build
A complete GraphQL code generation system that:
- Scans all your GraphQL queries and fragments
- Connects to Shopify's multiple APIs (Storefront, Admin, Customer Account)
- Generates TypeScript types for every operation
- Creates a clean export system for easy importing
- Provides SDK functions for type-safe GraphQL requests
Tech Stack
- Next.js 15 with App Router
- Shopify Storefront API, Admin API, Customer Account API
- GraphQL Code Generator (
@graphql-codegen/cli
) -> This is crucial for this to work - GraphQL Request for client operations
Problem Setup
I had a Next.js 15 e-commerce app with dozens of GraphQL queries spread across multiple files. The queries are now organized by Shopify API type, making it clear which API each query targets:
lib/shopify/queries/
├── admin/
│ └── metaobjects.ts
├── customer-account/
│ └── customer.ts
└── storefront/
├── cart.ts
├── collection.ts
├── menu.ts
├── page.ts
├── product.ts
├── search.ts
└── ...12 total files
lib/shopify/fragments/
└── storefront/
├── product.ts
├── image.ts
├── cart.ts
└── seo.ts
lib/shopify/mutations/
└── storefront/
└── cart.ts
Each query file looked like this:
// The old way - manual types everywhere
export const getProductQuery = `
query getProduct($handle: String!) {
product(handle: $handle) {
id
title
handle
variants(first: 100) {
edges {
node {
id
price {
amount
currencyCode
}
}
}
}
}
}
`;
// Manual type definition - error-prone!
export type Product = {
id: string;
title: string;
handle: string;
variants: {
edges: Array<{
node: {
id: string;
price: {
amount: string; // Is this string or number?
currencyCode: string;
};
};
}>;
};
};
The Pain Points
- Type Mismatches: Shopify returns
amount
as a string, but I kept assuming it was a number - Missing Fields: Adding new fields to queries but forgetting to update types
- Fragment Management: Complex fragments shared between queries with no type safety
- Multiple APIs: Shopify has 3 different GraphQL APIs with different schemas
- Maintenance Nightmare: Every schema change required manual type updates
Why This Matters
In a headless commerce setup, your GraphQL queries are the foundation of your entire app. Wrong types lead to:
- Runtime errors in production
- Poor developer experience
- Bugs that are hard to track down
- Wasted time on manual type maintenance
Approach Exploration
I have considered couple of approaches.
Option 1: Continue Manual Types
Pros: Simple, no new tooling Cons: Error-prone, maintenance heavy, doesn't scale
Option 2: GraphQL Code Generator
Pros: Automatic, always in sync, supports multiple APIs, great DX Cons: Initial setup complexity, requires build step
Option 3: TypeScript + GraphQL Schema Introspection
Pros: No codegen needed Cons: Runtime overhead, complex setup for multiple APIs
Why I Chose GraphQL Code Generator
GraphQL Code Generator is the industry standard because it:
- Generates types at build time (no runtime cost)
- Supports complex multi-API setups like Shopify's
- Creates both types AND SDK functions
- Has excellent Next.js integration
- Handles fragments and complex nested structures perfectly
Implementation
Now comes the exciting part – setting up automatic type generation that will transform how you work with GraphQL in your Shopify project. We'll walk through this implementation step-by-step, and by the end, you'll have a system that automatically generates both TypeScript types and SDK functions for all your Shopify API operations.
The process involves several key steps: installing the necessary packages, configuring multiple Shopify API schemas, organizing your queries properly, and setting up the code generation pipeline. Each step builds on the previous one, so we'll take it methodically to ensure everything works perfectly.
Step 1: Install Dependencies
First, let's install the necessary packages for GraphQL code generation:
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-graphql-request @shopify/hydrogen-codegen npm install graphql-request graphql
What each package does:
@graphql-codegen/cli
: Main code generator CLI@graphql-codegen/typescript
: Generates base TypeScript types from GraphQL schema@graphql-codegen/typescript-operations
: Generates types for your specific queries/mutations@graphql-codegen/typescript-graphql-request
: Creates SDK functions for graphql-request client@shopify/hydrogen-codegen
: Provides Shopify's Customer Account API schema accessgraphql-request
: Lightweight GraphQL client (alternative to Apollo)
Step 2: Set Up Environment Variables
Create or update your .env
with Shopify API credentials. If you're deploying to Vercel, make sure to add these to your Vercel project's environment variables as well:
# Storefront API (public, for product/collection queries)
# Found in your Shopify headless app - use the "Storefront access token"
SHOPIFY_STOREFRONT_ACCESS_TOKEN=your_storefront_token
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=your-store.myshopify.com
# Admin API (private, for metafields/admin operations)
# Found in your Shopify private app - use the "Admin API access token"
SHOPIFY_ADMIN_API_ACCESS_TOKEN=your_admin_token
# Customer Account API (optional, for customer authentication)
# Found in your Shopify headless app - just the shop ID is needed for public schema access
NEXT_PUBLIC_SHOPIFY_SHOP_ID=your_shop_id
Why multiple APIs?
Shopify separates functionality across different GraphQL APIs:
- Storefront API: Public data (products, collections, cart)
- Admin API: Private operations (metafield definitions, admin queries)
- Customer Account API: Customer authentication and orders
For a deeper dive into how these APIs work together and their specific use cases, check out my comprehensive guide: Shopify Core APIs Overview.
Step 3: Create the Code Generator Configuration
This is where the magic happens! We're about to configure a system that will automatically scan all your GraphQL queries, connect to Shopify's schemas, and generate both precise TypeScript types and ready-to-use SDK functions. The best part? Once this is set up, every time you modify a query, the types update automatically – no more manual typing, no more guessing about field structures or nullability.
Create codegen.ts
in your project root:
import "dotenv/config";
import type { CodegenConfig } from "@graphql-codegen/cli";
import { getSchema } from "@shopify/hydrogen-codegen";
// Validate required environment variables
if (
!process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN ||
!process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN ||
!process.env.SHOPIFY_ADMIN_API_ACCESS_TOKEN
) {
throw new Error(
"Missing required Shopify env vars. Check: SHOPIFY_STOREFRONT_ACCESS_TOKEN, NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN, SHOPIFY_ADMIN_API_ACCESS_TOKEN"
);
}
/**
* Multi-API GraphQL Codegen Configuration
*
* This configuration generates separate TypeScript types for three Shopify APIs:
* - Storefront API: Most query files organized in storefront/ folder
* - Customer Account API: Uses Hydrogen's getSchema for proper schema access
* - Admin API: metaobjects.ts in admin/ folder
*/
const config: CodegenConfig = {
generates: {
// Storefront API - Most queries use this
"./lib/shopify/generated/storefront.ts": {
schema: {
[`https://${process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN}/api/2025-04/graphql.json`]:
{
headers: {
"X-Shopify-Storefront-Access-Token":
process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!,
},
},
},
documents: [
"./lib/shopify/queries/storefront/**/*.ts",
"./lib/shopify/mutations/storefront/**/*.ts",
"./lib/shopify/fragments/storefront/**/*.ts",
],
plugins: [
"typescript",
"typescript-operations",
"typescript-graphql-request",
],
},
// Admin API - metafields.ts and metaobjects.ts (definitions only)
"./lib/shopify/generated/admin.ts": {
schema: {
[`https://${process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN}/admin/api/2024-01/graphql.json`]:
{
headers: {
"X-Shopify-Access-Token":
process.env.SHOPIFY_ADMIN_API_ACCESS_TOKEN!,
},
},
},
documents: ["./lib/shopify/queries/admin/**/*.ts"],
plugins: [
"typescript",
"typescript-operations",
"typescript-graphql-request",
],
},
// Customer Account API - Uses Hydrogen's getSchema for proper OAuth handling
"./lib/shopify/generated/customer-account.ts": {
schema: getSchema("customer-account"),
documents: ["./lib/shopify/queries/customer-account/**/*.ts"],
plugins: [
"typescript",
"typescript-operations",
"typescript-graphql-request",
],
},
},
config: {
avoidOptionals: true,
maybeValue: "T | null",
},
};
export default config;
Configuration breakdown:
generates
: Defines what files to create and from which sourcesschema
: Points to Shopify's GraphQL schema endpoints with authenticationdocuments
: Array of files containing your GraphQL operationsplugins
: What to generate (base types, operation types, SDK functions)config.avoidOptionals
: Makes nullable fields explicit (T | null
instead ofT?
)
Step 4: Fix Your Query Format
Critical Step: GraphQL Code Generator only recognizes queries with the /* GraphQL */
comment.
Before (doesn't work with codegen):
export const getProductQuery = `
query getProduct($handle: String!) {
product(handle: $handle) {
id
title
}
}
`;
After (works with codegen):
export const getProductQuery = /* GraphQL */ `
query getProduct($handle: String!) {
product(handle: $handle) {
id
title
}
}
`;
Why the comment matters:
The /* GraphQL */
comment is a special marker that tells the code generator "this string contains a GraphQL operation." Without it, the generator skips the query entirely.
Step 5: Understanding Generated Type Names
GraphQL codegen automatically creates type names based on your operation names, following a predictable pattern.
Type Naming Rules
GraphQL codegen uses this pattern:
[OperationName] + [OperationType] + ["Variables" for variables]
Examples
For this query:
export const getProductsViaSearchQuery = /* GraphQL */ `
query SearchProducts($query: String!, $first: Int) {
search(query: $query, first: $first) {
totalCount
edges { node { ... } }
}
}
`;
Generated type names:
- Variables:
SearchProductsQueryVariables
- Result:
SearchProductsQuery
- SDK Function:
SearchProducts()
The Pattern Breakdown
- Operation Name:
SearchProducts
(from your GraphQL operation) - + Operation Type:
Query
(automatically detected and appended) - + "Variables":
Variables
(for input variables)
More examples:
query getProduct → GetProductQuery + GetProductQueryVariables
query predictiveSearch → PredictiveSearchQuery + PredictiveSearchQueryVariables
mutation addToCart → AddToCartMutation + AddToCartMutationVariables
Key Insights:
The JavaScript variable name like getProductsViaSearchQuery
is completely ignored by the code generator. Only the GraphQL operation name like SearchProducts
matters for type generation. The system automatically appends "Query", "Mutation", or "Subscription" to create the final type names, so you should never include the operation type in your operation name to avoid redundancy like SearchProductsQueryQuery
.
Best Practice:
// Good - clear operation name
query SearchProducts { ... } → SearchProductsQuery
// Bad - redundant naming
query SearchProductsQuery { ... } → SearchProductsQueryQuery
Step 6: Structure Your Query Files
Organize your queries with proper fragments for reusability:
// lib/shopify/fragments/product.ts
import imageFragment from "./image";
import seoFragment from "./seo";
const productFragment = /* GraphQL */ `
fragment product on Product {
id
handle
title
description
availableForSale
featuredImage {
...image
}
variants(first: 100) {
edges {
node {
id
title
price {
amount
currencyCode
}
selectedOptions {
name
value
}
}
}
}
seo {
...seo
}
}
${imageFragment}
${seoFragment}
`;
export default productFragment;
// lib/shopify/queries/product.ts
import productFragment from "../fragments/product";
export const getProductQuery = /* GraphQL */ `
query getProduct($handle: String!) {
product(handle: $handle) {
...product
}
}
${productFragment}
`;
export const getProductsQuery = /* GraphQL */ `
query getProducts($first: Int, $query: String, $sortKey: ProductSortKeys) {
products(first: $first, query: $query, sortKey: $sortKey) {
edges {
node {
...product
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
${productFragment}
`;
Fragment benefits:
- Reusability: Define common field sets once, use everywhere
- Consistency: Ensures the same fields are fetched across different queries
- Maintainability: Update fragment once, all queries get updated
- Type Safety: Generated types automatically include fragment fields
Step 7: Add Package.json Script
Add the codegen script to your package.json
:
{
"scripts": {
"codegen": "graphql-codegen --config codegen.ts",
"codegen:watch": "graphql-codegen --config codegen.ts --watch"
}
}
Step 8: Run Code Generation
Execute the type generation:
npm run codegen
What happens during generation:
- Codegen reads your configuration
- Fetches GraphQL schemas from Shopify APIs
- Scans all files in your
documents
arrays - Extracts GraphQL operations (queries, mutations, fragments)
- Matches operations against schemas
- Generates TypeScript types for each operation
- Creates SDK functions for making requests
You should see output like:
[SUCCESS] Generate to ./lib/shopify/generated/storefront.ts
[SUCCESS] Generate to ./lib/shopify/generated/admin.ts
Step 9: Create a Clean Export System
Create lib/shopify/types/index.ts
to organize all your types:
// Clean exports for all Shopify API types
// This file combines both generated types and manual types for easy importing
// Re-export generated types from Storefront API
export type {
// Operation types
GetProductQuery,
GetProductQueryVariables,
GetProductsQuery,
GetProductsQueryVariables,
PredictiveSearchQuery,
PredictiveSearchQueryVariables,
// Schema types
Product as GeneratedProduct,
ProductVariant as GeneratedProductVariant,
MoneyV2 as GeneratedMoney,
Image as GeneratedImage,
Collection as GeneratedCollection,
} from "../generated/storefront";
// Re-export generated types from Admin API
export type {
GetMetafieldsQuery,
GetMetafieldsQueryVariables,
MetafieldDefinition as GeneratedMetafieldDefinition,
} from "../generated/admin";
// Re-export any remaining manual types that extend generated ones
export type {
// Custom types that reshape or extend generated types
Cart,
CartItem,
Product,
Collection,
// ... other manual types you still need
} from "../types";
// Convenient type aliases for common use cases
import type { Product, Cart, Collection } from "../types";
export type ProductWithVariants = Product;
export type CartWithItems = Cart;
export type CollectionWithProducts = Collection;
// Operation result types for GraphQL requests
export type StorefrontQueryResult<T> = {
data: T;
errors?: Array<{
message: string;
locations?: Array<{
line: number;
column: number;
}>;
path?: Array<string | number>;
}>;
extensions?: Record<string, any>;
};
FAQ: Troubleshooting Common Issues
Common Mistakes and Solutions
Missing /* GraphQL */
Comment
Error:
No operations found in documents
What happened: Without the special comment, codegen couldn't identify which strings contained GraphQL operations.
Fix:
Add /* GraphQL */
before every query template literal.
Wrong API Configuration
Error:
Field 'customer' doesn't exist on type 'QueryRoot'
What happened: Customer queries were in the Storefront API configuration instead of Customer Account API.
Fix:
Move customer-related queries to the correct API configuration in codegen.ts
.
Fragment Import Issues
Error:
Unknown fragment "product"
What happened: Fragment wasn't properly imported or the template literal composition was incorrect.
Fix:
// Correct fragment usage
export const getProductQuery = /* GraphQL */ `
query getProduct($handle: String!) {
product(handle: $handle) {
...product
}
}
${productFragment} // ← Fragment must be composed here
`;
Environment Variables Not Loaded
Error:
Missing required Shopify env vars
What happened:
The dotenv/config
import in codegen.ts
couldn't find environment variables.
Fix:
Ensure your .env.local
file is in the project root and contains all required variables.
Debugging Commands
Verify Generated Types
# Check what types were actually generated
grep -E "export type.*Query" lib/shopify/generated/storefront.ts | head -10
Test Type Compilation
# Verify TypeScript compilation works
npx tsc --noEmit lib/shopify/types/index.ts
Query Extraction Checklist
If a query isn't generating types, ensure:
- File is listed in
codegen.ts
documents array - Query has
/* GraphQL */
comment - Query syntax is valid GraphQL
- Operation has a name (
query getProduct
not justquery
)
Final Working Version
Using the Generated Types
Here's how to use your generated types in a Next.js component:
// components/ProductCard.tsx
import { getSdk } from '@/lib/shopify/generated/storefront';
import { GraphQLClient } from 'graphql-request';
import type {
GetProductQuery,
GetProductQueryVariables
} from '@/lib/shopify/types';
// Create GraphQL client
const client = new GraphQLClient(
`https://${process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN}/api/2025-04/graphql.json`,
{
headers: {
'X-Shopify-Storefront-Access-Token': process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!,
},
}
);
// Get SDK with full type safety
const sdk = getSdk(client);
export async function ProductCard({ handle }: { handle: string }) {
// Fully typed variables
const variables: GetProductQueryVariables = { handle };
// Fully typed response
const response: GetProductQuery = await sdk.getProduct(variables);
const product = response.product;
if (!product) {
return <div>Product not found</div>;
}
return (
<div>
<h2>{product.title}</h2>
<p>{product.description}</p>
{product.featuredImage && (
<img
src={product.featuredImage.url}
alt={product.featuredImage.altText || product.title}
width={product.featuredImage.width || 300}
height={product.featuredImage.height || 300}
/>
)}
<div>
${product.priceRange.minVariantPrice.amount} {product.priceRange.minVariantPrice.currencyCode}
</div>
</div>
);
}
Project Structure Overview
Your final project structure should look like:
├── codegen.ts # Code generator configuration
├── lib/shopify/
│ ├── queries/ # GraphQL queries
│ │ ├── product.ts # Product queries
│ │ ├── cart.ts # Cart operations
│ │ ├── search.ts # Search functionality
│ │ └── ...
│ ├── fragments/ # Reusable GraphQL fragments
│ │ ├── product.ts # Product fields
│ │ ├── image.ts # Image fields
│ │ └── ...
│ ├── generated/ # Auto-generated files (don't edit!)
│ │ ├── storefront.ts # Storefront API types
│ │ ├── admin.ts # Admin API types
│ │ └── customer-account.ts # Customer API types
│ ├── types/
│ │ ├── index.ts # Clean exports
│ │ └── types.ts # Legacy manual types
│ └── utils/
│ └── clients.ts # GraphQL client setup
└── package.json # Scripts for codegen
Development Workflow
- Write or modify a GraphQL query with
/* GraphQL */
comment - Run code generation:
npm run codegen
- Import generated types in your components
- Use the SDK functions for type-safe requests
- Repeat as you add new features
Key benefits of this approach:
- Always in sync: Types match your actual queries
- Compile-time safety: Catch errors before runtime
- Great DX: Autocomplete and IntelliSense for all fields
- SDK functions: No need to write GraphQL request boilerplate
- Multi-API support: Handle Shopify's complex API structure
Optional Improvements & Best Practices
1. Automated Code Generation
Add codegen to your build process:
{
"scripts": {
"build": "npm run codegen && next build",
"dev": "npm run codegen && next dev"
}
}
2. Git Integration
Add generated files to .gitignore
or commit them:
# Option 1: Ignore generated files (regenerate on each deploy) lib/shopify/generated/ # Option 2: Commit generated files (faster deploys, easier debugging) # Keep generated files in git
Tip: Committing generated files makes deployments faster but creates larger diffs. Choose based on your team's preference.
3. Type-Safe Client Wrapper
For the Customer Account API, create a specialized client that handles authentication properly:
// lib/shopify/utils/customer-account-sdk.ts
import { GraphQLClient } from "graphql-request";
import { getSdk } from "../generated/customer-account";
import { SHOPIFY_CUSTOMER_ACCOUNT_API_ENDPOINT } from "lib/constants";
/**
* Creates Customer Account API SDK client with authentication
*/
export function createCustomerAccountSdk(accessToken: string) {
// Create a client with custom fetch that handles token formatting
const client = new GraphQLClient(SHOPIFY_CUSTOMER_ACCOUNT_API_ENDPOINT, {
fetch: async (url: RequestInfo | URL, init?: RequestInit) => {
const { formatAccessToken } = await import("lib/auth/session");
const formattedToken = formatAccessToken(accessToken);
return fetch(url, {
...init,
headers: {
...init?.headers,
"Content-Type": "application/json",
Authorization: formattedToken, // Customer Account API expects token directly, no "Bearer" prefix
},
});
},
});
return getSdk(client);
}
// Export for type inference helpers
export type CustomerAccountSdk = ReturnType<typeof createCustomerAccountSdk>;
4. Cache Integration
Integrate with Next.js caching:
// lib/shopify/cached-client.ts
import { unstable_cache } from "next/cache";
import { shopifyClient } from "./client";
export const getCachedProduct = unstable_cache(
async (handle: string) => {
return shopifyClient.getProduct({ handle });
},
["product"],
{ revalidate: 3600 } // 1 hour cache
);
5. Development Scripts
Add helpful development scripts:
{
"scripts": {
"codegen": "graphql-codegen --config codegen.ts",
"codegen:watch": "graphql-codegen --config codegen.ts --watch",
"codegen:check": "graphql-codegen --config codegen.ts --check",
"types:check": "tsc --noEmit"
}
}
Summary
By implementing GraphQL Code Generation, you've transformed your Shopify headless storefront from error-prone manual types to a robust, automatically-maintained type system.
What you've gained:
- Type Safety: Compile-time errors instead of runtime surprises
- Always In Sync: Types automatically match your queries
- Better DX: Full autocomplete and IntelliSense support
- SDK Functions: No more manual GraphQL request boilerplate
- Multi-API Support: Clean handling of Shopify's complex API structure
- Maintainability: Add new queries without manual type work
The key insight: The simple /* GraphQL */
comment is what unlocks automatic type generation. This small change eliminates hours of manual type maintenance and prevents entire categories of bugs.
Your codebase is now more reliable, maintainable, and developer-friendly. As you add new features, the type system will grow automatically with your queries, keeping everything in perfect sync.
Thanks, Matija
Frequently Asked Questions
Comments
You might be interested in

14th August 2025

3rd August 2025

2nd August 2025