Build a High-Performing Ecommerce with Svelte and Medusa Backend

Build a High-Performing Ecommerce with Svelte and Medusa Backend

Introduction

Online shopping, referred to as ecommerce or electronic commerce, involves purchasing and selling goods and services. The ease of use and security of online transactions has made them increasingly popular among individuals and businesses. However, setting up an ecommerce site is not a simple task. This process requires delivering excellent customer service, processing orders efficiently, and storing customer data.

In this tutorial, you will learn how to build a performant ecommerce site using Medusa and Svelte.

The Svelte ecommerce tutorial source code is readily available on GitHub.

Here is a brief preview of the application.

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/it63nlrgsbver6w05lux.gif

What is Svelte?

Svelte is a tool that helps you create fast web applications. It works similarly to other JavaScript frameworks like React and Vue, which make it easy to build interactive user interfaces.

However, Svelte has an advantage in converting your app into ideal JavaScript during development rather than interpreting the code at runtime.

You don't experience the performance cost of implementation or a delay when your application first loads. You can use Svelte to build your entire app or add it to an existing codebase. You can also create standalone components that work anywhere without the additional burden of working with a traditional framework.

What is Medusa?

Medusa is an open source Node.js-based composable commerce engine that offers a flexible and modular solution for ecommerce businesses. Its architecture consists of three essential components: the Medusa server, the admin dashboard, and the storefront.

It includes many powerful features, such as currency support, product configurations, multi-currency support, and the ability to process manual orders.

Medusa also provides essential ecommerce components like the headless server, the admin, and the storefront as building blocks for an ecommerce stack. With the Medusa storefront, you can build ecommerce stores for any platform as Android, iOS, and the web.

Prerequisites

To follow along, be sure you have the following:

Setup

Medusa server installation

To install Medusa on your computer, follow these steps:

Install Medusa CLI by running the following command in your terminal:


npm install @medusajs/medusa-cli -g

Now create a new Medusa server by running the following command:

medusa new my-medusa-store --seed

Using the --seed flag, the database is populated with demo data that serves as a starting point for the ecommerce store.

Start the Medusa server by running the following command in the ecommerce-store-server directory:

cd my-medusa-store
medusa develop

You can now use a tool like Postman or a browser to test it out.

Open your browser and go to the URL localhost:9000/store/products

If you find something like this in the browser, your connection to the Medusa server is working. Otherwise, review all the steps and ensure nothing is missing.

https://res.cloudinary.com/practicaldev/image/fetch/s--NUoLM-9e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6s6kc31skpgjsjso9b23.png

Medusa Admin Installation

In this section, you will install the Medusa Admin. The Medusa Admin provides many e-commerce features, including managing Return Merchandise Authorization (RMA) flows, store settings, order management, and product management from an interactive dashboard. You can learn more about Medusa admin and its features in this User Guide.

Here are the steps you will need to follow to set up your Medusa admin dashboard.

  • Clone the Admin GitHub repository by running:
git clone https://github.com/medusajs/admin medusa-admin
  • Change to the newly created directory by running:
cd medusa-admin
  • Install the necessary dependencies by running:
npm install
  • Test it out by navigating into the directory holding your Medusa admin and run:
npm run start

The admin runs by default on port 7000. You can access the administration page at localhost:7000. You should see a login page like this one:

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7nsr3vitrb24elw95p36.png

Using the --seed option when setting up Medusa will create a fake admin account for you. The email is admin@medusa-test.com and the password is supersecret. With the Medusa admin account, you can manage your store's products and collections, view orders, manage products, and configure your store and regions.

You can edit or create products through the Medusa admin.

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wr8bob9yl6x71mjv0aos.png

Create and Set Up A Svelte Project

The next step is to create and set up a new Svelte project for the ecommerce project. This Svelte commerce will use SvelteKit since it is the recommended setup method.

To create a new SvelteKit project, run the command below:

npm create svelte@latest svelte-eCommerce

The command above creates a new Svelte project.

https://res.cloudinary.com/practicaldev/image/fetch/s--3DGXDL2t--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ehdxq8zdmwk0nk5anm32.png

During installation, there will be prompts for several optional features, such as ESLint for code linting and Prettier for code formatting. Make sure you select the same option as shown in the image.

The next step is to run the following command:

cd svelte-eCommerce
npm install
npm run dev

This code sets up and runs a development server for a Svelte project. Here's what each line does:

  1. cd svelte-eCommerce changes the current directory to the project directory.

  2. You install dependencies by running npm install.

  3. Running the development server via npm run dev starts the project's development environment. This will compile the project and start a local server, allowing you to view and test the project in your browser.

Setup of Tailwind CSS

You can style this Svelte ecommerce site using Tailwind CSS. Follow this guide on Setting up Tailwind CSS in a SvelteKit project in your svelte-eCommerce directory.

Install Dependencies

Navigate into your svelte-eCommerce directory and install the below dependencies

npm i axios svelte-icons-pack sweetalert2

This command installs three npm packages: axios, svelte-icons-pack, and sweetalert2.

  • axios is a popular JavaScript library that provides a simple API for making HTTP requests to a server. It works in the browser and Node.js environments and is often used to send and receive data from web APIs.

  • svelte-icons-pack is a package of icons for use with the Svelte framework. This package provides a collection of icons that can be easily used in Svelte applications.

  • sweetalert2 is a JavaScript library for creating beautiful, responsive, and customizable alert dialogs. It is often used to provide feedback to users or prompt them for input in web applications.

Medusa with Svelte Storefront

You start building the Svelte ecommerce app here.

Change Svelte Port

In the svelte-eCommerce/vite.config.js file, replace the existing code with the code below

import { sveltekit } from '@sveltejs/kit/vite';

/** @type {import('vite').UserConfig} */
const config = {
    plugins: [sveltekit()],
    server: {
        port: 8000,
        strictPort: false,
    },
};

export default config;

The configuration object specifies the following options:

  1. The server option specifies the port that the development server should listen on (8000) and whether the server should only listen on that exact port or allow any available port (strictPort: false).

By default, the Medusa server allows connections for the storefront on port 8000.

Create a Base URL Environment Variable

Creating an environment file to store your base URL can make switching between development and production environments a breeze. It keeps sensitive information, such as API keys, separate from your codebase.

Create a .env file in the svelte-eCommerce directory with the following content:

VITE_API_BASE_URL="http://localhost:9000"

The BASE_URL environment variable is the URL of your Medusa server.

Create Utility Function

In the src directory, create a folder with the name util. This folder will contain the sharedload function for the products. The sharedload function, located in this directory, is responsible for retrieving data from an API and is designed to be reusable throughout the project where necessary.

In the src/util/shared.js file, paste the code below:

// @ts-nocheck

export const getProducts = async () => {
  try {
const productres = await fetch(`${import.meta.env.VITE_API_BASE_URL}/store/products/`);
    const productdata = await productres.json();
    const products = productdata?.products;
    return {
      products: products,
    };
  } catch (error) {
    console.log("Error: " + error);
  }
};

The preceding code exports a getProducts function that gets data from an API. A list of products is requested using the fetch API. This results in the API response providing the product list. Finally, the function returns an object with the list of products as a property. Notice how you use the BASE_URL defined in the .env file.

Store Components

It's now time to create a reusable component to display products.

The Header, the Footer, and the Products all fall under this section. Each of these components will appear on a different page of your storefront.

Components Directory

In the src directory, create a folder with the name components. It is the directory that will contain all the reusable components for the products.

Navbar Component

In the src directory, create the file src/components/Navbar.svelte and add the following code:

<script>
  export let productId;
  import Icon from "svelte-icons-pack/Icon.svelte";
  import AiOutlineShoppingCart from "svelte-icons-pack/ai/AiOutlineShoppingCart";
</script>

<div>
  <div class="fixed z-50 bg-white topNav w-full top-0 p-3 md:bg-opacity-0">
    <div class="max-w-6xl relative flex mx-auto flex-col md:flex-row">
      <a href="#/" class="md:hidden absolute top-1 right-14">
        <div class="relative">
          <div class="relative">
            <Icon src="{AiOutlineShoppingCart}" />
            {#if productId?.length >= 1}
            <div
              class="absolute px-1 bg-red-400 -top-1 -right-1 rounded-full border-2 border-white text-white"
              id="cart"
              style="font-size: 10px"
            >
              {productId?.length}
            </div>
            {/if}
          </div>
        </div>
      </a>

      <div class="flex-grow font-bold text-lg">
        <a href="/">
          <span>Best Store</span>
        </a>
      </div>

      <div class="menu hidden md:flex flex-col md:flex-row mt-5 md:mt-0 gap-16">
        <div class="flex flex-col md:flex-row gap-12 capitalize">
          <div class="text-red-400 font-bold border-b border-red-400">
            <a href="/"> home</a>
          </div>
          <div class="text-red-400 font-bold border-b border-red-400">
            <a href="/products">products</a>
          </div>
        </div>
        <div class="flex gap-12">
          <a href="#/" class="hidden md:block">
            <div class="relative">
              <Icon src="{AiOutlineShoppingCart}" />
              {#if productId?.length >= 1}
              <div
                class="absolute px-1 bg-red-400 -top-1 -right-1 rounded-full border-2 border-white text-white"
                id="cart"
                style="font-size: 10px"
              >
                {productId?.length}
              </div>
              {/if}
            </div>
          </a>
        </div>
      </div>
    </div>
  </div>
</div>

The code above is the head section component that renders a navigation bar at the top of the page. Home, products, and a shopping cart icon are all displayed in the navigation bar. If the shopping cart has ordered items, a badge will appear on the icon showing the number of items in the cart.

The navigation bar is hidden on small screens and appears as a dropdown menu on larger screens. The component also includes logic for handling key presses and clicks on the links and the shopping cart icon.

In the src directory, create the file src/components/Footer.svelte and add the following code:

<div>
  <div>
    <div class="bg-red-400 py-32 PX-4">
      <div class="max-w-6xl gap-6 mx-auto grid grid-cols-1 md:grid-cols-9">
        <div class="md:col-span-3 py-3 space-y-4">
          <div class="text-2xl font-bold text-gray-100">Best Store</div>
          <div class="text-gray-300 w-60 pr-0">
            At best store, we offer top-quality Hoddies, Joggers, Shorts and a
            variety of other items. We only sell the best grade of products.
          </div>
        </div>
        <div class="md:col-span-2 py-3 space-y-4">
          <div class="text-2xl font-bold text-gray-100">Information</div>
          <div class="text-gray-300 w-60 space-y-2 pr-0">
            <div class="">About Us</div>
            <div class="">Career</div>
          </div>
        </div>
        <div class="md:col-span-2 py-3 space-y-4">
          <div class="text-2xl font-bold text-gray-100">Our Services</div>
          <div class="text-gray-300 w-60 space-y-2 pr-0">
            <div class="">Clothing</div>
            <div class="">Fashion</div>
            <div class="">Branding</div>
            <div class="">Consultation</div>
          </div>
        </div>
        <div class="md:col-span-2 py-3 space-y-4">
          <div class="text-2xl font-bold text-gray-100">Contact Us</div>
          <div class="text-gray-300 w-60 space-y-2 pr-0">
            <div class="">+234 070-000-000</div>
            <div class="">care@best.com</div>
            <div class="">Terms & Privacy</div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

The code above is the bottom section component that renders a footer at the bottom of the page. The footer contains information about the Svelte store, its services, and its contact details. The footer will be used later.

Store Pages

Now, you have to create pages for your Svelte ecommerce storefront.

Upon creating a new project in Svelte, it comes with some default files, including +page.svelt and +page.js

+page.svelte

A +page.svelte component defines a page of your application. By default, pages are rendered both on the server (SSR) for the initial request and in the browser (CSR) for subsequent navigation.

+page.js

Often, a page must load some data before it can be rendered. For this, we add a +page.js (or +page.ts, if you're TypeScript-inclined) module that exports a load function:

In the src/routes directory, replace the content in the files src/routes/+page.svelte with the content below.

Homepage

In the src/routes/+page.svelte file, replace its content with this:

<script>
  // @ts-nocheck

  import Footer from "../components/Footer.svelte";
  import "../app.css";
  import Navbar from "../components/Navbar.svelte";
  import { writable, derived } from "svelte/store";

  // export let data;
  import {getProducts} from '../util/shared';

  let products;
    const productData = async () => {
        const data = await getProducts();
        products = data;
        console.log(products, 'products')
    }

  $: productData()
</script>

<div>
  <Navbar />
   <div class="mt-40">
    <div class="flex">
      <div class="flex-grow text-4xl font-extrabold text-center">
        Best Qualities You Can Trust
      </div>
    </div>
    <div class="flex">
      <div class="flex-grow text-4xl mt-12 font-extrabold text-center">
        <a
          href="/products"
          class="bg-red-400 hover:bg-red-700 text-white font-bold py-2 px-4 rounded "
          >Products</a
        >
      </div>
    </div>
    <div
      class="max-w-12xl mx-auto h-full flex flex-wrap justify-center py-28 gap-10"
    >
    {#if products}
      {#each products?.products as product, i}
        <div class="">
          <div

            class="rounded-lg shadow-lg bg-white max-w-sm"
          >
            <a
              href={`/products/${product.id}`}
              data-mdb-ripple="true"
              data-sveltekit-prefetch
              data-mdb-ripple-color="light"
            >
              <img class="rounded-t-lg" src={product.thumbnail} alt="" />
            </a>
            <div
              class="bg-red-400 py-8 relative font-bold text-gray-100 text-xl w-full flex flex-col justify-center px-6"
            >
              <div class="">{product.title}</div>
              <div class="">
                &euro; {product.variants[0].prices[0].amount / 100}
              </div>
            </div>
          </div>
        </div>
      {/each}
      {/if}
    </div>
  </div>
  <Footer />
</div>

The code above is the homepage of the Svelte ecommerce store. A list of products is displayed. A navigation bar appears at the top of the page, followed by a product details section and a footer at the bottom.

Product Page

First, create a "products" folder within the "src/routes" directory, then create two new files, "+page.svelte" and "+page.js" within the folder.

Add the following code to the src/routes/products/+page. Svelte file:

<script>
  // @ts-nocheck

  import Navbar from "../../components/Navbar.svelte";

  // @ts-nocheck

  import "../../app.css";
    import { getProducts } from "../../util/shared";

    let products;
    const productData = async () => {
        const data = await getProducts();
        products = data;
        console.log(products, 'products')
    }

  $: productData()
</script>

<Navbar />

<div class="mt-40">
  <div class="flex">
    <div class="flex-grow text-4xl font-extrabold text-center">
      Best Qualities You Can Trust
    </div>
  </div>
  <div
    class="max-w-12xl mx-auto h-full flex flex-wrap justify-center py-28 gap-10"
  >

 {#each products?.products as product, i}
      <div class="">
        <div

          class="rounded-lg shadow-lg bg-white max-w-sm"
        >
          <a
            href={`/products/${product?.id}`}
            data-mdb-ripple="true"
            data-sveltekit-prefetch
            data-mdb-ripple-color="light"
          >
            <img class="rounded-t-lg" src={product?.thumbnail} alt="" />
          </a>
          <div
            class="bg-red-400 py-8 relative font-bold text-gray-100 text-xl w-full flex flex-col justify-center px-6"
          >
            <div class="">{product?.title}</div>
            <div class="">
              &euro; {product?.variants[0]?.prices[0]?.amount / 100}
            </div>
          </div>
        </div>
      </div>
    {/each}
  </div>
</div>

The previous code displays a grid of product cards. It consists of a navigation bar at the top and a grid of product cards below. Each product card includes an image, a title, and a price.

The component also includes logic for handling clicks on the product cards and taking the user to a single product page.

The component uses thegetProducts function to get the list of products, which it then displays in the grid using each block. Each block iterates over the list of products and creates a product card for each.

Single Product Page

Create a new [id] folder in the src/routes/products directory with two files src/routes/products/[id]/+page.js and src/routes/products/[id]/+svelte.svelte

src/routes/products/[id] folder creates a route with a parameter, id, that can be used to load data dynamically when a user requests a page like [/products/prod_01GN6666V4R3KWPPTJ0GMD6T](http://localhost:8000/singlepage/prod_01GN6666V4R3KWPPTJ0GMD6TD4)

In the src/routes/products/[id]/+page.js directory, add the following code:

/* eslint-disable no-unused-vars */
// @ts-ignore
export const load = async ({ fetch, params }) => {
    return {
        params
    };
}

This code defines an async function called load that exports a single object containing the params object. The load function receives an object with two properties: fetch and params.

In the src/routes/products/[id]/+page.svelte file, add the following code:


<script>
  // @ts-nocheck

  import "../../../app.css";
  import "../../../components/Navbar.svelte";
  import axios from "axios";
  import Navbar from "../../../components/Navbar.svelte";
  import Swal from 'sweetalert2'

  export let data;
  let responseData = [];
  let currentImg = 0;
  let currentSize = "S";
  let currentPrice = "";
  let variantsId = 0;
  let cartId = "";
  let variantTitle;
  let products = [];
  import { writable, derived } from "svelte/store";
  import {browser} from '$app/environment'

  export const cartDetails = writable({
  cart_id: '',
})

if (!cartId) {
    axios({
        method: 'post',
        url: `${import.meta.env.VITE_API_BASE_URL}/store/carts`,
        withCredentials: true
    })
    .then(response => {
        console.log(response.data.cart.id, 'response.data.cart.id')
        localStorage.setItem("cart_id", response.data.cart.id)
    })
    .catch(error => {
        console.log(error);
    });
}

  const fetchData = async () => {
       cartId = browser && localStorage.getItem('cart_id')
    axios
      .get(`${import.meta.env.VITE_API_BASE_URL}/store/products/${data.params.id}`).then((response) => {
        if (response.data.product) {
            responseData = response?.data
        }
      })
      .catch((err) => {
        console.log("error", err)
      });
  };

$: fetchData();

</script>

<div class="mt-40">
  <main>
  <Navbar productId={JSON.parse( browser && localStorage.getItem('cart'))} />
    <div class="py-20 px-4">
      <div class="text-white max-w-6xl mx-auto py-2">
        <div class="grid md:grid-cols-2 gap-20 grid-cols-1">
          <div>
            <div class="relative">
              <div>
                <div class="relative">

                  <img src={responseData.product?.images[currentImg]?.url} alt="no image_" />
                  <div class="absolute overflow-x-scroll w-full bottom-0 right-0 p-4 flex flex-nowrap gap-4">
                    <div class="flex w-full flex-nowrap gap-4 rounded-lg">

                    {#if responseData?.product?.images}
                    {#each responseData.product.images as img, i}

                        <div
                        key={i}
                        on:click={() => (currentImg = i)}
                        title={responseData.product.images[i].url}
                        class="w-16 h-24 flex-none"
                        on:keydown={() => (currentImg = i)}
                        >
                        <div
                            class="h-full w-full rounded-lg cursor-pointer shadow-lg border overflow-hidden"
                        >
                            <img
                            src={responseData.product.images[i].url}
                            alt=""
                            class="h-full w-full"
                            />
                        </div>
                        </div>
                    {/each}
                    {/if}
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
          <div>
            <div class="flex md:flex-col flex-col space-y-7 justify-center">
              <div class="text-black space-y-3">
                <h2 class="font-bold text-xl text-black">{responseData?.product?.title}</h2>
                <p class="text-sm">{responseData?.product?.description}</p>
              </div>
              <div class="space-y-3">
                <div class="font-bold text-md text-black">Select Size</div>
                <div class="flex flex-row flex-wrap gap-4">
                {#if responseData?.product?.variants}
                    {#each responseData?.product?.variants as variant, i}
                    { variantTitle = variant.title.split("/")[0]}
                      <div>
                      <div
                        on:click={() => {
                           currentSize = variant?.title?.split("/")[0]
                           currentPrice = variant?.prices[0]?.amount[i]
                           variantsId = variant.id
                        }}
                        on:keydown={() => {
                           currentSize = variant?.title?.split("/")[0]
                           currentPrice = variant?.prices[0]?.amount[i]
                           variantsId = variant.id
                        }}

                        class={currentSize === variant?.title?.split("/")[0] ? 'border-purple-300 bg-red-400' : 'border-gray-100'}
                        contenteditable={false}
                      >
                        <span class="text-black text-sm">{variant?.title?.split("/")[0]}</span>
                      </div>
                    </div>
                    {/each}
                {/if}
                </div>
              </div>
              <div class="space-y-3">
                <div class="font-bold text-md text-black">Price</div>

                    {#if responseData?.product?.variants}
                        <div class="text-black">${responseData?.product?.variants.map(x => x.prices[0]?.amount)[0]}</div>
                    {/if}

              </div>

            </div>
          </div>
        </div>
      </div>
    </div>
 </main>
</div>

The code above fetches the product information and displays it on the page.

Add to cart Implementation

The purpose of this section is to explain how the add-to-cart feature works.

In the src/routes/products/[id]/+page.svelte file, add the addProduct function below the fetchData function and the button to add the product below the Price label.

<script>

  // ...
const addProduct = async (data) => {
  let cartId = browser && localStorage.cart_id;

  try {
    const response = await axios.post(
      `${import.meta.env.VITE_API_BASE_URL}/store/carts/${localStorage.cart_id}/line-items`,
      {
        variant_id: data,
        quantity: 1,
        metadata: {
          size: currentSize,
        },
      }
    );

    products = [response.data?.cart];
    browser && localStorage.setItem("cart", JSON.stringify([products]));
    if (response?.data?.cart) {
      if (response?.status === 200) {
        Swal.fire({
          icon: "success",
          title: "Item Added to Cart Successfully",
          showConfirmButton: true,
        }).then((res) => {
          if (res.isConfirmed) {
            window.location.reload();
          }
        });
      }
    }
  } catch (error) {
    console.log(error);
  }
};

// ...
</script>

<div class="mt-40">
  <main>
    <!-- ... -->
    <button
      class="bg-red-400 text-white font-bold py-2 px-4 rounded-full"
      on:click={() => {
        if (variantsId === 0) {
          alert("Please select a size before adding to cart.");
        } else {
          addProduct(variantsId);
        }
      }}
    >
      Add to Cart
    </button>
    <!-- ... -->
  </main>
</div>

The addProduct function is used to add the product to the cart using the selected variant ID. The code then displays the product data using Svelte bindings and event listeners.

Testing the Svelte Ecommerce

Follow the steps below to test your Svelte ecommerce:

Navigate into your Medusa server and run:

medusa develop

Navigate into your svelte-eCommerce directory and run:

npm run dev

Your Svelte ecommerce storefront is now running at localhost:8000 To view your homepage, visit localhost:8000 in your browser. You should see the homepage.

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o4u1ruso7l1s8pghxlx2.png

You can view the details of any of the products by clicking on them.

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/agchb2ddjd2h2xpa8rgx.png

Add the product to your cart by clicking the Add to Cart button.

Add to cart description

Conclusion

This tutorial demonstrates how to connect a Svelte ecommerce application to a Medusa server and implement an "Add to Cart" feature that allows customers to add items to their cart and manage its content. Other features that can be added to the application with the help of the Medusa server include a checkout flow for placing orders and integration with a payment provider such as Stripe.

The possibilities are endless when it comes to running an ecommerce store with Medusa, including but not limited to;

  • Integrate a payment provider such as PayPal

  • Add a product search engine to your storefront using Meilisearch

  • Authenticate Customer using the Authenticate Customer endpoint that allows the authorization of customers and manages their sessions.

  • Additionally, check out how to use the Authenticate Customer endpoint in this tutorial.

Should you have any issues or questions related to Medusa, then feel free to reach out to the Medusa team via Discord.