Building an E-commerce Store: A Step-by-Step Guide with Solidjs and Medusa
Introduction
The term “e-commerce” simply refers to the online sale of goods and services.
People can use an e-commerce store to buy and sell tangible (physical) goods, digital products, or services online. You will build an e-commerce store using Solid(as the storefront) and Medusa(as the backend).
By creating this app, you will learn:
How to set up the Medusa server and Medusa admin.
How to Create an e-commerce storefront using Solid.
How to Integrate Solid with Medusa
How to loop over an array of objects using the
<For>
component.How to create client-side routing using Solid Router.
How to create beautiful UIs using Material UI
and much more...
What is Medusa?
Medusa is an open-source composable e-commerce engine for developers who want to take ownership of their e-commerce stack. Medusa is made up of three parts:
The headless backend.
Admin dashboard.
Storefront.
Medusa is a versatile Javascript e-commerce platform that enables developers to take advantage of its features and capabilities regardless of the framework they use to build their web applications.
What is Solid?
Solid is a blazing-fast javascript framework that dodges virtual dom manipulation. Solid is a JavaScript framework for making interactive web applications.
With Solid, you can use your existing HTML and JavaScript knowledge to build components that can be reused throughout your app. Solid provides the tools to enhance your components with reactivity: declarative JavaScript code that links the user interface with the data that it uses and creates.
Let's create a cool e-commerce website using Solid.
Getting Started with Medusa
In this portion of this article, I am going to show you how to set up the Medusa server on your local machine.
Step 1: Install the Medusa CLI Tool
Medusa CLI is a tool that lets you run important commands while working with Medusa.
To use Medusa, you must first install the CLI tool, which is used to create a new Medusa server.
Using either npm
or yarn
, we can install Medusa CLI, but this tutorial will use npm
. Open any terminal of your choice and run the following command:
npm install @medusajs/medusa-cli -g
Step 2: Create a new Medusa Store Server
The store server is the main component that houses all of the store's logic and data.
This server will include all the functionalities related to your store's checkout workflow. Cart management, shipping, shipping, payment providers, user management, and other features are included.
Run the command below to create your new Medusa store server:
medusa new my-medusa-store --seed
Things to note from the preceding command:
my-medusa-store
represents the project's name, which you can change to whatever you want.The
--seed
command seeds your database with some sample data to get you started. Including a store, an administrator account, a region and products with variants.
Step 3: Test the Medusa Store Server
To test the Medusa store server, cd into my-medusa-store
, and run the command below:
medusa develop
Navigate to localhost:9000/store/products/
in your browser, and the following should appear:
Step 4: Set up Medusa Admin
The Medusa admin dashboard allows us to perform all kinds of actions on our system data. It provides a way for you to create new products, edit your products, delete your products, view orders and so on.
The Medusa admin is connected to the Medusa store server, so make sure that you have successfully installed and tested the Medusa store server first before proceeding with the admin.
To install the admin, start by cloning the admin Github repository:
git clone https://github.com/medusajs/admin medusa-admin
Next, cd into the medusa-admin
directory and install the dependencies using npm:
npm install
Step 5: Test the Medusa Admin
To run the Medusa admin, type in the following command:
npm start
Now navigate to localhost:7000
on your browser to view the admin page:
The email is admin@medusa-test.com
and the password is supersecret
.
Note: Before you can test the Medusa admin, you need to make sure the Medusa server is running.
Building the Storefront with Solid
In this section of the article, I'll demonstrate how to use Solidjs to build the storefront for your e-commerce application.
Step 1: Create a new Solid App
Run the following command in your terminal to create a new Solid app:
npx degit solidjs/templates/js solid-app
Next, cd
into solid-app
and install its dependencies:
cd solid-app
npm install
Once the installation is complete, start your local development server:
npm run dev
Step 2: Install all Necessary Dependencies
To build the e-commerce app more quickly, you will need some dependencies.
We will be able to easily create stunning UIs with the help of Material UI. Run the following command to install Material UI for Solid:
npm install @suid/material @suid/icons-material
Solid Router is a universal router for Solidjs that works whether you are rendering on the client or on the server. To install Solid Router run the command below:
npm i @solidjs/router
Medusa JS Client is an SDK that provides easy an way to access the Medusa API. Run the following command to install the Medusa JS Client:
npm install @medusajs/medusa-js
Step 3: Integrate Solid with Medusa
At the root of your solid-app project, create a .env
file and add the following content to it:
VITE_baseUrl=http://localhost:9000
baseUrl
environment variable is the URL of your Medusa server.
Medusa uses Cross-Origin Resource Sharing(CORS) to only allow specific origins to access the server.
You must configure Vite because Medusa by default only accepts access to storefronts on port 8000, while Vite's default port is 5173.
Navigate into vite.config.json
and replace the content with the following:
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';
//https://vitejs.dev/config/
export default defineConfig({
plugins: [solidPlugin()],
server: {
port: 8000,
},
build: {
target: 'esnext',
},
});
Open a new browser tab and navigate to http://localhost:8000
after restarting your development server.
Note: Any request your storefront makes to the server will be denied by Medusa if Vite is not configured to use the required port number, 8000.
Next, create a utility that will enable the reuse of the Medusa JS Client instance throughout your application.
Create a new file src/utils/client.js
and add the following content:
import Medusa from "@medusajs/medusa-js"
const medusaClient = new Medusa({ baseUrl: import.meta.env.VITE_baseUrl })
export { medusaClient }
Step 4: Building the Storefront Components
In this section of this article, we will build the storefront component for our app. One of the components we are going to create is the:
- Header Component: Make a new folder in the
src
directory and call itcomponent
. Then within thecomponent
folder, make a newHeader.jsx
file and add the following content:
//Header.jsx
import { A } from "@solidjs/router";
import { Box } from "@suid/material";
import { Typography, IconButton, AppBar, Toolbar } from "@suid/material";
import ShoppingCartIcon from "@suid/icons-material/ShoppingCart";
const Header = () => {
const cartCount = localStorage.getItem("cartCount") ?? 0;
return (
<Box>
<AppBar position="fixed" color="primary">
<Toolbar>
<Box
sx={{
display: "flex",
width: "100%",
alignItems: "center",
flexDirection: {
lg: "row",
md: "column",
sm: "column",
xs: "row",
},
justifyContent: "space-between",
}}
>
<Typography
variant="h6"
component="h6"
sx={{
marginBottom: { lg: "0", md: "0.7rem", sm: "0.7rem", xs: "0" },
}}
>
Reuben09
</Typography>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
marginBottom: { lg: "0", md: "0.7rem", sm: "0.7rem", xs: "0" },
}}
>
<A href="/">
<Typography
sx={{
marginRight: "1rem",
fontSize: "1.1rem",
cursor: "pointer",
}}
>
Home
</Typography>
</A>
<A href="/products">
<Typography
sx={{
marginRight: "1rem",
fontSize: "1.1rem",
cursor: "pointer",
}}
>
Products
</Typography>
</A>
<IconButton>
<ShoppingCartIcon sx={{ color: "#fff" }} />
</IconButton>
<p>{cartCount}</p>
</Box>
</Toolbar>
</AppBar>
</Box>
);
};
export default Header;
ProductList Component: This component will display lists of products fetched from the Medusa server using the
medusaClient
utility function, using Solid Router, the component will also create links to individual products.Inside the
component
folder, make a new file and call itProductList.jsx
. Open the newly createdProductList.jsx
file and add the following content:
//ProductList.jsx
import { createEffect, createSignal, For } from "solid-js";
import { A } from "@solidjs/router";
import { Box, Typography, Link } from "@suid/material";
import { medusaClient } from "../utils/client.js";
function ProductList() {
const [products, setProducts] = createSignal([]);
createEffect(() => {
const fetchProducts = async () => {
const res = await medusaClient.products.list();
setProducts(res.products);
};
fetchProducts();
});
return (
<>
<Box
sx={{
marginTop: "2rem",
paddingLeft: { lg: "4rem", xs: "1.5rem" },
paddingRight: { lg: "4rem", xs: "1.5rem" },
}}
component="section"
>
<Typography
sx={{
textAlign: "center",
fontSize: "1.5rem",
marginBottom: "2rem",
}}
>
Featured Products
</Typography>
<Box sx={{ margin: "0 2rem" }}>
<Box
sx={{
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr 1fr",
gridGap: "2rem",
}}
>
<For each={products()}>
{(product) => (
<Box
component="div"
sx={{
backgroundColor: "rgb(231, 231, 231)",
height: "350px",
padding: "0.5rem",
}}
>
<Box component="div" sx={{ height: "12rem" }}>
<Box
component="img"
src={product?.thumbnail}
sx={{
height: "12rem",
width: "100%",
objectFit: "cover",
}}
/>
</Box>
<Typography
sx={{
padding: "1rem 0",
textAlign: "center",
fontWeight: "600",
}}
>
{product?.title}
</Typography>
<Typography sx={{ textAlign: "center" }}>
€ {product?.variants[0].prices[0].amount / 100}
</Typography>
<Box sx={{ textAlign: "center" }}>
<Link underline="always">
<A href={`/products/${product.id}`}>See product</A>
</Link>
</Box>
</Box>
)}
</For>
</Box>
</Box>
</Box>
</>
);
}
export default ProductList;
- Newsletter component: In the
component
folder create a newNewsLetter.jsx
file and add the following content:
import {
Grid,
Box,
Typography,
Input,
IconButton,
Button
} from "@suid/material";
function NewsLetter() {
return (
<Box
component="section"
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
paddingLeft: { lg: "4rem", xs: "2rem" },
paddingRight: { lg: "4rem", xs: "2rem" }
}}
>
<Box component="form">
<Typography
variant="h2"
mt={10}
sx={{
textAlign: "center",
fontSize: "1.5rem",
marginBottom: "0.3rem",
}}
>
{" "}
Subscribe to our newsletter
</Typography>
<Typography sx={{textAlign: "center", marginBottom: "1rem" }}>
Get 10% off your first purchase and stay on top of the latest in
Debutify, it's win-win-WIN!
</Typography>
<Box sx={{marginBottom: "2rem" }}>
<Input
placeholder="First Name"
sx={{textAlign: "center", width: "100%" }}
/>
</Box>
<Box sx={{marginBottom: "2rem" }}>
<Input
placeholder="Your Email"
sx={{textAlign: "center", width: "100%" }}
/>
</Box>
<Box sx={{textAlign: "center", width: "100%", marginBottom: "1rem" }}>
<IconButton>
<Button variant="contained" color="primary">
Subscribe
</Button>
</IconButton>
</Box>
</Box>
</Box>
);
}
export default NewsLetter;
- Footer component: Also in the
component
folder, create a newFooter.jsx
file and add this content:
import { Box, Typography } from "@suid/material";
import NewsLetter from "./NewsLetter";
function Footer (){
return (
<>
<Box component="section" sx={{ marginBottom: "2rem" }}>
<NewsLetter />
</Box>
<Box
component="div"
sx={{textAlign: "center", backgroundColor: "#1976D2", padding: "1rem" }}
color="primary"
>
<Box
component="a"
sx={{textAlign: "center", color: "#fff" }}
href="https://github.com/Reuben09"
>
created by Reuben09
</Box>
</Box>
</>
)
}
export default Footer;
Step 5: Create the Storefront Pages
From here it gets easy, all you need to do is import the components that you have created in the previous section into these pages:
- Home page: Make a new folder in the
src
directory and call itpages
. Then within thepages
folder, create a newHome.jsx
file and add the following content:
import { Box, Typography, IconButton, Button } from "@suid/material";
import ProductList from '../component/ProductList'
import Footer from '../component/Footer'
function Home (){
return <div>
<Box
sx={{
backgroundImage:
"url(https://images.unsplash.com/photo-1552346154-21d32810aba3?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=870&q=80)",
width: "100%",
height: "80vh",
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
marginBottom: "5rem"
}}
>
<Box
sx={{
width: "100%",
height: "80vh",
backgroundColor: "rgba(25, 118, 210, 0.2)",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center"
}}
>
<Typography
sx={{
fontSize: "3rem",
color: "#ffffff",
}}
>
Reuben09
</Typography>
<Typography
sx={{
fontSize: "3rem",
color: "#ffffff",
}}
>
Sneakers
</Typography>
<Typography sx={{textAlign: "center", color: "#ffffff" }}>
Users of the highest converting shopify theme deserve a lifestyle to
match!
</Typography>
<Box sx={{marginTop: "1rem" }}>
<IconButton>
<Button
variant="contained"
color="primary"
sx={{marginRight: "0.5rem" }}
>
Shop now
</Button>
</IconButton>
<IconButton>
<Button
variant="contained"
color="primary"
sx={{marginLeft: "0.5rem" }}
>
Learn more
</Button>
</IconButton>
</Box>
</Box>
</Box>
<Box component="div" sx={{marginBottom: "2rem" }}>
<ProductList />
</Box>
<Box section="footer">
<Footer />
</Box>
</div>
}
export default Home;
- Products page: Also in the
pages
folder, make a newProducts.jsx
file and add the following content:
import ProductList from '../component/ProductList'
import Footer from '../component/Footer'
function Products (){
return <div>
<ProductList />
<Footer />
</div>
}
export default Products;
SingleProduct page: This page is where we will fetch product items based on their ids and display their details. It will also allow users to add the product to their cart
Make a new
SingleProduct.jsx
file in thepages
folder and paste in the following content:
import { useParams } from "@solidjs/router";
import { createEffect, createSignal } from 'solid-js'
import {Container, Box, Grid, IconButton, Button, Typography} from '@suid/material';
import Footer from '../component/Footer'
import { medusaClient } from '../utils/client.js'
const addProduct = async (cartId, product) => {
const { cart } = await medusaClient.carts.lineItems.create(cartId, {
variant_id: product()?.variants[0].id,
quantity: 1
})
console.log(cart);
localStorage.setItem('cartCount', cart.items.length)
setTimeout(window.location.reload(), 5000)
}
function SingleProduct (){
const [productItem, setProductItem] = createSignal();
const [regionId, setRegionId] = createSignal("");
const params = useParams();
const productId = params.productId;
createEffect(()=> {
const fetchSingleProduct = async () => {
const results = await medusaClient.products.retrieve(productId);
setProductItem(results.product)
}
const fetchRegions = async () => {
const results = await medusaClient.regions.list()
setRegionId(results.regions[1].id)
}
fetchSingleProduct()
fetchRegions()
})
const handleAddToCart = async () => {
const cartId = localStorage.getItem('cartId');
if (cartId) {
addProduct(cartId, productItem)
} else {
const { cart } = await medusaClient.carts.create({ region_id: regionId })
localStorage.setItem('cartId', cart.id);
addProduct(cart.id, productItem)
}
}
return <>
<Box sx={{marginTop: "7rem"}}>
<Container maxWidth="sm">
<Grid container spacing={2}>
<Grid item xs={6}>
<Box>
<img sx={{width:"500px"}}
alt={productItem()?.title}
src={productItem()?.thumbnail} />
</Box>
</Grid>
<Grid item xs={6}>
<Box sx={{marginTop: "2rem", display: "flex", justifyContent: "flex-start", alignItems: "flex-start", flexDirection: "column"}}>
<Typography variant="h5" mb={2}>{productItem()?.title}</Typography>
<Typography mb={2}>€ {productItem()?.variants[0].prices[0].amount / 100 }</Typography>
<Typography mb={2}>{productItem()?.description}</Typography>
<IconButton>
<Button onClick={handleAddToCart} variant="contained" color="primary">
Add to cart
</Button>
</IconButton>
</Box>
</Grid>
</Grid>
</Container>
</Box>
<Footer />
</>
}
export default SingleProduct;
Step 6: Register the Storefront Pages:
In this section of this article, we are going to enable client-side routing in our application using Solid Router.
index.jsx
is our entry point, so we will import Router
and wrap the App component within it.
Open index.jsx
and replace it with the following content:
/* @refresh reload */
import { render } from 'solid-js/web';
import { Router } from "@solidjs/router";
import './index.css';
import App from './App';
const root = document.getElementById('root');
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
throw new Error(
'Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got mispelled?',
);
}
render(() =>
<Router>
<App />
</Router>, root);
That's it! you can now register the three pages. Open App.jsx
and replace it with the following content:
import { Routes, Route } from "@solidjs/router"
import { lazy } from "solid-js";
import Header from './component/Header'
const Products = lazy(() => import("./pages/Products"));
const Home = lazy(() => import("./pages/Home"));
const SingleProduct = lazy(() => import("./pages/SingleProduct"));
function App() {
return (
<div>
<Header />
<Routes>
<Route path="/" component={Home} />
<Route path="/products" component={Products} />
<Route path="/products/:productId" component={SingleProduct} />
</Routes>
</div>
);
}
export default App;
Step 7: Test the Storefront
Restart your server and open a new browser tab at http://localhost:8000
and this is what your browser should display:
Home page:
Products page: On your navbar, click on products to get to the products page:
SingleProducts page: Click on any of the products to get to the single product page:
Conclusion
After introducing Medusa and Solid in this article, I showed how to create an e-commerce website using Medusa (as the backend) and Solid (as the storefront) from scratch.
Reference
Medusa docs - docs.medusajs.com/introduction
Solid Router - github.com/solidjs/solid-router
Material UI - suid.io/getting-started/usage