DEV Community

Cover image for Building Composable Commerce with Nuxt, Shopify, and Storyblok Crash Course Part Three
Jakub Andrzejewski
Jakub Andrzejewski

Posted on • Edited on • Originally published at storyblok.com

Building Composable Commerce with Nuxt, Shopify, and Storyblok Crash Course Part Three

In this section, we will replace current mocks for both Home Page and Product Page with actual data from the Shopify. To integrate with Apollo, we will be using the official Apollo module for Nuxt.

Nuxt Apollo

The installation is very similar to other modules that we have already added to our storefront. Let’s install the module with the following command:

yarn add -D @nuxtjs/apollo@next
Enter fullscreen mode Exit fullscreen mode

Next, let’s add it to the modules array in nuxt.config.ts file:

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  modules: ['@nuxtjs/tailwindcss', '@nuxt/image-edge', '@nuxtjs/apollo'],
  ...
})
Enter fullscreen mode Exit fullscreen mode

But that is not it yet as we also need to configure Apollo to fetch the data from Shopify. We can do so by registering a new client with a host and by passing a X-Shopify-Storefront-Access-Token as a header.

Let’s add the following apollo configuration object in our nuxt.config.ts file:

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  ...
  apollo: {
    clients: {
      default: {
        httpEndpoint: process.env.SHOPIFY_STOREFRONT_HOST,
        httpLinkOptions: {
          headers: {
            'X-Shopify-Storefront-Access-Token': process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN
          },
        }
      }
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

Now, we also need to create a .env file with these environment variables. For this tutorial, I used the publicly available credentials from Shopify demo playground so no need to worry about them:

SHOPIFY_STOREFRONT_HOST=https://graphql.myshopify.com/api/2023-01/graphql.json
SHOPIFY_STOREFRONT_ACCESS_TOKEN=ecdc7f91ed0970e733268535c828fbbe
Enter fullscreen mode Exit fullscreen mode

For your project, remember to replace them to fetch the data from your store instead.

Fetching data from Shopify

As we have already configured our Apollo GraphQL client to work with Shopify, we can now start fetching the actual data from the platform and populate our storefront application with it.

Let’s create a new folder graphql and inside of it a new file called getProductsQuery.ts. In here, we will write a GraphQL query that will be responsible for fetching the data about our products and will also accept some variables like the number of products or query.

export const getProductsQuery = gql`
query Products ($first: Int!, $query: String) {
  products(first: $first, query: $query) {
    edges {
      node {
        id
        images(first: 1) {
          edges {
            node {
              src
            }
          }
        }
        title
        description
        handle
        priceRange {
          maxVariantPrice {
            amount
            currencyCode
          }
        }
      }
    }
  }
}
`
Enter fullscreen mode Exit fullscreen mode

In this query, we will be fetching data about products that will be used later on in our Vue component as product title, image, price, etc. Apart from mandatory param of first which will be used to determine how many products we want to fetch, we will also add here an optional query parameter that will be used later in the Product page to fetch related products. To use it, we will modify the index.vue page in a following way:

<script setup lang="ts">
import { getProductsQuery } from '../graphql/getProductsQuery';

const variables = { first: 3 }
const { data } = await useAsyncQuery(getProductsQuery, variables)
</script>

<template>
  <div>
    <HeroBanner />
    <div class="flex my-20">
      <ProductCard
        v-for="{ node } in data.products.edges"
        :key="node.id"
        :image="node.images.edges[0].node.src"
        :title="node.title"
        :price="`${node.priceRange.maxVariantPrice.amount} ${node.priceRange.maxVariantPrice.currencyCode}`"
        :link="`products/${node.handle}`"
        :description="node.description"
      />      
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Let’s stop for a second to explain each step individually:

  1. We are importing previously created GraphQL query.
  2. We are registering a new variable called variables where we could pass the amount of products o query.
  3. We are calling a useAsyncQuery composable that will under the hood send this query to Shopify (with variables as well).
  4. We have access to the response from a data variable.
  5. We are using the response data in the template to display a list of products (the complexity of nested properties is caused by nesting in Shopify, we cannot do anything about it unfortunately 😞).

If we did everything properly, we should see the following result in the browser:

Product List

The images are not yet properly optimized as we need to add them to the [image.domains](http://image.domains) array in nuxt.config.ts. Let’s do this now:

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  ...
  image: {
    domains: ['mdbootstrap.com', 'cdn.shopify.com']
  },
  ..
})
Enter fullscreen mode Exit fullscreen mode

Our Home Page looks good so far. Let’s focus right now on the Product Page so that we could navigate from the Home Page and have real data here as well!

In the graphql folder, create a new file called getProductQuery and add the following code:

export const getProductsQuery = gql`
  query Product($handle: String!) {
    productByHandle(handle: $handle) {
      id
      title
      productType
      priceRange {
        maxVariantPrice {
          amount
          currencyCode
        }
      }
      description
      images(first: 1) {
        edges {
          node {
            src
          }
        }
      }
      variants(first: 1) {
        edges {
          node {
            id
          }
        }
      }
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

In here, we are fetching the data about our product that we will display in the Product Page. We will be using handle as the unique identifier for each product. Apart from the regular data, we will be also fetching the variant ID that will be used later on for redirecting to checkout.

Now, let’s use it in the pages/products/[handle].vue page to fetch the data about the product after visiting the Product Page with a certain handle:

<script setup lang="ts">
import { getProductQuery } from '~~/graphql/getProductQuery';
const route = useRoute()

const { data: product } = await useAsyncQuery(getProductQuery, { handle: route.params.handle })
const price = computed(() => `${product.value.productByHandle.priceRange.maxVariantPrice.amount} ${product.value.productByHandle.priceRange.maxVariantPrice.currencyCode}`)
</script>

<template>
  <section>
    <div class="grid grid-cols-2 items-center px-20">
      <NuxtImg
        :src="product.productByHandle.images.edges[0].node.src"
        class="rounded-lg shadow-lg -rotate-6"
        alt="Product Image"
        format="webp"
      />
      <div class="rounded-lg shadow-lg p-12 backdrop-blur-2xl">
        <h2 class="text-4xl font-bold mb-6">{{ product.productByHandle.title }}</h2>
        <p class="text-gray-500 mb-6">
          {{ product.productByHandle.description }}
        </p>

        <button
          class="px-7 py-3 bg-green-600 text-white font-medium text-sm rounded shadow-md hover:bg-green-700 hover:shadow-lg focus:bg-green-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-green-800 active:shadow-lg transition duration-150 ease-in-out"
        >
          Pay {{ price }}
        </button>
      </div>
    </div>
  </section>
</template>
Enter fullscreen mode Exit fullscreen mode

A lot of things have happened here so let’s stop for a second to discuss each meaningful part:

  1. Import getProductQuery GraphQL query.
  2. Utilize useRoute composable to get access to route params where our handle lives.
  3. Call useAsyncQuery composable with getProductQuery query and pass handle as variables.
  4. Create a helper computed property that will create a price of the product in a form 5 $
  5. Use the data in the template.

If we did everything correctly, we should see the following result in the browser:

Product Detail Page

To not put all the logic in the same time, I decided to split fetching data about the product from fetching data about the related products. Let’s do it right now.

We will add another call to the Shopify to fetch related products (I intentionally removed some of the code so that it will be easier to understand):

<script setup lang="ts">
...
import { getProductsQuery } from '~~/graphql/getProductsQuery';
...

const { data: product } = await useAsyncQuery(getProductQuery, { handle: route.params.handle })
...
const { data: related } = await useAsyncQuery(getProductsQuery, { first: 3, query: `product_type:${product.value.productByHandle.productType}`, })
</script>

<template>
  <section>
    ...
    <div class="flex my-20">
      <ProductCard
        v-for="{ node } in related.products.edges"
        :key="node.id"
        :image="node.images.edges[0].node.src"
        :title="node.title"
        :price="`${node.priceRange.maxVariantPrice.amount} ${node.priceRange.maxVariantPrice.currencyCode}`"
        :link="`/products/${node.handle}`"
        :description="node.description"
      />      
    </div>
  </section>
</template>
Enter fullscreen mode Exit fullscreen mode

Let’s stop for a second here to explain what was done:

  1. Import getProductsQuery GraphQL query.
  2. Call useAsyncQuery composable where we are passing variables to get first three products and as a query we are passing product_type:${product.value.productByHandle.productType} → this is Shopify-specific way of fetching data based on some query conditions.
  3. We use the related products data in the template

If we did everything correctly, we should see the following result in the browser:

Related Products

Uff that was a lot! There is only one thing left to do here in terms of Shopify itself and it is a mutation that will create a Check out session once we click Pay button. Let’s add it now.

We will create a new file in graphql folder called createCheckoutMutation.ts :

export const createCheckoutMutation = gql`
  mutation Checkout($variantId: ID!) {
    checkoutCreate(
      input: { lineItems: { variantId: $variantId, quantity: 1 } }
    ) {
      checkout {
        webUrl
      }
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

As a required parameter we will pass a variantId that we will get from fetching the data about the product in Product Page. As a return value we will get the url of checkout that we will redirect the user to.

Now, let’s use it in thepages/products/[handle].vue page

<script setup lang="ts">
import { createCheckoutMutation } from '~~/graphql/createCheckoutMutation';
...

const { data: product } = await useAsyncQuery(getProductQuery, { handle: route.params.handle })
...
...

const redirectToPayment = async () => {
  const { data } = await useAsyncQuery(createCheckoutMutation, { variantId: product.value.productByHandle.variants.edges[0].node.id })

  window.location.href = data.value.checkoutCreate.checkout.webUrl
}
</script>

<template>
  <section>
    <div class="grid grid-cols-2 items-center px-20">
        ...
        <button
          @click="redirectToPayment"
          class="px-7 py-3 bg-green-600 text-white font-medium text-sm rounded shadow-md hover:bg-green-700 hover:shadow-lg focus:bg-green-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-green-800 active:shadow-lg transition duration-150 ease-in-out"
        >
          Pay {{ price }}
        </button>
                ...
  </section>
</template>
Enter fullscreen mode Exit fullscreen mode

Let’s stop for a second here to explain what was done:

  1. Import createCheckoutMutation GraphQL mutation.
  2. Create a new method called redirectToPayment . It will send the mutation to Shopify with the parameter of variantId.
  3. We are redirecting the user to the webUrl returned by the previous GraphQL mutation.

If we did everything correctly, after clicking a Pay button, after a while we should be redirected to the Shopify checkout page like the following (the url could be something like https://graphql.myshopify.com/checkouts/co/8201a00b239e6d9bd081b0ee9fdaaa38/information:

Shopify Checkout

It was a lot of stuff but together we have managed to make through it! Now, we will move into Storyblok to see how we can add the dynamic content and also, use the Shopify plugin for Storyblok 🚀

Top comments (2)

Collapse
 
bernardao profile image
Iago

I found the solution. It was a problem with @nuxtjs/apollo version in package.
The working one it's the same as appears on the repo
"@nuxtjs/apollo": "5.0.0-alpha.5"
I was using
"@nuxtjs/apollo": "5.0.0-alpha.14"
And the mutation request doesn't work

Collapse
 
bernardao profile image
Iago

Thank you for the course Jakub.
I found 1 errata.
In the getProductQuery file in the first line
export const getProductsQuery = gql
getProductsQuery it should be in singular getProductQuery

I'm unable to finish the course, since the function redirectToPayment is not working for me, I checked in github and it's the same
const redirectToPayment = async () => {
const { data } = await useAsyncQuery(createCheckoutMutation, { variantId: product.value.productByHandle.variants.edges[0].node.id })
window.location.href = data.value.checkoutCreate.checkout.webUrl
}

I'm getting 2 warnings and the redirect it doesn't work

[Vue warn]: Unhandled error during execution of native event handler

[nuxt] [useAsyncData] Component is already mounted, please use $fetch instead. See nuxt.com/docs/getting-started/data...