Last month, I was working on a client's e-commerce project when I realized something frustrating: we had this powerful GraphQL Code Generation setup that automatically creates TypeScript types and SDK functions, but we were still making API calls the old way - manually typing everything and dealing with nested response structures. This is part of building a complete headless Shopify storefront with Next.js.
After setting up automatic TypeScript type generation for Shopify Storefront queries, I discovered we had this beautiful getSdk function that creates fully-typed methods for every GraphQL query. Yet somehow, we were ignoring it and sticking with the manual shopifyFetch approach.
This guide shows you exactly how to migrate from those manual API calls to the generated SDK, eliminating boilerplate code and getting compile-time validation for every GraphQL operation. Note: You'll need to complete the GraphQL codegen setup first - the linked article above walks through that entire process.
The Problem: Ignoring Our Own Generated SDK
Here's what our code looked like before the migration:
typescript
// The manual approach we were stuck inconst { body }: { body: ShopifyProductHandlesOperation } =
await shopifyFetch<ShopifyProductHandlesOperation>({
query: getProductHandlesQuery,
variables: { first: 250, after: after || undefined }
});
const products = removeEdgesAndNodes(body.data.products) asShopifyProductHandle[];
The irony? Our GraphQL codegen setup was already generating a getSdk function that creates perfectly typed methods for every query. We just weren't using it.
After completing the automatic TypeScript generation setup, we had everything needed for type-safe operations - we just needed to actually use the generated SDK instead of the manual approach.
The Solution: Direct SDK Usage
The fix was surprisingly simple: use the generated SDK functions directly instead of the manual approach. Here's the transformation:
typescript
// Before: Manual with lots of boilerplateconst { body } = await shopifyFetch<ShopifyProductHandlesOperation>({
query: getProductHandlesQuery,
variables: { first: 250 }
});
const products = body.data.products;
// After: Clean SDK callconst result = await storefrontSdk.getProductHandles({ first: 250 });
const products = result.products; // Direct access!
This approach eliminates all the manual type specifications, string-based queries, and nested data access while providing full compile-time validation.
Implementation Steps
Step 1: Set Up the SDK Client
First, install the GraphQL client library and create your SDK wrapper:
The key here is getSdk(client) - this function was automatically generated by GraphQL Code Generation and creates typed methods for every query in your schema. Each query becomes a method on the SDK with full TypeScript support and compile-time validation.
Step 2: The Migration Pattern
Here's the step-by-step transformation pattern. Let's convert a real function that fetches product handles with pagination:
Cleaner variable passing: No need for intermediate queryVariables object
Direct result access: result.collection instead of res.body.data.collection
Same business logic: All the data transformation and error handling logic remains unchanged
Better IntelliSense: Your IDE now shows available fields as you type
✅ Best Practice: Keep your existing business logic (data transformation, error handling, caching) unchanged. Only replace the data fetching layer.
Pitfalls & Debugging
Common Migration Mistakes
1. Forgetting to update data access patterns
typescript
// ❌ Wrong - still using old patternconst result = await storefrontSdk.getProduct({ handle });
return result.body.data.product; // body.data doesn't exist!// ✅ Correct - direct accessconst result = await storefrontSdk.getProduct({ handle });
return result.product;
2. Not removing unused imports
typescript
// ❌ This will cause build warnings/errorsimport { getProductQuery } from"../../queries/product"; // Not needed anymoreimport { ShopifyProductOperation } from"../../types"; // Not needed anymore// ✅ Clean importsimport { Product } from"../../types"; // Only what you actually use
3. Assuming error handling is the same
The SDK has its own error handling mechanisms. If you had custom error wrapping, you might need to adjust:
typescript
// Old error handlingtry {
const res = await shopifyFetch<Type>({ query, variables });
} catch (e) {
if (isShopifyError(e)) {
throw { cause: e.cause, status: e.status, message: e.message, query: 'functionName' };
}
}
// New - let SDK handle errors naturally or wrap if neededtry {
const result = await storefrontSdk.functionName(variables);
} catch (e) {
// SDK already provides good error informationconsole.error('GraphQL Error:', e);
throw e;
}
Debugging Tips
When builds fail after migration:
Check that all handleSdkRequest calls are removed
Verify imports are cleaned up
Make sure data access patterns are updated (result.field not result.body.data.field)
When TypeScript complains:
The SDK validates variables at compile time - check parameter names match your GraphQL schema
If you get "Property doesn't exist" errors, check the GraphQL schema for the correct field names
When runtime errors occur:
Use browser dev tools to inspect the actual GraphQL response
Check that environment variables (API tokens, endpoints) are still correctly configured
⚠️ Common Bug: If you see "Cannot read property 'X' of undefined", you're likely still trying to access body.data.something instead of just something.
Final Working Version
Here's a complete before/after comparison of a migrated file:
100% type safe - compile-time validation of all parameters
Zero manual types - no more <ShopifyProductOperation>
Direct data access - no more body.data nesting
Auto-completion - IDE knows exactly what fields are available
Results and Benefits
Developer Experience Improvements
Before migration:
typescript
// Lots of manual work, easy to make mistakesconst res = await shopifyFetch<ShopifyProductOperation>({
query: getProductQuery, // Could be wrong stringvariables: {
handle, // Could misspell parameter namefirst: 10, // Might not be valid for this query
},
});
const product = res.body.data.product; // Deep nesting
After migration:
typescript
// Clean, validated, auto-completedconst result = await storefrontSdk.getProduct({
handle, // TypeScript validates this exists// first: 10 // Would error - getProduct doesn't accept 'first'
});
const product = result.product; // Direct access
Performance Benefits
Smaller bundle size: No need to ship query strings to the client
Better tree shaking: Only used SDK functions are included in the bundle
Fewer runtime errors: Type validation catches issues at compile time
Maintenance Benefits
Schema changes: When GraphQL schema updates, regenerate types and get compile errors for breaking changes
Refactoring: Renaming fields in GraphQL automatically updates TypeScript types
Documentation: SDK functions include JSDoc comments from GraphQL schema
Migration time per file: 10-30 minutes each, following the same pattern shown above.
Common Migration Pitfalls
The biggest mistake: Forgetting to update data access patterns.
typescript
// ❌ Wrong - still using old nested patternconst result = await storefrontSdk.getProduct({ handle });
return result.body.data.product; // body.data doesn't exist!// ✅ Correct - direct access const result = await storefrontSdk.getProduct({ handle });
return result.product;
Other common issues:
Not removing unused imports (causes build warnings)
Assuming error handling works the same way (SDK has built-in error handling)
Trying to access fields that don't exist in the GraphQL schema
Results: From 50+ Lines to 15
After migrating all our Shopify API calls, we eliminated over 50% of our GraphQL-related code while gaining complete compile-time type safety. The transformation from shopifyFetch<Type>({ query, variables }) to storefrontSdk.methodName(variables) made our codebase cleaner and more maintainable.
The real win isn't just shorter code - it's the developer experience. Your IDE now shows exactly which parameters each query accepts, what fields are available in responses, and catches typos before they become runtime errors.
If you've set up GraphQL Code Generation but are still using manual API calls, this migration will pay dividends immediately. The pattern is consistent across all query types, making it straightforward to apply across your entire codebase.
Let me know in the comments if you have questions about migrating your own GraphQL calls, and subscribe for more practical development guides.