Use Next.js 13 With TypeScript To Create a Headless Shopify App
Tags: nextjs, typescript, shopify, webdev
Published: 2023-08-29 18:32:06

This doc will show you how to set up a simple ecommerce app using Next.js 13 and Shopify as a headless ecommerce platform. We will use the following:

- Next.js w/ TypeScript
- GraphQL
- Shopify

Our goal for this simple demo will be to fetch products from Shopify and display them on the homepage. Then we will add dynamic routing to go to an individual product page, and view the details of that product there.

## Initial Setup

Quickly download the latest Next.js TypeScript starter:

```bash
npx create-next-app@latest --ts .
```

Answer the questions like this:

```bash
✔ Would you like to use ESLint? … **No** / Yes
✔ Would you like to use Tailwind CSS? … No / **Yes**
✔ Would you like to use `src/` directory? … No / **Yes**
✔ Would you like to use App Router? (recommended) … No / **Yes**
✔ Would you like to customize the default import alias? … **No** / Yes
```

We need to correct some things before getting started. First, open `tsconfig.json` and update it to this:

```json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "Node",
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
```

Then open `src/app/globals.css`. You might be getting an `unknownAtRules` warning. If you are, follow **[these steps here](https://github.com/tailwindlabs/tailwindcss/discussions/5258#discussioncomment-1979394)** to fix the issue.

Still in `globals.css`, update the code with this reset from **[Josh Comeau](https://www.joshwcomeau.com/css/custom-css-reset/)**:

```css
/* src/app/globals.css */

@tailwind base;
@tailwind components;
@tailwind utilities;

*, *::before, *::after {
  box-sizing: border-box;
}

* {
  margin: 0;
	padding: 0;
}

body {
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img, picture, video, canvas, svg {
  display: block;
  max-width: 100%;
}

input, button, textarea, select {
  font: inherit;
}

p, h1, h2, h3, h4, h5, h6 {
  overflow-wrap: break-word;
}

#root, #__next {
  isolation: isolate;
}
```

Update `src/app/page.tsx` to this:

```tsx
// src/app/page.tsx

const HomePage = () => {
  return (
    <div>
      <h1>Shopify + Next.js 13!</h1>
    </div>
  );
};

export default HomePage;
```

I like to use function expressions, so I will be using those throughout. If you prefer the function keyword, absolutely feel free to do so. All that matters is you pick one and stick with it.

Next, let’s update the root `layout.tsx` to this:

```tsx
// src/app/layout.tsx

import { Inter } from "next/font/google";

import type { Metadata } from "next";
import type { ReactNode } from "react";

import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  description: "Generated by create next app",
  title: "Shopify + Next.js 13!"
};

type RootLayoutProps = {
  children: ReactNode;
};

const RootLayout = ({ children }: RootLayoutProps) => {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  );
};

export default RootLayout;
```

Next, go to GitHub and create a repo (if you want). If you do, you only need to run the following 3 commands to push it up to the remote repo:

```bash
git branch -M main
git remote add origin <https://github.com/YOUR_USERNAME_HERE/GIT_LINK_HERE>
git push -u origin main
```

## Store Setup

Go to **[Shopify Partners](https://www.shopify.com/partners)** and create an account if you do not have one already. Once in your dashboard, click **Stores.** If you do not have any Stores, click **Add store**, and then select **Create development store**, and then go through the process of creating a dev store.

Just make sure to select **Create a store to test and build**

## Shopify Products Setup

If you’re in need of some products, you can either get some dummy products from **[this repo](https://github.com/shopifypartners/product-csvs)**, or you can download my small list of products which we’ll use for this demo.

[products_export_1.csv](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e0934eb7-1782-48b7-9ce4-86fbfe280e21/products_export_1.csv)

## Shopify Storefront API Setup

### **Enable custom app development**

- From your Shopify admin, click **Settings** > **Apps and sales channels**.
- Click **Develop apps**.
- Click **Allow custom app development**.
- Read the warning and information provided, and then click **Allow custom app development**.

### **Create and install a custom app**

- From your Shopify admin, click **Settings** > **Apps and sales channels**.
- Click **Develop apps**
- Click **Create an app**
- In the modal window, enter the **App name** and select an **App developer**
- Click **Create app**.

### Select API scopes

- Click **Configure Admin API scopes**.
- In the **Admin API access scopes** section, select the API scopes that you want to assign to the app.
    - For this demo, select at least `write_products` & `read_products`under **Products**.
- Click **Save**
    - Make sure to copy the **Admin API access token** as you can only see it once!
- Click **Install App**.

## Configure Environment Variables

Back in the code, create a `.env.local` file at the root level and add the following:

```
ADMIN_API_ACCESS_TOKEN=YOUR_TOKEN_HERE
```

Replace `YOUR_TOKEN_HERE` with your **Admin API access token**

You do not need the API key or the API secret key

## Shopify GraphiQL App Setup

- Go here to install the **[GraphQL API app](https://shopify-graphiql-app.shopifycloud.com/)**
- On this page:
    - Enter your Shop URL
        - To get your shop url, when in the admin, click on home
        - The URL should be something like:
        
        `https://admin.shopify.com/store/STORE_NAME_HERE`
        
        - Copy and paste whatever is your STORE_NAME_HERE
    - You can leave the Admin API and Storefront API checkboxes alone, or check more if you want
    - Click **Install**
    - Wait for the redirect to finish, and click **Install app**
- Once finished, you should be in the GraphiQL playground with this query to start with:

```graphql
{
  shop {
    name
  }
}
```

- Run it and you should see something like this:

```json
{
  "data": {
    "shop": {
      "name": "Next.js Headless"
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 1,
      "actualQueryCost": 1,
      "throttleStatus": {
        "maximumAvailable": 1000,
        "currentlyAvailable": 999,
        "restoreRate": 50
      }
    }
  }
}
```

We want to query for products, so let's create a ProductsQuery.

Let's start off simple with getting just the product titles:

```graphql
query ProductsQuery {
  products(first: 6) {
    nodes {
      title
    }
  }
}
```

We must provide an argument to `products()`. Typically it is either`first` or `last`

Now we should see a result:

```json
{
  "data": {
    "products": {
      "nodes": [
        {
          "title": "Head Bust Digital Print"
        },
        {
          "title": "Dripping Hand Digital Print"
        },
        {
          "title": "Woman's Face Digital Print"
        },
        {
          "title": "Purpe & Red Waves Digital Print"
        },
        {
          "title": "Purple Rock Sculpture Digital Print"
        },
        {
          "title": "Tree on a Hill Digital Print"
        }
      ]
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 8,
      "actualQueryCost": 3,
      "throttleStatus": {
        "maximumAvailable": 1000,
        "currentlyAvailable": 843,
        "restoreRate": 50
      }
    }
  }
}
```

- Let's just display this data on the homepage for now, and we'll come back to it and add more

## Displaying Simple Shopify Data

Let’s start off simple by fetching just the product titles for now to ensure everything is working correctly

Inside `.env.local` file and add the following and update the placeholders accordingly:

```
GRAPHQL_API_URL=https://STORE_NAME.myshopify.com/admin/api/API_VERSION/graphql.json
```

- You get the `API_VERSION` while in the GraphiQL playground. There is a drop down called `API version`. You just need the `year-month`. Currently at the time of writing, the current version is `2023-07`
- And your `STORE_NAME` is this part of your store url:
*https://**my-store-name**.myshopify.com*

To have Next.js load these variables, simply restart the dev server. You should see `info - Loaded env from ...` in the terminal.

Open `src/app/page.tsx` and add this above the component:

```tsx
const getProducts = async () => {
	// ...
}
```

In the `src` folder, create a `utils` folder

Inside the `utils` folder, create a file called `gql.ts` and add this to it:

```tsx
export const gql = String.raw;
```

This gives us a nice wrapper around multi line template strings, making it much easier to construct / format our GraphQL queries.

Now let's fetch some data! First, let’s add this type above the function:

```tsx
type GraphQLResponse = {
  data: {
    products: {
      nodes: {
        title: string;
      }[];
    };
  };
  extensions: {
    cost: {
      requestedQueryCost: number;
      actualQueryCost: number;
      throttleStatus: {
        maximumAvailable: number;
        currentlyAvailable: number;
        restoreRate: number;
      };
    };
  };
};

const getProducts = async () => {
	// ...
}
```

Now let’s create the fetch call:

```tsx
// src/app/page.tsx

type GraphQLResponse = {
  // ...
}

const getProducts = async (): Promise<GraphQLResponse> => {
  const res = await fetch(process.env.GRAPHQL_API_URL!, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Shopify-Access-Token": process.env.ADMIN_API_ACCESS_TOKEN!
    },
    body: JSON.stringify({
      query: gql`
        query ProductsQuery {
          products(first: 6) {
            nodes {
              title
            }
          }
        }
      `
    })
  });

  if (!res.ok) {
    const text = await res.text(); // get the response body for more information

    throw new Error(`
      Failed to fetch data
      Status: ${res.status}
      Response: ${text}
    `);
  }

  return res.json();
};
```

- ***NOTE 1***: For the `env` variables to work, you ***MUST*** put them inside template strings, or use the `!` operator to note they are not null. Otherwise, you get a type error that `string | undefined` does not work for type `string`. The “proper” approach would be to actually handle the undefined case, but this is just a quick demo, so I think this is ok for now.
- ***NOTE 2***: For some reason, Shopify does not like when you have the `API_VERSION` as it's own env variable. Perhaps due to being a number, or enum? Just put the whole URL as a variable and it will be ok

Here is a link to the Shopify GraphQL **[docs](https://shopify.dev/docs/api/admin-graphql)** if you want to check it out. Previously, the access token header was called `X-Shopify-Storefront-Access-Token`. Do not use this! Ensure you are using the updated name as seen in the code above. I may or may not have gotten stuck on this for an unnecessary amount of time >_>

Our query is pretty straightforward. We use the `GRAPHQL_API_URL` environment variable as the endpoint, make sure to use `POST` for the `method`, set the `headers` as seen above, and then wrap the query in a `JSON.stringify`. You could certainly abstract this out into its own function later on, if you prefer. Again, since this is a simple demo, it’s fine here for now.

Now let's update the page component to show the product titles in a list:

```tsx
const HomePage = async () => {
  const json = await getProducts();

  return (
    <main>
      <h1>Shopify + Next.js 13!</h1>

      <ul>
        {json.data.products.nodes.map((product) => (
          <li key={product.title}>{product.title}</li>
        ))}
      </ul>
    </main>
  );
};
```

Here, we are leveraging the power of React Server Components, which is the default in Next.js 13. We don’t have to use `getStaticProps` like we used to, or use the `useEffect` hook. We can use plain old async/await with the standard fetch api. Which, to me at least, feels a lot better is a much simpler approach than in the past.

And if you go to the browser, you should see the list of product names on the page.

## Getting and Display More Data

Now let's update our product query on the home page to show the `description`, `featuredImage`, `handle`, `price`, `tags`, and `title`

But before we do, we need to add Shopify’s CDN to the list of domains in `next.config.js`

Otherwise, we’ll get an error and won’t be able to display the images. Shut down the local dev server, and update the code to this:

```tsx
// next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    domains: ["cdn.shopify.com"]
  },
  reactStrictMode: true
};

module.exports = nextConfig;
```

After changing it, start the server back up again.

Next, let's update our GraphQL query to this:

```graphql
query ProductsQuery {
  products(first: 6) {
    nodes {
      description
      featuredImage {
        altText
        height
        id
        url
        width
      }
      handle
			id
      priceRangeV2 {
        minVariantPrice {
          amount
          currencyCode
        }
      }
      tags
      title
    }
  }
}
```

The `altText` for the images will be `null` at first if you run this in the GraphiQL playground . To fix this, so go to

- Products, then select a product
- Click on the image
- Click **add alt text**
- Add something simple like `Preview for PRODUCT_NAME`
- Click **Save alt text**
    - Repeat this for all product images

Go back to Shopify GraphiQL App, and rerun the query. `altText` should have a value now

Now is also a good time to setup some types for Shopify

In the `src` directory, create another directory called `types`, and inside there, create a file called `index.ts`

Add these types:

```tsx
// src/types/index.ts

export type ShopifyExtension = {
  cost: {
    actualQueryCost: number;
    requestedQueryCost: number;
    throttleStatus: {
      currentlyAvailable: number;
      maximumAvailable: number;
      restoreRate: number;
    };
  };
};

export type ShopifyProduct = {
  description: string;
  featuredImage: {
    altText: string;
    height: number;
    id: string;
    url: string;
    width: number;
  };
  handle: string;
  id: string;
  priceRangeV2: {
    minVariantPrice: {
      amount: string;
      currencyCode: string;
    };
  };
  tags: string[];
  title: string;
};
```

There are certainly *way* more fields than these in the Schema, but this is a good starting point based on our query / small demo.

Back in `src/app/page.tsx`, we can update our `GraphQLResponse` type to this:

```tsx
import type { ShopifyExtension, ShopifyProduct } from "@/types";

type GraphQLResponse = {
  data: {
    products: {
      nodes: ShopifyProduct[];
    };
  };
  extensions: ShopifyExtension;
};
```

Looks a lot cleaner!

Next, we need to update our query in the fetch request

```tsx
const getProducts = async (): Promise<GraphQLResponse> => {
  const res = await fetch(process.env.GRAPHQL_API_URL!, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Shopify-Access-Token": process.env.ADMIN_API_ACCESS_TOKEN!
    },
    body: JSON.stringify({
      query: gql`
        query ProductsQuery {
          products(first: 6) {
            nodes {
              description
              featuredImage {
                altText
                height
                id
                url
                width
              }
              handle
              priceRangeV2 {
                minVariantPrice {
                  amount
                  currencyCode
                }
              }
              tags
              title
            }
          }
        }
      `
    })
  });

  // ...
};
```

Now we can update the JSX to this:

```tsx
<main className="container mx-auto">
  <div className="px-5">
    <h2 className="font-bold text-2xl mb-3">Our Products:</h2>
    <ul className="grid grid-cols-12 gap-4 pb-12">
      {json.data.products.nodes.map((product) => {
        const prodId = product.id.split("/").pop();

        return (
          <li
            key={product.id}
            className="border border-slate-200 rounded-md overflow-hidden col-span-full md:col-span-6 lg:col-span-4"
          >
            <div>
              <Image
                src={product.featuredImage.url}
                alt={product.featuredImage.altText}
                width={product.featuredImage.width}
                height={product.featuredImage.height}
                className="h-96 w-full object-cover"
                placeholder="blur"
                blurDataURL={product.featuredImage.url}
              />
            </div>

            <div className="p-5">
              {product.tags.map((tag) => (
                <span
                  key={tag}
                  className="bg-yellow-400 font-bold py-1 px-3 rounded-full text-xs"
                >
                  {tag}
                </span>
              ))}

              <h3 className="font-medium mt-3 text-3xl">{product.title}</h3>

              <h4>
                {formatPrice(product.priceRangeV2.minVariantPrice.amount)}{" "}
                {product.priceRangeV2.minVariantPrice.currencyCode}
              </h4>

              <p className="mt-2 mb-4">{product.description}</p>

              <Link
                href={`/product/${prodId}`}
                className="border border-blue-600 inline-block p-2 rounded-md text-blue-600 hover:bg-blue-600 hover:text-white ease-in-out duration-150"
              >
                View Product
              </Link>
            </div>
          </li>
        );
      })}
    </ul>
  </div>
</main>
```

WIth this update, we’ll need to import a few things from `next`:

```tsx
import Image from "next/image";
import Link from "next/link";
```

The price doesn't display correctly, so we can create a `formatPrice` util function to do that for us:

```tsx
// src/utils/formatPrice.ts

export const formatPrice = (price: string) =>
  Intl.NumberFormat("en-CA", {
    style: "currency",
    currency: "CAD",
    minimumFractionDigits: 2
  }).format(parseInt(price, 10));
```

## Creating Product Pages

In our home page JSX, we have this link at the bottom:

```tsx
<Link href={`/product/${prodId}`} className="...">
  View Product
</Link>
```

Notice we are linking to a dynamic page using the product’s id. We had to format it a bit because otherwise the full id is this:

```tsx
"gid://shopify/Product/PRODUCT_ID_HERE"
```

Which is not very url friendly. Previously we could use the `handle`, but now `id` with a type of `ID` is the only accepted argument

To create this route, in the app directory we can create a directory called `product`, then another directory inside there called `[id]`. In the [id] directory, create a `page.tsx` file. For now, let’s use this dummy JSX code:

```tsx
// src/app/product/[id]/page.tsx

const SingleProductPage = () => {
  return <div>SingleProductPage</div>;
};

export default SingleProductPage;
```

Now if we go back to the browser and click on any of the “View Product” buttons, it should take us to the page for that product

Now let’s flesh out the page some more to display the info for a given product. First, let's create our type:

```tsx
// src/app/product/[id]/page.tsx

type GraphQLResponse = {
  data: {
    product: ShopifyProduct;
  };
  extensions: ShopifyExtension;
};
```

Notice the structure for `data` is slightly different now that we are querying a single product instead of a group of products

Next, our `getProduct` function:

```tsx
// src/app/product/[id]/page.tsx

const getProduct = async (id: string): Promise<GraphQLResponse> => {
  const res = await fetch(process.env.GRAPHQL_API_URL!, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Shopify-Access-Token": process.env.ADMIN_API_ACCESS_TOKEN!
    },
    body: JSON.stringify({
      query: gql`
        query SingleProductQuery($id: ID!) {
          product(id: $id) {
            id
            title

          }
        }
      `,
      variables: {
        id: `gid://shopify/Product/${id}`
      }
    })
  });

  if (!res.ok) {
    const text = await res.text(); // get the response body for more information

    throw new Error(`
      Failed to fetch data
      Status: ${res.status}
      Response: ${text}
    `);
  }

  return res.json();
};
```

In GraphQL, if we want to dynamically query something, we need to use variables like `$id` here. Previously, we could use the type `String!`, but that is no longer possible. We must now use the type `ID!`. Not a big deal, just pointing out the change. And we’ll just fetch the product title and id for now to get started.

Furthermore, along with the `query` property, we must also pass along the `variables` property. Here, we must use the full `"gid://shopify/Product/ID_HERE”` shown earlier. We cannot simply use the number portion, we must use the full string. We then interpolate the `id` parameter into this id string.

With Next, we can access the id from the URL by using the params prop in our component. And as a quick note, whatever you call the dynamic segment, in this case `id`, will be the property availabe in the `params` object.

So in the component we can do this:

```tsx
// src/app/product/[id]/page.tsx

type SingleProdutPageProps = {
  params: {
    id: string;
  };
};

const SingleProductPage = async ({ params }: SingleProdutPageProps) => {
  const json = await getProduct(params.id);
  const { product } = json.data;

  return (
    <div>
      <h1>View page for: {product.title}</h1>
    </div>
  );
};
```

And we should see the product title on the page. Now let’s update the query to get and display more data on the page!

But before we do that, the page is kind of awkward. It’s just the `h1` with no navigation. Let’s set up a simple global `Navbar` and `Footer`. We’ll also move the main `h1` from the home page into the `nav` so the simple demo feels a bit more cohesive

## Adding Global Nav & Footer

To get started, in the `src` directory, create another directory called `components`

In this `components` directory, create 2 files: `Navbar.tsx` and `Footer.tsx`

Here is the code for the `Navbar`:

```tsx
// src/components/Navbar.tsx

import Link from "next/link";

const Navbar = () => {
  return (
    <nav className="mt-10">
      <h1 className="font-bold mb-3 text-3xl text-center">Shopify + Next.js 13!</h1>

      <ul className="text-center">
        <li>
          <Link href="/" className="text-blue-600 hover:underline">
            Home
          </Link>
        </li>
      </ul>
    </nav>
  );
};

export default Navbar;
```

Here is the code for the Footer:

```tsx
// src/components/Footer.tsx

const Footer = () => {
  return (
    <footer className="bg-blue-600 mt-auto py-3 text-center text-white">
      Shopify + Next.js 13!
    </footer>
  );
};

export default Footer;
```

Next, in the root `layout.tsx`, let’s update the JSX to this:

```tsx
// src/app/layout.tsx

const RootLayout = ({ children }: RootLayoutProps) => {
  return (
    <html lang="en">
      <body className={`${inter.className} flex flex-col min-h-screen`}>
        <Navbar />
        {children}
        <Footer />
      </body>
    </html>
  );
};
```

With this in place, the footer will always be on the bottom of the page.

## Single Product Page

First, let’s update our GraphQL query with the other pieces of data we want to display:

```tsx
body: JSON.stringify({
  query: gql`
    query SingleProductQuery($id: ID!) {
      product(id: $id) {
        description
        featuredImage {
          altText
          height
          id
          url
          width
        }
        id
        priceRangeV2 {
          minVariantPrice {
            amount
            currencyCode
          }
        }
        tags
        title
      }
    }
  `,
  variables: {
    id: `gid://shopify/Product/${id}`
  }
})
```

Next let’s update the JSX for the single product page to this:

```tsx
<div className="container mx-auto md:pb-10">
  <div className="flex flex-col md:flex-row md:items-center">
    <div className="md:basis-1/2">
      <Image
        src={product.featuredImage.url}
        alt={product.featuredImage.altText}
        width={product.featuredImage.width}
        height={product.featuredImage.height}
        placeholder="blur"
        blurDataURL={product.featuredImage.url}
      />
    </div>

    <div className="p-5 md:basis-1/2">
      {product.tags.map((tag) => (
        <span key={tag} className="bg-yellow-400 font-bold py-1 px-3 rounded-full text-xs">
          {tag}
        </span>
      ))}

      <h3 className="font-medium mt-3 text-3xl">{product.title}</h3>

      <h4>
        {formatPrice(product.priceRangeV2.minVariantPrice.amount)}{" "}
        {product.priceRangeV2.minVariantPrice.currencyCode}
      </h4>

      <p className="mt-2 mb-4">{product.description}</p>

      <button
        className="border border-blue-600 inline-block p-2 rounded-md text-blue-600 hover:bg-blue-600 hover:text-white ease-in-out duration-150"
        type="button"
      >
        Add to Cart
      </button>
      <span className="block mt-1 text-xs">* Note: this won't actually do anything</span>
    </div>
  </div>
</div>
```

And that’s it! You’re done!

## Host on Vercel

Go to **[Vercel](https://vercel.com/)** and create an account if you don't already have one. Once signed in, go to your dashboard.

Click **+ New Project**, and select the project from GitHub (authorize if needed). Make sure to enter the environment variables! Click **Deploy**.

## Next Steps

This was intended to be a starting point for using Next.js 13 and Shopify as a headless backend. Some things to implement going forward would be:

- Add to cart functionality
- Display products in a cart
- Going through checkout process
- User sign in / sign out

I hope you enjoyed this article! You can view [**my repo**](https://github.com/andrews1022/demo-nextjs-13-shopify) here to see the code for reference if you ever get stuck. Cheers!