Skip to main content

Advanced GraphQL Codegen Setup for Shopify Admin API in Hydrogen

Learn how to set up GraphQL Codegen in Shopify Hydrogen, integrate the Admin API, and build a custom type-safe SDK that transforms your developer experience from manual GraphQL strings to fully-typed function calls.

Advanced GraphQL Codegen Setup for Shopify Admin API in Hydrogen

At FocusReactive, we live and breathe modern web development. As a tech consultancy with deep expertise in Next.js, Headless CMS, and eCommerce, we're always looking for ways to build better applications that are scalable and maintainable. For brands that want to deliver high-quality, content-rich shopping experiences, Shopify's headless suite, Hydrogen, gives you a solid foundation. But to really make the most of it, you need a development workflow that's efficient and minimizes errors.

Shopify is a huge platform with dozens of APIs covering everything from payments and shipping to analytics and marketing. While you can explore the full range at https://shopify.dev/docs/api, there are three APIs that play a key role in any Hydrogen application. The good news is that two of them come pre-configured in every Hydrogen project, but you'll need to set up the third one "Admin API" yourself:

  • Storefront API: The public-facing API for fetching product data, managing carts, and handling customer checkout
  • Customer Account API: A specialized API for managing all aspects of a logged-in user's account, from order history to address books
  • Admin API: The heavy-duty API for managing the backend of your store. It handles everything from creating products and collections to managing orders

Working with these APIs in a modern TypeScript environment presents a challenge: how do we keep our application's code in sync with the API schemas? The answer is GraphQL Codegen.

So, what is it? GraphQL Codegen is a tool that introspects a GraphQL API's schema and your app's queries to automatically generate TypeScript types. This means no more guesswork, no more any types, and no more runtime errors caused by a misspelled field. It brings full type safety and IDE autocompletion to your data-fetching layer, making development faster and more reliable.

This article will walk you through a practical setup, starting with the default codegen configuration in a new Hydrogen project. From there, we'll show you how to integrate the Shopify Admin API. Finally, we'll guide you through building a fully-typed, custom SDK for the Admin API that'll improve your developer experience and make your application more reliable. Let's get started.

Hydrogen's Default Codegen Setup

Let's start by scaffolding a new Hydrogen project and understanding what it gives us out of the box.

First, create a new project using the official CLI. We'll skip the --quickstart flag to make sure we get a full TypeScript environment, which we need for codegen to work properly.

npm create @shopify/hydrogen@latest

Once the installation is complete, open the project in your code editor and find the .graphqlrc.ts file in the root directory. This is the central configuration file for GraphQL Codegen.

// .graphqlrc.ts

import type {IGraphQLConfig} from 'graphql-config';
import {getSchema} from '@shopify/hydrogen-codegen';

export default {
projects: {
default: {
schema: getSchema('storefront'),
documents: [
'./*.{ts,tsx,js,jsx}',
'./app/**/*.{ts,tsx,js,jsx}',
'!./app/graphql/**/*.{ts,tsx,js,jsx}',
],
},
customer: {
schema: getSchema('customer-account'),
documents: ['./app/graphql/customer-account/*.{ts,tsx,js,jsx}'],
},
},
} as IGraphQLConfig;

As you can see, Hydrogen has already set up two separate "projects" for us:

  1. default: This project connects to the public Storefront API. It's configured to scan almost all files in your project for GraphQL queries, except for those specifically designated for other APIs
  2. customer: This project targets the Customer Account API and only looks for queries inside the app/graphql/customer-account/ directory. This separation is important because each API has a different schema, and codegen needs to validate queries against the correct one

Default Queries in Action

Let's see how this works in practice.

Storefront API Example

In app/routes/($locale)._index.tsx, you'll find a query to fetch recommended products. The GraphQL query is defined directly within the file:

// app/routes/($locale)._index.tsx

const RECOMMENDED_PRODUCTS_QUERY = `#graphql
fragment RecommendedProduct on Product {
id
title
handle
priceRange {
minVariantPrice {
amount
currencyCode
}
}
featuredImage {
id
url
altText
width
height
}
}
query RecommendedProducts ($country: CountryCode, $language: LanguageCode)
@inContext(country: $country, language: $language) {
products(first: 4, sortKey: UPDATED_AT, reverse: true) {
nodes {
...RecommendedProduct
}
}
}
` as const;

This query is then executed in the loader function using the context.storefront client, which is made available to all loaders and actions.

// app/routes/($locale)._index.tsx

export async function loader({context}: LoaderFunctionArgs) {
// ...
const {storefront} = context;
const {products} = await storefront.query(RECOMMENDED_PRODUCTS_QUERY);
// ...
return json({products});
}

Customer Account API Example

For a customer-specific query, look inside app/graphql/customer-account/CustomerDetailsQuery.ts. The query to fetch a customer's profile is defined here:

// app/graphql/customer-account/CustomerDetailsQuery.ts

export const CUSTOMER_DETAILS_QUERY = `#graphql
query CustomerDetails {
customer {
...Customer
}
}
// ... fragment definition
` as const;

This query is then imported and used in the account page loader at app/routes/($locale).account.tsx, this time using the context.customerAccount client:

// app/routes/($locale).account.tsx

import {CUSTOMER_DETAILS_QUERY} from '~/graphql/customer-account/CustomerDetailsQuery';

export async function loader({context}: LoaderFunctionArgs) {
const {data, errors} = await context.customerAccount.query(
CUSTOMER_DETAILS_QUERY,
);

if (errors?.length || !data?.customer) {
throw new Error('Customer not found');
}

return json({customer: data.customer});
}

When you run the codegen command, it scans these files, finds the queries, validates them against the appropriate schemas, and generates TypeScript definition files.

npm run codegen

This command populates two key files: storefrontapi.generated.d.ts and customer-accountapi.generated.d.ts. These files contain all the necessary types for your query results, giving you autocompletion and type safety in your components.

Troubleshooting

If you're just starting, you might run into a couple of common issues:

  1. Failed to evaluate config error: If you see this error from your IDE's GraphQL plugin or when running codegen, it usually means ts-node is missing as a dev dependency. Fix it with:
    npm i -D ts-node
  2. Type Errors in Customer Account Queries: Sometimes, the order of projects in .graphqlrc.ts can cause issues where the wrong schema is used for validation. If you see errors related to customer queries, try reordering the projects so that the more specific customer project comes before the catch-all default project.

Now that we have a handle on the basics, let's extend our application's capabilities by integrating the Shopify Admin API.

Integrating the Shopify Admin API

While the Storefront and Customer Account APIs cover the customer-facing experience, many headless applications require administrative capabilities. Whether it's for custom internal dashboards, programmatic product management, or, in our case, creating discounts on the fly, the Admin API is the tool for the job.

Let's level up our codegen setup to support it.

Install the Shopify API Codegen Preset

Shopify provides an official preset that makes configuring its APIs much easier. Let's add it to our project.

npm i -D @shopify/api-codegen-preset

Update the Codegen Configuration

Next, we'll modify our .graphqlrc.ts file. We'll add a new admin project using the shopifyApiProject helper we just installed. This is also a good time to rename the default project to storefront for better clarity and consistency.

Your updated .graphqlrc.ts should look like this:

// .graphqlrc.ts

import type {IGraphQLConfig} from 'graphql-config';
import {getSchema} from '@shopify/hydrogen-codegen';
import {ApiType, shopifyApiProject} from '@shopify/api-codegen-preset';

export default {
projects: {
customer: {
schema: getSchema('customer-account'),
documents: ['./app/graphql/customer-account/*.{ts,tsx,js,jsx}'],
},

// add the new Admin API project
admin: shopifyApiProject({
apiType: ApiType.Admin,
apiVersion: '2025-04',
outputDir: './types',
documents: ['app/graphql/admin/**/*.{js,ts,jsx,tsx}'],
}),

// rename 'default' to 'storefront'
storefront: {
schema: getSchema('storefront'),
documents: [
'./*.{ts,tsx,js,jsx}',
'./app/**/*.{ts,tsx,js,jsx}',
'!./app/graphql/**/*.{ts,tsx,js,jsx}',
],
},
},
} as IGraphQLConfig;

Configure the Admin API Client

To communicate with the Admin API, we need a private access token.

  • Get Your Access Token: In your Shopify Partner Dashboard, navigate to Settings → Apps and sales channels → Develop apps → Create an app. Give your app a name (e.g., "Hydrogen Admin API"). In the app's Configuration tab, you must select the API scopes your app needs. For our example of creating a discount, you'll need to grant it write_discounts access. Once configured, navigate to the API credentials tab to find and reveal your Admin API access token.

  • Manage Your Environment Variables: This token is a secret and should never be hardcoded. First, let's make TypeScript aware of our new variable by adding it to env.d.ts:

    // env.d.ts

    interface Env extends HydrogenEnv {
    // declare additional Env parameter use in the fetch handler and loader context here
    ADMIN_API_ACCESS_TOKEN: string;
    }

    Next, add the token to your local .env file (which is git-ignored). When you deploy, you'll need to add this variable to your hosting provider's environment settings (e.g., in Shopify Oxygen or Vercel).

  • Install and Set Up the Client: Now, let's install the official Admin API client package.

    npm i @shopify/admin-api-client

    Finally, we'll instantiate this client in app/lib/context.ts and add it to our loader context, making it available throughout the app.

    // app/lib/context.ts

    import {createAdminApiClient} from '@shopify/admin-api-client';
    // ...

    export function createContext(
    request: Request,
    responseHeaders: Headers,
    env: Env,
    ) {
    // ...

    const admin = createAdminApiClient({
    storeDomain: env.PUBLIC_STORE_DOMAIN,
    apiVersion: '2025-04',
    accessToken: env.ADMIN_API_ACCESS_TOKEN,
    });

    return {
    // ...
    admin,
    };
    }

Create and Execute an Admin API Mutation

Now that we have everything set up, we can create and use our first Admin API mutation.

First, create a new directory and file: app/graphql/admin/CreateDiscountCodeMutation.ts. Paste the following GraphQL mutation into it:

// app/graphql/admin/CreateDiscountCodeMutation.ts

export const CREATE_DISCOUNT_CODE_MUTATION = `#graphql
mutation createDiscountCode($basicCodeDiscount: DiscountCodeBasicInput!) {
discountCodeBasicCreate(basicCodeDiscount: $basicCodeDiscount) {
codeDiscountNode {
id
}
userErrors {
field
message
}
}
}
`;

Now, run the codegen to generate the types for our new mutation:

npm run codegen

This will create a new types directory containing our Admin API schema (admin-2025-04.schema.json) and the corresponding TypeScript definitions (admin.types.d.ts, admin.generated.d.ts).

Finally, you can call this mutation from any action. Here's how you would do it, using the context.admin client we configured earlier.

import {CREATE_DISCOUNT_CODE_MUTATION} from '~/graphql/admin/CreateDiscountCodeMutation';

export async function action({context}: ActionFunctionArgs) {
const {admin} = context;

// the 'request' method is generic, but codegen ensures our
// mutation and variables are type-checked
const {data, errors} = await admin.request(CREATE_DISCOUNT_CODE_MUTATION, {
variables: {
basicCodeDiscount: {
// ...
},
},
});

// ...
}

Great! We've successfully integrated the Admin API, but we can make the developer experience even better. Let's build a fully-typed SDK that will make interacting with our API feel like calling native TypeScript functions.

Advanced Integration: Building a Type-Safe Admin SDK

We've already achieved type safety for our Admin API calls, which is a significant win. But the current developer experience involves manually importing GraphQL mutation strings and passing them to a generic request method.

Before (what we have now):

context.admin.request(CREATE_DISCOUNT_CODE_MUTATION, { variables: { ... } })

After (what we're building):

context.admin.createDiscountCode({ basicCodeDiscount: { ... } })

This shift is more than just a cosmetic change. It provides a better developer experience with compile-time validation of variables and fully typed, autocompletable results, all without the need to import GraphQL documents into our route files.

Take Full Control of the Codegen Configuration

To build our SDK, we need to move beyond the shopifyApiProject helper and define our codegen pipeline explicitly. This gives us the granular control necessary to add the SDK generation plugin. We'll update our .graphqlrc.ts config to detail every step of the generation process for the admin project:

// .graphqlrc.ts

import type {IGraphQLConfig} from 'graphql-config';
import {getSchema, pluckConfig, preset} from '@shopify/hydrogen-codegen';
import {
ApiType,
preset as adminPreset,
pluckConfig as adminPluckConfig,
} from '@shopify/api-codegen-preset';

const SHOPIFY_API_VERSION = '2025-04';
const outputDir = './app/generated';

export default {
projects: {
customer: {
schema: getSchema('customer-account'),
documents: ['./app/graphql/customer-account/*.{ts,tsx,js,jsx}'],
extensions: {
codegen: {
pluckConfig,
generates: {
[`${outputDir}/customeraccountapi.generated.d.ts`]: {
preset,
},
},
},
},
},

admin: {
schema: `https://shopify.dev/admin-graphql-direct-proxy/${SHOPIFY_API_VERSION}`,
documents: ['./app/graphql/admin/*.{js,ts,jsx,tsx}'],
extensions: {
codegen: {
pluckConfig: adminPluckConfig,
generates: {
[`${outputDir}/admin.schema.json`]: {
plugins: ['introspection'],
config: {minify: true},
},

[`${outputDir}/admin.types.ts`]: {
plugins: ['typescript'],
},

[`${outputDir}/admin.generated.d.ts`]: {
preset: adminPreset,
presetConfig: {
apiType: ApiType.Admin,
apiVersion: SHOPIFY_API_VERSION,
},
},
},
},
},
},

storefront: {
schema: getSchema('storefront'),
documents: [
'./*.{ts,tsx,js,jsx}',
'./app/**/*.{ts,tsx,js,jsx}',
`!${outputDir}/**`,
'!./app/graphql/**',
],
extensions: {
codegen: {
pluckConfig,
generates: {
[`${outputDir}/storefrontapi.generated.d.ts`]: {
preset,
},
},
},
},
},
},
} as IGraphQLConfig;

Configure SDK Generation

First, let's install the GraphQL Codegen plugins that will do the heavy lifting:

npm i -D @graphql-codegen/import-types-preset @graphql-codegen/typescript-generic-sdk

Now, update your .graphqlrc.ts with the final configuration. Note how the admin project is expanded with a detailed extensions.codegen.generates block. We also move all generated files into a dedicated app/generated directory for better organization.

// .graphqlrc.ts

import type {IGraphQLConfig} from 'graphql-config';
import {getSchema, pluckConfig, preset} from '@shopify/hydrogen-codegen';
import {
ApiType,
preset as adminPreset,
pluckConfig as adminPluckConfig,
} from '@shopify/api-codegen-preset';

const SHOPIFY_API_VERSION = '2025-04';
const outputDir = './app/generated';

export default {
projects: {
customer: {
schema: getSchema('customer-account'),
documents: ['./app/graphql/customer-account/*.{ts,tsx,js,jsx}'],
extensions: {
codegen: {
pluckConfig,
generates: {
[`${outputDir}/customer-account.generated.d.ts`]: {
preset,
},
},
},
},
},

admin: {
schema: `https://shopify.dev/admin-graphql-direct-proxy/${SHOPIFY_API_VERSION}`,
documents: ['./app/graphql/admin/*.{js,ts,jsx,tsx}'],
extensions: {
codegen: {
pluckConfig: adminPluckConfig,
generates: {
[`${outputDir}/admin.schema.json`]: {
plugins: ['introspection'],
config: {minify: true},
},

[`${outputDir}/admin.types.ts`]: {plugins: ['typescript']},

[`${outputDir}/admin.generated.d.ts`]: {
preset: adminPreset,
presetConfig: {
apiType: ApiType.Admin,
apiVersion: SHOPIFY_API_VERSION,
},
},

[`${outputDir}/admin.sdk.ts`]: {
preset: 'import-types',
presetConfig: {
typesPath: './admin.generated',
importTypesNamespace: 'Operations',
},
plugins: ['typescript-generic-sdk'],
config: {importOperationTypesFrom: 'Operations'},
},
},
},
},
},

storefront: {
schema: getSchema('storefront'),
documents: [
'./*.{ts,tsx,js,jsx}',
'./app/**/*.{ts,tsx,js,jsx}',
`!${outputDir}/**`,
'!./app/graphql/**',
],
extensions: {
codegen: {
pluckConfig,
generates: {
[`${outputDir}/storefront.generated.d.ts`]: {
preset,
},
},
},
},
},
},
} as IGraphQLConfig;

After updating the config, run npm run codegen. You'll see a new app/generated directory containing all our types and, most importantly, our new admin.sdk.ts file.

Implement the SDK Requester Bridge

The generated admin.sdk.ts file contains the SDK's structure, but it doesn't know how to actually make an API request. We need to connect it to the @shopify/admin-api-client. We'll create a bridge - "requester" function that does exactly that.

Create a new file at app/lib/admin-api-sdk.ts:

// app/lib/admin-api-sdk.ts

import {
createAdminApiClient,
type AdminApiClient,
} from '@shopify/admin-api-client';
import type {DocumentNode} from 'graphql';
import {getSdk, type Requester} from '~/generated/admin.sdk';

type AdminApiSdkConfig = {
accessToken: string;
storeDomain: string;
apiVersion: string;
};

/**
* this function acts as a bridge between the generic SDK and the specific
* Shopify Admin API client. It adapts the client's `request` method to
* the shape that the SDK's `getSdk` function expects.
*/
const createRequester = (client: AdminApiClient): Requester => {
return async <R, V>(doc: DocumentNode, variables?: V): Promise<R> => {
const query = doc.loc?.source.body;
if (!query) {
throw new Error('DocumentNode must contain a query string.');
}

const response = await client.request<R>(query, {
variables: variables || {},
});

if (response.errors) {
// Customize error handling as needed
throw new Error(JSON.stringify(response.errors));
}

if (response.data === undefined) {
throw new Error('No data received from API');
}

return response.data;
};
};

export const createAdminApiSdk = (config: AdminApiSdkConfig) => {
const adminApiClient = createAdminApiClient({
storeDomain: config.storeDomain,
apiVersion: config.apiVersion,
accessToken: config.accessToken,
});

const requester = createRequester(adminApiClient);

return getSdk(requester);
};

Integrate the SDK into the Application Context

Now, we just need to use our new SDK factory. Revisit app/lib/context.ts and swap createAdminApiClient with our new createAdminApiSdk.

// app/lib/context.ts

// import { createAdminApiClient } from '@shopify/admin-api-client'; // remove this
import {createAdminApiSdk} from '~/lib/admin-api-sdk'; // add this

export function createContext(
request: Request,
responseHeaders: Headers,
env: Env,
) {
// ...

// replace the old client with our new SDK
const admin = createAdminApiSdk({
storeDomain: env.PUBLIC_STORE_DOMAIN,
apiVersion: '2025-04',
accessToken: env.ADMIN_API_ACCESS_TOKEN,
});

return {
// ...
admin,
};
}

Using the New SDK

With the SDK integrated, our action becomes much cleaner and more intuitive.

Before:

import {CREATE_DISCOUNT_CODE_MUTATION} from '~/graphql/admin/CreateDiscountCodeMutation';

export async function action({context}: ActionFunctionArgs) {
const {data} = await context.admin.request(CREATE_DISCOUNT_CODE_MUTATION, {
variables: {
basicCodeDiscount: {
// ...
},
},
});
// ...
}

After:

// No need to import the GraphQL mutation string!

export async function action({context}: ActionFunctionArgs) {
const {discountCodeBasicCreate} = await context.admin.createDiscountCode({
basicCodeDiscount: {
// ...
},
});
// ...
}

The difference is clear. Our code is more readable, less verbose, and fully type-safe from the function call to the destructured result. We've successfully built a modern and maintainable way to interact with the Shopify Admin API.

Conclusion

We've come a long way from the foundational codegen setup that comes with a fresh Shopify Hydrogen installation to a truly advanced developer workflow. By progressing from the default configuration to integrating the Admin API, and finally to building a custom, type-safe SDK, you've created a new level of efficiency for your headless commerce projects.

While Hydrogen's defaults provide an excellent starting point, the real magic happens when you tailor the tools to fit a professional development pipeline. It eliminates a whole class of potential bugs, speeds up development with autocompletion, and makes your codebase cleaner, more readable, and far easier to maintain in the long run.

This kind of setup is what we build at FocusReactive. Creating content-rich eCommerce websites requires not only a deep understanding of platforms like Shopify but also a commitment to development practices that ensure scalability and reliability.

As a Headless Commerce agency, we deliver custom, high-performance solutions designed to meet the unique requirements of our clients. If you're looking to build a modern online storefront or need a free consultation on your headless strategy - contact us.

CONTACT US TODAY

Don't want to fill out the form? Then contact us by email [email protected]