Use Next.js 13 With TypeScript To Create a Headless Shopify App

andrews1022

Andrew Shearer

Posted on August 29, 2023

Use Next.js 13 With TypeScript To Create a Headless Shopify App

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:

npx create-next-app@latest --ts .
Enter fullscreen mode Exit fullscreen mode

Answer the questions like this:

✔ 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
Enter fullscreen mode Exit fullscreen mode

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

{
  "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"]
}
Enter fullscreen mode Exit fullscreen mode

Then open src/app/globals.css. You might be getting an unknownAtRules warning. If you are, follow these steps here to fix the issue.

Still in globals.css, update the code with this reset from Josh Comeau:

/* 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;
}
Enter fullscreen mode Exit fullscreen mode

Update src/app/page.tsx to this:

// src/app/page.tsx

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

export default HomePage;
Enter fullscreen mode Exit fullscreen mode

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:

// 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;
Enter fullscreen mode Exit fullscreen mode

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:

git branch -M main
git remote add origin <https://github.com/YOUR_USERNAME_HERE/GIT_LINK_HERE>
git push -u origin main
Enter fullscreen mode Exit fullscreen mode

Store Setup

Go to Shopify 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, or you can download my small list of products which we’ll use for this demo.

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_productsunder 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
Enter fullscreen mode Exit fullscreen mode

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
  • 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:

{
  shop {
    name
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Run it and you should see something like this:
{
  "data": {
    "shop": {
      "name": "Next.js Headless"
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 1,
      "actualQueryCost": 1,
      "throttleStatus": {
        "maximumAvailable": 1000,
        "currentlyAvailable": 999,
        "restoreRate": 50
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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

query ProductsQuery {
  products(first: 6) {
    nodes {
      title
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

We must provide an argument to products(). Typically it is eitherfirst or last

Now we should see a result:

{
  "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
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • 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:

const getProducts = async () => {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

In the src folder, create a utils folder

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

export const gql = String.raw;
Enter fullscreen mode Exit fullscreen mode

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:

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

const getProducts = async () => {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Now let’s create the fetch call:

// 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();
};
Enter fullscreen mode Exit fullscreen mode
  • 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 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:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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:

// next.config.js

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

module.exports = nextConfig;
Enter fullscreen mode Exit fullscreen mode

After changing it, start the server back up again.

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

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

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:

// 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;
};
Enter fullscreen mode Exit fullscreen mode

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:

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

type GraphQLResponse = {
  data: {
    products: {
      nodes: ShopifyProduct[];
    };
  };
  extensions: ShopifyExtension;
};
Enter fullscreen mode Exit fullscreen mode

Looks a lot cleaner!

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

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
            }
          }
        }
      `
    })
  });

  // ...
};
Enter fullscreen mode Exit fullscreen mode

Now we can update the JSX to this:

<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>
Enter fullscreen mode Exit fullscreen mode

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

import Image from "next/image";
import Link from "next/link";
Enter fullscreen mode Exit fullscreen mode

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

// src/utils/formatPrice.ts

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

Creating Product Pages

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

<Link href={`/product/${prodId}`} className="...">
  View Product
</Link>
Enter fullscreen mode Exit fullscreen mode

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:

"gid://shopify/Product/PRODUCT_ID_HERE"
Enter fullscreen mode Exit fullscreen mode

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:

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

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

export default SingleProductPage;
Enter fullscreen mode Exit fullscreen mode

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:

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

type GraphQLResponse = {
  data: {
    product: ShopifyProduct;
  };
  extensions: ShopifyExtension;
};
Enter fullscreen mode Exit fullscreen mode

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:

// 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();
};
Enter fullscreen mode Exit fullscreen mode

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:

// 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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:

// 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;
Enter fullscreen mode Exit fullscreen mode

Here is the code for the Footer:

// 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;
Enter fullscreen mode Exit fullscreen mode

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

// 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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:

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}`
  }
})
Enter fullscreen mode Exit fullscreen mode

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

<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>
Enter fullscreen mode Exit fullscreen mode

And that’s it! You’re done!

Host on Vercel

Go to Vercel 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 here to see the code for reference if you ever get stuck. Cheers!

💖 💪 🙅 🚩
andrews1022
Andrew Shearer

Posted on August 29, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related