Code

Shopify Admin GraphQL API + gql.tada = ❤️

Kyle Kashuba Published: June 19, 2024 Updated: February 14, 2026 5 min read
Shopify Admin GraphQL API + gql.tada = ❤️

Codegen can be annoying! You have to manage a separate process in order to leverage type safety.
In this guide we’ll walk through how to use gql.tada for making
type-safe calls to the Shopify Admin API in your Remix app.

Installation

Follow the installation instructions to
get gql.tada set up properly in your project.

For the schema in your tsconfig plugin configuration, you can specify a direct proxy URL from Shopify that allows for introspection:

{
  "compilerOptions": {
    "plugins": [
      {
        "name": "@0no-co/graphqlsp",
        "schema": "https://shopify.dev/admin-graphql-direct-proxy/2024-07",
        "tadaOutputLocation": "./app/graphql-env.d.ts"
      }
    ]
  }
}

Make sure to replace 2024-07 with your API version if applicable.

GraphQL Client Setup

Install `urql`, a GraphQL client which will
facilitate our queries and mutations to the Shopify Admin API.

Create a function that will create a client for a given shop:

// ~/app/graphql.server.ts
import { cacheExchange, Client, fetchExchange } from '@urql/core';
import { prisma } from '~/db.server';

const API_VERSION = '2024-07';

export const createShopifyClient = async (shop: string) => {
  const session = await prisma.session.findFirst({
    where: { shop, isOnline: false }
  });

  if (!session) throw new Error(Session not found for shop: ${shop});

  return new Client({
    url: `https://${shop}/admin/api/${API_VERSION}/graphql.json`,
    exchanges: [cacheExchange, fetchExchange],
    fetchOptions: {
      headers: {
        'X-Shopify-Access-Token': session.accessToken
      }
    }
  });
};

Integrate with authenticate.admin

Shopify’s Remix app template includes a shopify.server.ts file which provides an export
called authenticate. This is what facilitates authentication with Shopify and gives you
a way to make Admin API queries. Augment it to include the urql client:

// ... shopifyApp setup ...

export const authenticate = {
  /**
   * Authenticates with Shopify, allowing access to Shopify's APIs when accessing
   * the app admin.
   *
   * Note: This injects a `urql` client, which can consume types from `gql.tada`. You should use this over
   * `admin.graphql` whenever possible for type safe variables and response data. Example:
   *
   * ```ts
   * const { admin, session } = await authenticate.admin(request);
   *
   * const response = await admin.urql.mutation(yourGraphQLQuery, {
   *   someVariable: 'hello world!'
   * });
   * 
   */
  admin: async (request: Request) => {
    const { session, admin, ...rest } = await shopify.authenticate.admin(request);
    const client = await createShopifyClient(session.shop);
    
    return { session, admin: { urql: client, ...admin }, ...rest };
  }
}; 

// ...

Example usage

Here’s an example route that queries a list of products from the Shopify API,
using a query definition created with gql.tada:

import { json, LoaderFunctionArgs } from '@remix-run/node';
import { BlockStack, List, TextField } from '@shopify/polaris';
import { Form, useLoaderData } from '@remix-run/react';
import { useState } from 'react';
import { authenticate } from '~/shopify.server';

// Make sure the import path here is correct, aligned with how
// you set up gql.tada!
import { graphql } from '~/shared/graphql';

const productsQuery = graphql(
  query Products($search: String!) {
    products(query: $search, first: 50) {
      edges {
        node {
          id
          title
        }
      }
    }
  }
);

export const loader = async ({ request }: LoaderFunctionArgs) => {
  const { admin } = await authenticate.admin(request);
  const searchParams = new URL(request.url).searchParams;

const response = await admin.urql.query(productsQuery, {
    search: searchParams.get('search') || ''
  });

return json({
    products: response.data?.products.edges.map(edge => edge.node) || []
  });
};

export default function Products() {
  const { products } = useLoaderData<typeof loader>();
  const [search, setSearch] = useState('');

return (
    <BlockStack>
      <Form>
        <TextField
          label="Search"
          autoComplete="off"
          name="search"
          value={search}
          onChange={setSearch}
        />
      </Form>

      <List>
        {products.map(product => (
          <List.Item key={product.id}>{product.title}</List.Item>
        ))}
      </List>
    </BlockStack>
  );
}

Notice how the query variables and response data are all typed properly, without
the need for you to manage a separate process for generating types for your query documents.


Find this helpful? You might also like our apps:

  • Customer Fields allows you to build custom registration forms and retrieve valuable data from customers at just the right time.
  • Meteor Mega Menu offers a variety of beautiful dropdown menu templates which you can attach to your existing Shopify navigation, meaning your store could have a menu makeover in minutes.

Ready to Grow Your
Online Business?

Connect with our team to discuss your project and see how we can help.