Building an E-commerce Store: A Step-by-Step Guide with Solidjs and Medusa
Tags: javascript, webdev, tutorial, solidjs
Published: 2023-03-09 05:38:02

## 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?](https://github.com/medusajs/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?](https://github.com/solidjs/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:

```javascript
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:

```javascript
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:

```javascript
medusa develop
```

Navigate to `localhost:9000/store/products/` in your browser, and the following should appear:

![](https://i.imgur.com/tqZ6Kyi.png align="left")

### 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:

```javascript
git clone https://github.com/medusajs/admin medusa-admin
```

Next, cd into the `medusa-admin` directory and install the dependencies using npm:

```javascript
npm install
```

### Step 5: Test the Medusa Admin

To run the Medusa admin, type in the following command:

```javascript
npm start
```

Now navigate to `localhost:7000` on your browser to view the admin page:

![](https://i.imgur.com/rC0Nuqb.png align="left")

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:

```javascript
npx degit solidjs/templates/js solid-app
```

Next, `cd` into `solid-app` and install its dependencies:

```javascript
cd solid-app
npm install
```

Once the installation is complete, start your local development server:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```json
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:

```javascript
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:
    

```javascript
//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:
    

```javascript
//ProductList.jsx
import { createEffect, createSignal, For } from 'solid-js'
import { A } from "@solidjs/router"
import { Box, Typography, Link, Grid } 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"}}>
      <Grid container spacing={1}>
        <For each={products()}>
                {(product) => 
                 <Grid item lg={3} xs={6} sm={6} sx={{ backgroundColor: "#fff" }}>
                <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>
                </Grid>
                }</For>
              </Grid>
                     </Box>
    </Box>
    </>
  ) 
}

export default ProductList;
```

- **Newsletter component:** In the `component` folder create a new `NewsLetter.jsx` file and add the following content:
    

```javascript
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:
    

```javascript
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:
    

```javascript
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",
            }}
          >
            WebShop
          </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:
    

```javascript
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:
    

```javascript
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 lg={6} xs={12} sm={6}>
             <Box>
                      <img sx={{width:"500px"}}
                          alt={productItem()?.title}
                          src={productItem()?.thumbnail} />
                  </Box>
                  </Grid>
                  <Grid item lg={6} xs={12} sm={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:

```javascript
/* @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:

```javascript
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:**

![](https://i.imgur.com/PpYGm6j.png align="left")

**Products page:** On your navbar, click on products to get to the products page:

![](https://i.imgur.com/cJ4Qp2v.png align="left")

**SingleProducts page:** Click on any of the products to get to the single product page:

![](https://i.imgur.com/LtRiBLc.png align="left")

## 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 - https://docs.medusajs.com/introduction

Solid Router - https://github.com/solidjs/solid-router

Material UI - https://suid.io/getting-started/usage