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.
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:
- Basic knowledge of JavaScript
- Node.js ( v16+) installed
- Npm (v8+) installed
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.
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:
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.
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.
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:
-
cd svelte-eCommerce
changes the current directory to the project directory. - You install dependencies by running
npm install
. - 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:
- 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.
Footer Component
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="">
€ {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="">
€ {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 theparams
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.
You can view the details of any of the products by clicking on them.
Add the product to your cart by clicking the Add to Cart button.
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.
Top comments (23)
Hey how can we host this? Do we need separate server for medua and the svelte or we can host them on one server?
In this situation, deployment will be seperate. You can check out this deployment guide from Medusa.
For future reference, the medusa-admin repo is deprecated. Medusa can be directly installed and deployed in your backend app now docs.medusajs.com/admin/quickstart
Thanks for this tutorial !
You are welcome @p19y
Really cool, thanks for sharing!
Thanks @nicklasgellner
Hi, thanks for the great post. this is useful for starting up with an ecommerce app using svelte and medusa. I have built an The open-source frontend for any eCommerce for Litekart, MedusaJS, Woocommerce, Bigcommerce and Shopify. Might wish to checkout
github.com/itswadesh/svelte-commerce
Thanks for your nice comment and great work on building LiteKart. I will check it out and give my feedback.
SEO will definitely be useful for such a store.
Cool. so basically medusa Opensource allows you to avoid paying for shopify subscription plugins. I guess?
Yes! Medusa is the Opensource Shopify alternative
Hello this content is absolutely amazing, is there the possibility to have a small tutorial on the checkout flow too? Or at least some pointers on how that would be handled. Thanks in advance.