Tools & Services

Building a Headless Shopify E-commerce Site with Nuxt and The Storefront API (Part 2)

Tools & Services

Building a Headless Shopify E-commerce Site with Nuxt and The Storefront API (Part 2)

Introduction

When working with Shopify, there are two different APIs that developers will likely come across: Admin API vs Storefront API. And while most developers' first instinct would probably be to use the Admin API, it's preferable to use the Storefront API instead.

If you want to jump right into deploying the app and tinkering with the code, click on the following button to deploy your own project!

Deploy to Netlify

Why use the Storefront API instead of Admin?

The Admin API is primarily meant for admin-related actions and provides extensive access to user and store data which could become a security vulnerability if used incorrectly. On the other hand, the Storefront API is designed to be primarily read access only and can be exposed without risking sensitive data or actions.

So what can you expect in this post?

In Part 1 of this series, we introduced a brand new Shopify and Nuxt template. In this post, we'll be taking a look at how the following features were configured so you can integrate the Shopify features you need into your own site:

  • Authenticate with Shopify
  • See all available products and their respective variants
  • See a specific product's details

Prerequisites

Before we get started, there are a few things you'll need:

Once you have those things setup, you'll want to add the following environment variables to your Netlify site.

  • SHOPIFY_API_ENDPOINT - You can learn more in the Shopify Storefront API - Endpoints and Queries docs
    • Example: https://{store_name}.myshopify.com/api/2022-07/graphql.json
  • SHOPIFY_STOREFRONT_API_TOKEN - You can find this in your store's Settings > Apps and Sales Channels page. You'll need to "create an app" which will have a Storefront Access Token for you to copy:
    • Example URL for where the settings is located: https://{store_name}.myshopify.com/admin/settings/apps/development
    • Example Storefront Access Token: f72nfdb048uafj2518f7648caf9

Once you have this configured, it's time to connect Shopify with our app!

Authenticate with Shopify

At the core of every request is the need to authenticate with Shopify. Rather than implement this in every API request we have, let's start by abstracting this functionality into a reusable function.

To start, let's create a utility function /netlify/functions/utils/postToShopify that utilizes the node-fetch package to make a POST request to the Shopify API endpoint that you configured in the previous section.

const fetch = require('node-fetch')

exports.postToShopify = async () => {
  const result = await fetch(process.env.SHOPIFY_API_ENDPOINT, {
    method: 'POST'
  })
}

Now that we have this setup, it's time to configure the correct headers to our request to authenticate with Shopify. We do this by configuring the Content-Type and a custom header called X-Shopify-Storefront-Access Token.

const fetch = require('node-fetch')

exports.postToShopify = async () => {
  const result = await fetch(process.env.SHOPIFY_API_ENDPOINT, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Shopify-Storefront-Access-Token': process.env.SHOPIFY_STOREFRONT_API_TOKEN,
    },
  })
}

Now that our headers are configured, you might think that everything is ready to go, but since this is a reusable function, we need to customize what we're asking Shopify for.

Customizing the POST Request

When it comes to sending the actual request to Shopify of what we want, this is accomplished in the body of the request.

Shopify takes a specific format when it comes to a request, which is an object that contains query and variables so that it can then be passed into GraphQL.

Since this is the standard format for Shopify requests, we can add a parameter to our function that mirrors this structure to be consistent.

const fetch = require('node-fetch')

exports.postToShopify = async ({ query, variables }) => {
  const result = await fetch(process.env.SHOPIFY_API_ENDPOINT, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Shopify-Storefront-Access-Token': process.env.SHOPIFY_STOREFRONT_API_TOKEN,
    },
    body: JSON.stringify({ query, variables })
  })
}

And in case you're wondering why there's a JSON.stringify function being called, that's because when sending a request to the server, the request being sent cannot contain complex data structure likes objects. As a result, objects are typically turned into strings (and parsed later on by the receiving end).

The last thing we need to do to complete our request to Shopify is taking the JavaScript Promise and parsing the response into JSON and returning it as the output from the function.

const fetch = require('node-fetch')

exports.postToShopify = async ({ query, variables }) => {
  const result = await fetch(process.env.SHOPIFY_API_ENDPOINT, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Shopify-Storefront-Access-Token': process.env.SHOPIFY_STOREFRONT_API_TOKEN,
    },
    body: JSON.stringify({ query, variables })
  }).then((res) => res.json())

  return result.data
}

And with that, the function is complete! But since errors happen and sometimes we get empty results, we'll add some conditionals and a try / catch block to account for those scenarios.

📄 /netlify/functions/utils/postToShopify.js

const fetch = require('node-fetch')

exports.postToShopify = async ({ query, variables }) => {
  try {
    const result = await fetch(process.env.SHOPIFY_API_ENDPOINT, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Shopify-Storefront-Access-Token':
          process.env.SHOPIFY_STOREFRONT_API_TOKEN,
      },
      body: JSON.stringify({ query, variables }),
    }).then((res) => res.json())

    // Check to make sure we actually have data
    // Otherwise return the correct response so
    // it can be handled properly on the receiving end
    if (result.errors) {
      console.log({ errors: result.errors })
    } else if (!result || !result.data) {
      console.log({ result })
      return 'No results found.'
    }

    return result.data
  } catch (error) {
    console.log(error)
  }
}

You can find the file from the project here.

See All Available Products

Now that we have the ability to authenticate to Shopify, the first and foremost thing users need to be able to do is see all available products and any variants that might be relevant to them.

Let's start by creating a serverless function /netlify/functions/get-product-list.js with the basic wrapper that calls our postToShopify.js function and returning a standard response.

const { postToShopify } = require('./utils/postToShopify')

exports.handler = async () => {
  const shopifyResponse = await postToShopify()

  return {
    statusCode: 200,
    body: JSON.stringify(shopifyResponse),
  }
}

In this current state though, we have not actually passed the query that will be run. The main thing to understand about the Storefront API is that it uses GraphQL. It would be beyond the scope of this article to cover it, but if I were to summarize it in one line, GraphQL provides you a structured way of requesting data with the properties you want (rather than getting only a preset structure of data).

Here's a starter GraphQL request that we want to make:

const { postToShopify } = require('./utils/postToShopify')

exports.handler = async () => {
  const shopifyResponse = await postToShopify({
    query: `
      query getProductList {
        products(sortKey: TITLE, first: 100) {
          edges {
            node {
              id
              title
            }
          }
        }
      }
    `,
  })

  return {
    statusCode: 200,
    body: JSON.stringify(shopifyResponse),
  }
}

First, you probably noticed that we're passing the GraphQL query as a string with indentation. The reason we're doing this is for readability and not because of a programmatic syntax requirement.

Next, let's break down the query line by line at a high level:

  • query GetProductList - This tells GraphQL we are sending a query (similar to a GET request in a REST API) that can be identified as "GetProductList".
  • products(sortKey: TITLE, first: 100) - Now we tell GraphQL what we want to request from the database. In this case, we're requesting "products" and passing in parameters to sort the results by TITLE and limiting the request to only the first 100.
  • edges - These are the connections found to "products".
  • node - These are the individual items that represent each "product".
  • id and title - These are properties of the node that we are requesting. You'll see shortly that there are many more properties which we'll be requesting in order to populate our store.

If you'd like to see what other options are available for products, check out the official docs for Storefront API Products.

Here's the full query that I use for the Shopify Nuxt starter kit that's wrapped in a try / catch block to account for an error path.

📄 /netlify/functions/get-product-list.js

const { postToShopify } = require('./utils/postToShopify')

exports.handler = async () => {
  try {
    const shopifyResponse = await postToShopify({
      query: `
        query getProductList {
          products(sortKey: TITLE, first: 100) {
            edges {
              node {
                id
                handle
                description
                title
                productType
                totalInventory
                variants(first: 5) {
                  edges {
                    node {
                      id
                      title
                      quantityAvailable
                      priceV2 {
                        amount
                        currencyCode
                      }
                    }
                  }
                }
                priceRange {
                  maxVariantPrice {
                    amount
                    currencyCode
                  }
                  minVariantPrice {
                    amount
                    currencyCode
                  }
                }
                images(first: 1) {
                  edges {
                    node {
                      src
                      altText
                    }
                  }
                }
              }
            }
          }
        }
      `,
    })

    return {
      statusCode: 200,
      body: JSON.stringify(shopifyResponse),
    }
  } catch (error) {
    console.log(error)
  }
}

You can find the final file in the project here.

See a specific product's details

Once a user can see all the items, the next step is to allow them to see a specific product's details. While you might be able to query all your items at once in the initial "get-product-list" request, this is not scalable long term since we cannot guarantee an item will appear in the first 100 items.

As a result, what we want to do is a create an endpoint designed to fetch a product directly based on its handle. You can learn more about the API in the official docs.

Here's how I implemented the endpoint by allowing the endpoint to take a custom itemHandle parameter that can be passed via the URL of the page.

📄 /pages/products/_handle.vue

export default {
  async asyncData({ $http, route }) {
    const productData = await $http.$post('/api/get-product', {
      itemHandle: route.params.handle,
    })
    return {
      product: productData.productByHandle,
    }
  },
}

📄 /netlify/functions/get-product.js

const { postToShopify } = require('./utils/postToShopify')

exports.handler = async (event) => {
  const { itemHandle } = JSON.parse(event.body)

  const shopifyResponse = await postToShopify({
    query: `
      query getProduct($handle: String!) {
        productByHandle(handle: $handle) {
          id
          handle
          description
          title
          totalInventory
          variants(first: 5) {
            edges {
              node {
                id
                title
                quantityAvailable
                priceV2 {
                  amount
                  currencyCode
                }
              }
            }
          }
          priceRange {
            maxVariantPrice {
              amount
              currencyCode
            }
            minVariantPrice {
              amount
              currencyCode
            }
          }
          images(first: 1) {
            edges {
              node {
                src
                altText
              }
            }
          }
        }
      }
    `,
    variables: {
      handle: itemHandle,
    },
  })

  return {
    statusCode: 200,
    body: JSON.stringify(shopifyResponse),
  }
}

The main difference between this GraphQL request and the one we did for get-product-list.js is the fact that we are also making use of the variables which allows you to pass a custom request to Shopify. In this case, since we need to request a specific product, the handle variable is passed into the query in the following lines:

  • query getProduct($handle: String!) - This tells GraphQL that our query will be receiving a variable of type String and it is required
  • productByHandle(handle: $handle) - This makes a request to the "productByHandle" API and passing in our $handle variable to the parameter handle

Other than that, the rest of the fields should look familiar from the last query we made.

You can find the serverless function in the project here and where it's implemented in the project.

Final Thoughts

Through this post, I hope you were able to see how serverless functions provide a key foundation for unlocking our ability to design APIs that we can then use within our favorite frameworks.

For additional resources, be sure to check out:

If you have any questions, be sure to drop them in the issues of the repo and I'll get to them when I can. Happy coding!

Keep reading

Recent posts

Book cover with the title Deliver web project 10 times faster with Jamstack enterprise

Deliver web projects 10× faster

Get the whitepaper