Building an E-commerce Store: A Step-by-Step Guide with Solidjs and Medusa

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:

  1. How to set up the Medusa server and Medusa admin.

  2. How to Create an e-commerce storefront using Solid.

  3. How to Integrate Solid with Medusa

  4. How to loop over an array of objects using the <For> component.

  5. How to create client-side routing using Solid Router.

  6. 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 it component. Then within the component folder, make a new Header.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 it ProductList.jsx. Open the newly created ProductList.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" }}>
                    &euro; {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 new NewsLetter.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 new Footer.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 it pages. Then within the pages folder, create a new Home.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 new Products.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 the pages 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}>&euro; {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

Did you find this article valuable?

Support Chukwuka Reuben by becoming a sponsor. Any amount is appreciated!