---
title: "Stop Writing Manual GraphQL Calls: Migrate to Generated SDK in 30 Minutes"
slug: "eliminate-graphql-boilerplate-generated-sdk"
published: "2025-08-14"
updated: "2025-12-25"
validated: "2025-10-20"
categories:
  - "Shopify"
tags:
  - "graphql codegen"
  - "shopify storefront api"
  - "typescript sdk"
  - "graphql-request"
  - "nextjs graphql"
  - "api migration"
  - "type safety"
  - "developer experience"
  - "shopify headless"
llm-intent: "reference"
audience-level: "intermediate"
framework-versions:
  - "graphql code generation"
  - "next.js 15"
  - "typescript"
  - "shopify storefront api"
  - "graphql-request"
status: "stable"
llm-purpose: "Complete guide to migrating from manual GraphQL calls to generated SDK in Next.js. Eliminate boilerplate, get type safety, and boost DEX."
llm-prereqs:
  - "Access to GraphQL Code Generation"
  - "Access to Next.js 15"
  - "Access to TypeScript"
  - "Access to Shopify Storefront API"
  - "Access to graphql-request"
llm-outputs:
  - "Migrated Docker stack running on target VPS"
  - "Validated volume backups and restored data"
---

**Summary Triples**
- (GraphQL Code Generator, generates, getSdk function which contains typed methods for each GraphQL operation)
- (getSdk, requires, a GraphQL client instance (e.g., graphql-request's GraphQLClient) to instantiate SDK methods)
- (Manual shopifyFetch pattern, is replaced by, calling sdk.<operationName>(variables) returned by getSdk)
- (SDK method responses, are, TypeScript-typed according to the operation, enabling compile-time validation)
- (Migration, reduces, GraphQL boilerplate (example claims ~50% reduction))
- (Pagination variables, continue to be, passed as method arguments to SDK methods (e.g., { first, after }))
- (removeEdgesAndNodes helper, may still be needed when, schema returns edges/nodes; SDK does not automatically flatten unless you configure codegen plugins)
- (Codegen must be run, before, you can import getSdk or consume generated types)
- (Runtime behavior, remains, equivalent to manual calls (network requests unchanged); benefit is compile-time safety and smaller source code)
- (Tests, should be updated to, mock or stub SDK methods (or GraphQLClient) rather than low-level fetch wrappers)

### {GOAL}
Complete guide to migrating from manual GraphQL calls to generated SDK in Next.js. Eliminate boilerplate, get type safety, and boost DEX.

### {PREREQS}
- Access to GraphQL Code Generation
- Access to Next.js 15
- Access to TypeScript
- Access to Shopify Storefront API
- Access to graphql-request

### {STEPS}
1. Install graphql-request dependency
2. Create SDK client wrapper
3. Transform function calls
4. Update data access patterns
5. Clean up imports
6. Verify type safety

<!-- llm:goal="Complete guide to migrating from manual GraphQL calls to generated SDK in Next.js. Eliminate boilerplate, get type safety, and boost DEX." -->
<!-- llm:prereq="Access to GraphQL Code Generation" -->
<!-- llm:prereq="Access to Next.js 15" -->
<!-- llm:prereq="Access to TypeScript" -->
<!-- llm:prereq="Access to Shopify Storefront API" -->
<!-- llm:prereq="Access to graphql-request" -->
<!-- llm:output="Migrated Docker stack running on target VPS" -->
<!-- llm:output="Validated volume backups and restored data" -->

# Stop Writing Manual GraphQL Calls: Migrate to Generated SDK in 30 Minutes
> Complete guide to migrating from manual GraphQL calls to generated SDK in Next.js. Eliminate boilerplate, get type safety, and boost DEX.
Matija Žiberna · 2025-08-14

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](https://www.buildwithmatija.com/blog/shopify-headless-vs-liquid-when-to-choose).

After setting up [automatic TypeScript type generation for Shopify Storefront queries](https://www.buildwithmatija.com/blog/shopify-typescript-codegen-nextjs), 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 in
const { body }: { body: ShopifyProductHandlesOperation } = 
  await shopifyFetch<ShopifyProductHandlesOperation>({
    query: getProductHandlesQuery,
    variables: { first: 250, after: after || undefined }
  });

const products = removeEdgesAndNodes(body.data.products) as ShopifyProductHandle[];
```

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](https://www.buildwithmatija.com/blog/shopify-typescript-codegen-nextjs), 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 boilerplate
const { body } = await shopifyFetch<ShopifyProductHandlesOperation>({
  query: getProductHandlesQuery,
  variables: { first: 250 }
});
const products = body.data.products;

// After: Clean SDK call
const 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:

```bash
pnpm add graphql-request
```

Create the SDK client:

```typescript
// File: lib/shopify/utils/storefront-sdk.ts
import { GraphQLClient } from 'graphql-request';
import { getSdk } from '../generated/storefront';
import { ensureStartsWith } from 'lib/utils';
import { SHOPIFY_GRAPHQL_API_ENDPOINT } from 'lib/constants';

const domain = process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN
  ? ensureStartsWith(process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN, 'https://')
  : '';

const endpoint = `${domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}`;

const client = new GraphQLClient(endpoint, {
  headers: {
    'X-Shopify-Storefront-Access-Token': process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!,
  },
});

export const storefrontSdk = getSdk(client);
export type StorefrontSdk = ReturnType<typeof getSdk>;
```

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:

**Before - Manual approach:**
```typescript
// File: lib/shopify/fetchers/storefront/products.ts
import { shopifyFetch } from "../../utils/clients";
import { getProductHandlesQuery } from "../../queries/product";

const getAllProductHandles = async (): Promise<string[]> => {
  // ... pagination setup ...
  
  while (hasNextPage) {
    const { body }: { body: ShopifyProductHandlesOperation } =
      await shopifyFetch<ShopifyProductHandlesOperation>({
        query: getProductHandlesQuery, // String-based query
        variables: {
          first: 250,
          after: after || undefined,
          query: `-tag:${HIDDEN_PRODUCT_TAG}`,
        },
      });

    const products = removeEdgesAndNodes(
      body.data.products, // Nested access
    ) as ShopifyProductHandle[];

    // ... handle results ...
    hasNextPage = body.data.products.pageInfo.hasNextPage;
    after = body.data.products.pageInfo.endCursor;
  }
};
```

**After - SDK approach:**
```typescript
// File: lib/shopify/fetchers/storefront/products.ts
import { storefrontSdk } from "../../utils/storefront-sdk";

const getAllProductHandles = async (): Promise<string[]> => {
  // ... pagination setup ...
  
  while (hasNextPage) {
    const result = await storefrontSdk.getProductHandles({
      first: 250,
      after: after, // SDK handles null/undefined automatically
      query: `-tag:${HIDDEN_PRODUCT_TAG}`,
    });

    const products = removeEdgesAndNodes(
      result.products, // Direct access - no body.data!
    ) as ShopifyProductHandle[];

    // ... handle results ...
    hasNextPage = result.products.pageInfo.hasNextPage;
    after = result.products.pageInfo.endCursor;
  }
};
```

The transformation eliminates three key pain points:

1. **Import changes:** Replace `shopifyFetch` and string query imports with just `storefrontSdk`
2. **Function calls:** `shopifyFetch<Type>({ query, variables })` becomes `storefrontSdk.functionName(variables)` - no more manual type specifications
3. **Data access:** `body.data.products` becomes `result.products` - direct access to your data

The SDK automatically validates parameter names at compile-time and infers return types, catching errors before they reach production.

### Step 3: Clean Up Imports

After migration, remove the imports you no longer need:

```typescript
// ❌ Remove these - no longer needed
import { getProductHandlesQuery } from "../../queries/product"; 
import { ShopifyProductHandlesOperation } from "../../types";

// ✅ Keep these - still needed for data transformation  
import { ShopifyProductHandle } from "../../types";
```

The SDK has the queries and operation types baked in, but you'll still need types for data transformation after receiving the results.

### Step 4: Complex Example - Collection Products

Here's a more complex function showing the same pattern with filters and pagination:

**Before - Manual approach:**
```typescript
const getProductsInCollection_original = async ({
productFilters = [],
reverse,
sortKey,
collection,
first,
last,
after,
before,
}: CollectionProductsParams): Promise<CollectionProductsResult> => {

    const queryVariables = {
      handle: collection,
      filters: productFilters.length > 0 ? productFilters : undefined,
      reverse,
      sortKey: mapSortKeyForContext(sortKey || "COLLECTION_DEFAULT", true),
      first,
      last,
      after,
      before,
    };

    const res = await shopifyFetch<ShopifyCollectionProductsOperation>({
      query: getCollectionProductsQuery,
      variables: queryVariables,
    });

    if (!res.body.data.collection) {
      return { products: [], pageInfo: {...}, filters: [], totalCount: 0 };
    }

    const products = reshapeProducts(
      res.body.data.collection.products.edges.map((edge) => edge.node),
    );

    return {
      products,
      pageInfo: res.body.data.collection.products.pageInfo,
      filters: res.body.data.collection.products.filters,
      totalCount: calculateTotalFromFilters(res.body.data.collection.products.filters),
    };

};
```

**After - SDK approach:**
```typescript
const getProductsInCollection_original = async ({
productFilters = [],
reverse,
sortKey,
collection,
first,
last,
after,
before,
}: CollectionProductsParams): Promise<CollectionProductsResult> => {

    const result = await storefrontSdk.getCollectionProducts({
      handle: collection,
      filters: productFilters.length > 0 ? productFilters : undefined,
      reverse,
      sortKey: mapSortKeyForContext(sortKey || "COLLECTION_DEFAULT", true),
      first,
      last,
      after,
      before,
    });

    if (!result.collection) {
      return { products: [], pageInfo: {...}, filters: [], totalCount: 0 };
    }

    const products = reshapeProducts(
      result.collection.products.edges.map((edge) => edge.node),
    );

    return {
      products,
      pageInfo: result.collection.products.pageInfo,
      filters: result.collection.products.filters,
      totalCount: calculateTotalFromFilters(result.collection.products.filters),
    };

};
```

**Key improvements:**

- 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 pattern
const 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;
```

**2. Not removing unused imports**
```typescript
// ❌ This will cause build warnings/errors
import { getProductQuery } from "../../queries/product"; // Not needed anymore
import { ShopifyProductOperation } from "../../types"; // Not needed anymore

// ✅ Clean imports
import { 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 handling
try {
  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 needed
try {
  const result = await storefrontSdk.functionName(variables);
} catch (e) {
  // SDK already provides good error information
  console.error('GraphQL Error:', e);
  throw e;
}
```

### Debugging Tips

**When builds fail after migration:**

1. Check that all handleSdkRequest calls are removed
2. Verify imports are cleaned up
3. Make sure data access patterns are updated (result.field not result.body.data.field)

**When TypeScript complains:**

1. The SDK validates variables at compile time - check parameter names match your GraphQL schema
2. If you get "Property doesn't exist" errors, check the GraphQL schema for the correct field names

**When runtime errors occur:**

1. Use browser dev tools to inspect the actual GraphQL response
2. 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:

**Before (Manual approach):**
```typescript
// lib/shopify/fetchers/storefront/products.ts
import { shopifyFetch } from "../../utils/clients";
import { getProductQuery, getProductRecommendationsQuery } from "../../queries/product";
import { ShopifyProductOperation, ShopifyProductRecommendationsOperation } from "../../types";

export async function getProduct(handle: string): Promise<Product | undefined> {
  const res = await shopifyFetch<ShopifyProductOperation>({
    query: getProductQuery,
    variables: { handle },
  });

  return reshapeProducts([res.body.data.product])[0];
}

export async function getProductRecommendations(productId: string): Promise<Product[]> {
  const res = await shopifyFetch<ShopifyProductRecommendationsOperation>({
    query: getProductRecommendationsQuery,
    variables: { productId },
  });

  return reshapeProducts(res.body.data.productRecommendations);
}
```

**After (SDK approach):**
```typescript
// lib/shopify/fetchers/storefront/products.ts
import { storefrontSdk } from "../../utils/storefront-sdk";

export async function getProduct(handle: string): Promise<Product | undefined> {
  const result = await storefrontSdk.getProduct({ handle });

  return reshapeProducts([result.product])[0];
}

export async function getProductRecommendations(productId: string): Promise<Product[]> {
  const result = await storefrontSdk.getProductRecommendations({ productId });

  return reshapeProducts(result.productRecommendations);
}
```

**The transformation:**

- 50% less code - eliminated boilerplate
- 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 mistakes
const res = await shopifyFetch<ShopifyProductOperation>({
  query: getProductQuery, // Could be wrong string
  variables: {
    handle, // Could misspell parameter name
    first: 10, // Might not be valid for this query
  },
});

const product = res.body.data.product; // Deep nesting
```

**After migration:**
```typescript
// Clean, validated, auto-completed
const 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 Progress Tracking

We successfully migrated several key files:

**✅ Completed Migrations**

- lib/shopify/fetchers/storefront/products.ts - 6+ functions
- lib/shopify/fetchers/storefront/cart.ts - 5 functions
- lib/shopify/fetchers/storefront/collections.ts - 4 functions

**🔄 Remaining Files (Optional)**

- lib/shopify/variantOptions.ts - 2 functions
- lib/shopify/fetchers/storefront/pages.ts - 2 functions
- lib/shopify/fetchers/storefront/menu.ts - 1 function
- lib/shopify/fetchers/storefront/shop.ts - 2 functions
- lib/shopify/fetchers/storefront/files.ts - 1 function
- lib/shopify/fetchers/storefront/sitemap.ts - 2 functions

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 pattern
const 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.

Thanks, Matija

## LLM Response Snippet
```json
{
  "goal": "Complete guide to migrating from manual GraphQL calls to generated SDK in Next.js. Eliminate boilerplate, get type safety, and boost DEX.",
  "responses": [
    {
      "question": "What does the article \"Stop Writing Manual GraphQL Calls: Migrate to Generated SDK in 30 Minutes\" cover?",
      "answer": "Complete guide to migrating from manual GraphQL calls to generated SDK in Next.js. Eliminate boilerplate, get type safety, and boost DEX."
    }
  ]
}
```