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.

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:
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 APIscustomer
: This project targets the Customer Account API and only looks for queries inside theapp/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:
- Failed to evaluate config error: If you see this error from your IDE's GraphQL plugin or when running
codegen
, it usually meansts-node
is missing as a dev dependency. Fix it with:npm i -D ts-node
- 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 specificcustomer
project comes before the catch-alldefault
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.