Peter Ekene Eze Learn, Apply, Share

Build an ecommerce site with SvelteKit and the Shopify Storefront API

10 min read 2992

Build an ecommerce site with SvelteKit and Shopify APIs

If you have been keeping up to date with the headless commerce ecosystem, you will have heard some chatter in the community about the updated Shopify Storefront API. The API allows store owners to use their Shopify store as a backend service to power any frontend application of their choice.

What this means is that you can have a Shopify store that has all your products and then build your custom ecommerce site with any frontend tool of your choice (like React, Vue, Angular, Next.js, Nuxt, and so on). This allows you to sell the products in your Shopify store via other channels like mobile applications, online games, and web applications.

When we saw this announcement, my team at Netlify decided to take it for a spin and build stuff with it. The result was five different starter templates — Astro, Nuxt, Gridsome, Eleventy, and Angular — all built with a Shopify-powered backend store. Let’s build another one with SvelteKit!

Set up Shopify

The first thing we should probably do is set up a Shopify store. All this will not be possible without one. Here’s how you can quickly set one up for yourself:

If you did all that, take a break and get a glass of water. Then come back and join me. Let’s build this thing!

Set up SvelteKit

To get started with SvelteKit, you might want to take a quick look at SvelteKit’s documentation to get a sense of how it works. Otherwise, stick around and I’ll walk you through just what you need to get this site up!

Install and run SvelteKit with the commands below:

npm init [email protected] sveltekit-shopify-demo
cd sveltekit-shopify-demo
npm install
npm run dev -- --open

These commands will do a couple of things for you:

  • Create a new SvelteKit project for you
  • Install the required packages
  • Open the project on your browser at localhost:3000 like so:

SvelteKit Welcome Page

We made a custom demo for .
No really. Click here to check it out.

Okay, looks like we are all set to start editing this project and making it look like the site we want to build. Oh, and by the way, this is the project we are building, if you’d like to take a peep:

Shopify SvelteKit Product Listings

Alright, let’s get building!

Styling

For convenience, I’ll use a global style file in this project. Open your app.css file and update it with this CSS snippet for styling. That’s it for styling. All we need to do now is reference the right classes in the project files and this application should turn out exactly as expected.

Fetch products from Shopify

What’s an ecommerce site without products, right? I know. If you created your Shopify account and added products to it, you should be able to see your product listing page in your Shopify admin dashboard.

Here’s mine. Thanks to my colleague Tara for creating this store and filling it up with products so that I can use it and pretend that I did all the work.

Store Demo Products

Now what we want to do is make an API call from our SvelteKit application to fetch all these products from our Shopify store and display them on our application. Before we do that, let’s talk about authentication.

Authentication

Wouldn’t it be good to know that the data in your store is protected and you alone can access it? It is. Every Shopify store has credentials that you can use to access them from other applications — in this case, from our SvelteKit application.

If you haven’t generated credentials for your store yet, go ahead and generate the credentials now. Back in your SvelteKit project, create a .env file and update it with your Shopify API keys like so:

VITE_SHOPIFY_STOREFRONT_API_TOKEN = "ADD_YOUR_API_TOKEN_HERE"
VITE_SHOPIFY_API_ENDPOINT = "ADD_YOUR_STORE_API_ENDPOINT_HERE"

Fetch your products

Now that we’ve gotten authentication out of the way, we can go ahead and fetch the products. This is probably a good time to let you know that the Shopify Storefront API is only GraphQL based. This means that there’s no REST alternative, so we’ll define GraphQL queries to interact with the API.

Before we fetch the products, we need a place to store them so that we can subsequently use the product data in other places in our application. This is where Svelte store comes in. If you’d like more information about it, I got you covered — read the linked info.

Create a store.js file in the root of your project folder and update it with this snippet:

// store.js
import { writable } from 'svelte/store';
import { postToShopify } from '../src/routes/api/utils/postToShopify';

export const getProducts = async () => {
    try {
        const shopifyResponse = await postToShopify({
            query: `{
         products(sortKey: TITLE, first: 100) {
          edges {
            node {
              id
              handle
              description
              title
              totalInventory
              productType
              variants(first: 5) {
                edges {
                  node {
                    id
                    title
                    quantityAvailable
                    price
                  }
                }
              }
              priceRange {
                maxVariantPrice {
                  amount
                  currencyCode
                }
                minVariantPrice {
                  amount
                  currencyCode
                }
              }
              images(first: 1) {
                edges {
                  node {
                    src
                    altText
                  }
                }
              }
            }
          }
        }
    }
      `
        });
        return shopifyResponse;
    } catch (error) {
        console.log(error);
    }
};

Okay, what’s going there? Let’s walk through it. First, we define a getProducts query that requests for the first 100 products we have in our Shopify store. Then, we pass the query to our PostToShopify utility function that takes the query, adds our API keys to authenticate the request, and calls the Shopify endpoint.

But you probably noticed that the postToShopify function doesn’t exist yet, so let’s go ahead and create it in the projects src/api/utils folder. If that folder doesn’t exist, you can create it or put the function wherever you want (just be sure to reference it properly). Mine lives in this directory: src/routes/api/utils/postToShopify.js.

Update the file with the snippet below:

export const postToShopify = async ({ query, variables }) => {
    try {
        const result = await fetch(import.meta.env.VITE_SHOPIFY_API_ENDPOINT, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-Shopify-Storefront-Access-Token': import.meta.env.VITE_SHOPIFY_STOREFRONT_API_TOKEN
            },
            body: JSON.stringify({ query, variables })
        }).then((res) => res.json());
        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);
    }
};

N.B., I’m using the environment variables we set earlier to authenticate the requests we are making to Shopify. Be sure to prefix your environment variables with import.meta.env.

With that done, we can now test our implementation to check if we are successfully fetching products from our Shopify store. Go into your project’s src/routes/index.svelte and update it with this snippet:

// src/routes/index.svelte
<script context="module">
    import { products, getProducts } from '../../store';
    export async function load(ctx) {
        await getProducts();
        return { props: { products } };
    }
</script>

<script>
export let products;
</script>
<svelte:head>
    <title>Home</title>
</svelte:head>
<section>
<h2>
    {#each $products as product}
        <p>{product.node.title} </p>
    {/each}
</h2>
</section>

What we are doing here is:

  • Getting the products’ data from our store
  • Passing the products to the index page as a prop
  • Iterating over the entire product data and displaying the name of each product

Let’s check the browser and see if that’s the case:

SvelteKit Products List Fetch

And we are indeed fetching the products from our store correctly.

Congratulations, we’ve gotten over the first task. But of course, we are only doing this to test our implementation. Let’s create two components (ProductCard.svelte and ProductList.svelte) to help us organize and display the products the way we want.

Create components to organize and display the products

Create a components folder in your projects src folder and add the two files I mentioned above. I’ll set it up like this:

// src/components/ProductCard.svelte
<script>
    export let product;
</script>
<section>
    <div class="product-card">
        <div class="product-card-frame">
            <a href={`/products/${product.handle}`}>
                <img class="prodimg" src={product.images.edges[0].node.src} alt={product.handle} />
            </a>
        </div>
        <div class="product-card-text">
            <h3 class="product-card-title">{product.title}</h3>
            <p class="product-card-description">{product.description.substring(0, 60) + '...'}</p>
        </div>
        <a href={`/products/${product.handle}`}>
            <button>View Item {">"}</button>
        </a>
    </div>
</section>

Here, we are expecting a product prop that will be passed into this component from wherever we render it. When we get that prop, we then extract the different product details we need from it and then use those details to construct our product card as you can see in the snippet above.

Let’s do the same for our product listing component. Create a ProductList.svelte file in the src/components folder and set it up like so:

// src/components/ProductList.svelte
<script>
    import ProductCard from '../components/ProductCard.svelte';
    export let products;
</script>
<div class="product-grid">
    {#each products as product}
        <ProductCard product={product.node} />
    {/each}
</div>

Here, we receive a products prop from our index page (that is where we’ll render this component) and iterate over the products to render a product card for each product. With this, we can go over to the index page src/routes/index.svelte and render our productList component. Update it with this snippet:

// src/routes/index.svelte
<script context="module">
    import { products, getProducts } from '../../store';
    export async function load(ctx) {
        await getProducts();
        const productType = ctx.page.query.get('type');
        if (productType) {
            products.update((items) => {
                const updated = items.filter((product) => product.node.productType === productType);
                return updated;
            });
        }
        return { props: { products} };
    }
</script>
<script>
    import ProductList from '../components/ProductList.svelte';
    export let products
</script>
<svelte:head>
    <title>Shoperoni</title>
</svelte:head>
<main>
   <ProductList products={$products} />
</main>

Here, we are doing the following things:

  • Getting the product data from our store
  • Filtering the list of products based on the page query (we’ll use the page query to filter the product list later in a Header component)
  • Passing down the filtered product list to the page as a prop
  • Rendering our ProductList component and passing the product data into it as a prop

That’s it! When we check back on the browser, we should get a better-looking product listing page:

SvelteKit Product Cards Listing Page

Build a product details page

So we’ve set up our product listing page, great! what happens if a user clicks View Item on the screenshot above? At the moment, nothing. Actually, something will happen: the browser will navigate to this route /products/[the-product-title] and it will result in a 404 because that page doesn’t exist yet.

SvelteKit Shopify Product Details Page 404

To create individual product pages, let’s update our store.js file and add another query that will take our product handle and use it to fetch that particular product from our Shopify store.

This would mean that whenever a user visits our individual product page, i.e., /products/aged-gruyere, the product handle aged-gruyere will be available on the page as page.params.handle. We can then use this handle to query Shopify for that product. Update store.js with this query:

// store.js
import { writable } from "svelte/store";
import { postToShopify } from "./src/routes/api/utils/postToShopify";
export const products = writable([]);
export const productDetails = writable([]);
export const getProducts = async () => {
   // get products query
};

// Get product details
export const getProductDetails = async (handle) => {
  try {
    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: handle,
      },
    });

    productDetails.set(shopifyResponse.productByHandle);
    return shopifyResponse.productByHandle;
  } catch (error) {
    console.log(error);
  }
};

Here, we are defining a new query that will fetch a particular product based on the product’s handle which we pass into the query as a variable. When we call getProductDetails() from our dynamic page and pass the product handle to it, we should get the products data returned.

Alright, let’s create the dynamic page that will represent our individual product pages. In the routes folder, create a new routes/products/[handle].svelte file and set it up like so:

// src/routes/products/[handle].svelte
<script context="module">
    import { productDetails, getProductDetails } from '../../../store'; 
    export async function load(ctx) {
        let handle = ctx.page.params.handle;
        await getProductDetails(handle);
        return { props: { productDetails } };
    }
</script>
<script>
    export let productDetails;
    let quantity = 0;
    let product = $productDetails
    let productImage = product.images.edges[0].node.src;
    let productVariants = product.variants.edges.map((v) => v.node);
    let selectedProduct = productVariants[0].id;
        const addToCart = async () => {
        // add selected product to cart
        try {
            const addToCartResponse = await fetch('/api/add-to-cart', {
                method: 'POST',
                body: JSON.stringify({
                    cartId: localStorage.getItem('cartId'),
                    itemId: selectedProduct,
                    quantity: quantity
                })
            });
            const data = await addToCartResponse.json();
            // save new cart to localStorage
            localStorage.setItem('cartId', data.id);
            localStorage.setItem('cart', JSON.stringify(data));
            location.reload();
        } catch (e) {
            console.log(e);
        }
    };
    function price(itemPrice) {
        const amount = Number(itemPrice).toFixed(2);
        return amount + ' ' + 'USD';
    }
</script>
<main> <!-- page content --> </main>

At this point, you might be wondering why we have two <script> tags on this snippet. Well, here’s why: we want the load() function I defined inside the first script to run before the component is rendered. And to do that, we need to add context="module" to the script. Everything else can go into a second script tag without the context prop.

So what we are doing in the code snippet above is:

  • Running the load() function during initialization to get the individual product from our store
  • Passing the productDetails object down to the page as a prop
  • Receiving the productDetails prop in the page
  • Destructuring the productDetails object to get the data we need for the page

Next, let’s use the destructured product data to construct the product details page:

// src/routes/products/[handle].svelte
<script context="module">
//...
</script>
<script>
//...
</script>
<main class="product-page">
    <article>
        <section class="product-page-content">
            <div>
                <img class="product-page-image" src={productImage} alt={product.handle} />
            </div>
            <div>
                <h1>{product.title}</h1>
                <p>{product.description}</p>
                <form>
                    {#if productVariants.length > 1}
                        <div class="product-page-price-list">
                            {#each productVariants as { id, quantityAvailable, title, priceV2 }}
                                <div class="product-page-price">
                                    <input
                                        {id}
                                        bind:value={selectedProduct}
                                        type="radio"
                                        name="merchandiseId"
                                        disabled={quantityAvailable === 0}
                                    />
                                    <label for={id}>
                                        {title} - {price(priceV2.amount)}
                                    </label>
                                </div>
                            {/each}
                        </div>
                    {:else}
                        <div class="product-page-price is-solo">
                            {price(productVariants[0].priceV2.amount)}
                        </div>
                    {/if}
                    <div class="product-page-quantity-row">
                        <input
                            class="product-page-quantity-input"
                            type="number"
                            name="quantity"
                            min="1"
                            max={productVariants[0].quantityAvailable}
                            bind:value={quantity}
                        />
                        <button type="submit" on:click|preventDefault={addToCart} class="button purchase">
                            Add to Cart
                        </button>
                    </div>
                </form>
            </div>
        </section>
    </article>
</main>

And now if we click on the View Item button on the product listing page, we should get the individual product detail page like this:

Svelte Shopify Product Details Pages

Seems like we are just about done. Let’s go ahead and deploy this site!

Deploy to Netlify

Now that we have a product listing page, and we can view our individual product pages, we can go ahead and deploy this site.

To deploy a SvelteKit app, you need to adapt it to the deployment target of your choice. The SvelteKit documentation provides some adapters that you can quickly use to deploy your app. I’ve chosen to deploy to Netlify using the Netlify adapter that is provided by SvelteKit.

The first thing we need to do is install the Netlify adapter into our SvelteKit project:

npm i -D @sveltejs/[email protected]

Then, edit your svelte.config.js file and import the Netlify adapter that we just installed:

import adapter from '@sveltejs/adapter-netlify';
export default {
    kit: {
        adapter: adapter(), 
        target: '#svelte'
    }
};

We have the adapter installed and configured in our SvelteKit project. The next thing we want to do is create a netlify.toml file and set it up like so:

[build]
  command = "npm run build"
  publish = "build/"
  functions = "/functions/"

# Svelte requires node v12
[build.environment]
  NODE_VERSION = "12.20"

What we are doing here is telling Netlify that:

  • The command to run to build this site is npm run build
  • The directory where the built site will be available is /build
  • The directory to find our custom Netlify function is /functions (we are not using any Netlify functions in this project though)
  • We want it to build the site with Node v12.20

Lastly, push the project to Github and head over to your Netlify dashboard and deploy your site from the GitHub repository where you pushed it. If you need help here’s a one-minute guide on deploying to Netlify from GitHub to walk you through it. The demo is also hosted on Netlify if you’d like to explore the site.

Resources and next steps

So we’ve completed the building of a SvelteKit ecommerce site powered by a Shopify backend. In the next tutorial, we’ll hook it up with even more Shopify features as we add cart functionalities to this site. See you then.

LogRocket: See the technical and UX reasons for why users don’t complete a step in your ecommerce flow.

LogRocket is like a DVR for web apps and websites, recording literally everything that happens on your ecommerce app. Instead of guessing why users don’t convert, LogRocket proactively surfaces the root cause of issues that are preventing conversion in your funnel, such as JavaScript errors or dead clicks. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Start proactively monitoring your ecommerce apps — .

Peter Ekene Eze Learn, Apply, Share

12 Replies to “Build an ecommerce site with SvelteKit and the Shopify…”

  1. Hi, I am following your steps but I am running into issues. I get the error message below.

    {
    errors: ‘[API] Invalid API key or access token (unrecognized login or wrong password)’
    }
    Cannot read property ‘length’ of undefined

    However, if i use Postman and use the same query, headers, and url i get my test data back so I am not sure what is wrong.

    1. Hi Jarrod,

      Are you still facing this issue?

      If yes, please provide more details.

      At which point are you getting the error and what are the steps to replicate?

      However, It might be worth checking how you’re authorizing the requests to Shopify seeing as the error has to do with Invalid credentials.

      Check if your code is somehow modifying your keys.

      Happy to take a look at it for you if you provide the required details.

      1. Yes, I am still having problems.

        Here is my fetch code:

        try {
        const result = await fetch(import.meta.env.VITE_SHOPIFY_API_ENDPOINT, {
        method: ‘POST’,
        headers: {
        ‘Content-Type’: ‘application/json’,
        ‘X-Shopify-Storefront-Access-Token’: import.meta.env.VITE_SHOPIFY_STOREFRONT_API_TOKEN
        },
        body: JSON.stringify({ query, variables })
        }).then((res) => res.json());

        if (result.errors) {
        console.log({ errors: result.errors });
        } else if (!result || !result.data) {
        console.log({ result });
        return ‘No results found.’;
        }
        console.log(result);
        return result.data;
        } catch (error) {
        console.log(error);
        }

        I have used the storefront API and the Admin API. When I use the Admin API in Postman, I get the proper results back.

  2. Hey Jarrod,
    I had the same problem and it took me a while to figure it out. It’s not pretty but here is how I got it to work.

    import { getProducts } from ‘../store’;
    export async function load(ctx) {
    let products = await getProducts();
    return { props: { products } };
    }

    export let products = [];
    let data = JSON.stringify(products.products.edges);
    let parsedData = JSON.parse(data);
    console.log(“DATA:::”,parsedData);

    Home

    {#each parsedData as product}
    {product.node.title}
    {/each}

    Would be cool to see a better way, but this is what worked for me.

    1. Hey Mike,

      Thanks for your suggestion. I think it got me on the right path but now I have another error.

      So i get ‘cannot read property ‘edges’ of undefined which i assume is because it is empty. If i just check products to see if there is any data ie.

      let data = JSON.stringify(products);

      the browser console says TypeError: Failed to execute ‘fetch’ on ‘Window’: Request cannot be constructed from a URL that includes credentials:

          1. haha, i missed the temp email as it must have expired. I tweeted you the other day b/c couldnt msg you. hopefully we can end this game of tag lol

Leave a Reply