How I Use Medusa to Create a Powerful Next.js E-commerce Store
Tags: webdev, javascript, tutorial, opensource
Published: 2023-05-04 10:34:41

## Introduction

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3dl1vsvoveqpgml3adbj.png)

[Medusa](https://medusajs.com/) is an open-source modular commerce infrastructure for building highly performant, scalable, and data-intensive commerce applications. Medusa’s modular architecture makes it flexible and extensible. It is a great tool for building e-commerce stores. Medusa provides three (3) building blocks for a commerce system: the backend, an admin dashboard, and a storefront.

Next.js is an open-source React framework for building web applications. Next.js pre-rendering allows your application to be incredibly fast.

In this article, I will share how I built an e-commerce store with Medusa and Next.js. At the end of this article, you will have a solid understanding of how to set up a Medusa backend and admin panel and build your storefront from scratch with Next.js.

---

## Prerequisite

To follow this tutorial, you need:

- Node version 14+ or 16+ on your machine.
- Either npm, yarn, or pnpm. This tutorial will npm.
- Familiarity with JavaScript and Next.js

---

## Setting up the Medusa Backend and an Admin Dashboard

Run the following command in your terminal:

`npx create-medusa-app
`
Give your project a name - for this article, you can name it `my-store`. Choose the `medusa-starter-default`. For the storefront starter, select None. You are going to build a storefront from scratch with Next.js.

Install the admin dashboard in your Medusa backend. You can deploy your Medusa backend and your admin dashboard together later.

```javascript
cd my-store/backend
npm install @medusajs/admin

```
After installing, enable the admin inside `medusa-config.js` by uncommenting the following lines of code inside the `plugins` array.

`medusa-config.js
`

```javascript
const plugins = [
  //...
  {
    resolve: "@medusajs/admin",
    /** @type {import('@medusajs/admin').PluginOptions} */
    options: {
      autoRebuild: true,
    },
  },
];

```
Start your Medusa server:


```javascript
cd my-store/backend
npm run start

```
Your server should be running on port 9000.

Run localhost:9000/store/products inside an API testing platform like Postman or Thunder Client to see the store data populated by the `medusa-starter-default`.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z9ch91gihvt8hxgkop3q.png)

The admin dashboard is available at localhost:9000/app.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dyeenmos3s2snyf7lo9r.png)

Since you do not have a personal login credential yet, use the demo user credentials provided by Medusa.

> Enter admin@medusa-test.com in the email field and supersecret as the password.

After logging in, you can create new and private login credentials (and remove the demo credentials). Take a look at the products in the admin dashboard.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jjf3nm7nu93uoegzyc9u.png)

---

## Building the Storefront with Next.js

Medusa provides a Next.js starter storefront that you can use. However, the goal of this article is to show you how you can build your storefront from scratch using Next.js.

**Create a Next.js app**

```javascript
npx create-next-app@latest medusa-store-client

```
Select your preferred configurations. This tutorial will not make use of TypeScript.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pi3kdbn9ffg39wffiej9.png)

Navigate into your Next.js project, and install a few dependencies that you will need.

```javascript
cd medusa-store-client
npm install @medusajs/medusa-js styled-components

```
Use the Medusa JS Client to interact with the Medusa server and `styled-components`, which is a CSS-in-JS library, to style our components.

---

## Connecting the Storefront to the Medusa Server

By default, Medusa is configured to allow requests from port 8000. However, you can change the default port on the Medusa server. By default, Next.js runs on port 3000. Re-configure it to run on port 8000.

`package.json
`

```javascript
"scripts": {
    "dev": "next dev -p 8000",
}

```
The above code changes the development port to 8000.

Create a Medusa JS Client utility that will be used to make requests to the Medusa server.

Create a file named `medusaClient.js` at the root of your project.

`medusaClient.js`

```javascript
import Medusa from "@medusajs/medusa-js"

const medusaClient = new Medusa({baseUrl: process.env.NEXT_PUBLIC_MEDUSA_SERVER_URL })

export {medusaClient}

```
Put the `NEXT_PUBLIC_MEDUSA_SERVER_URL` in a `.env.local` file.

`.env.local`

```javascript
 NEXT_PUBLIC_MEDUSA_SERVER_URL="http://localhost:9000"

```

---

## Configuring Styled Components and Images

To protect your application from malicious users, Next requires configuration to use external images, and the compiler also needs to be notified of the use of styled-components. Configure styled-components and the URL to our product images inside `next.config.js`.

`next.config.js
`

```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  compiler: {
    styledComponents: true
  },
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: ['medusa-public-images.s3.eu-west-1.amazonaws.com'],
        port: '',

      },
    ],
  },

}

module.exports = nextConfig

```

---

## Creating the Navbar

Create a Navbar for your store inside the components directory and style it with styled-components.

`components/Navbar.js
`

```javascript
import Link from 'next/link';
import styled from 'styled-components';
import { ButtonContainer } from './Button';

function Navbar() {

    return (
        <Container>

           <div className="navwrapper">
                <Link href="/">
                    <h2>Ankur's store</h2>
                </Link>
                <Link href={'/components/Cart'}>
                    <ButtonContainer>
                        My Cart 
                    </ButtonContainer>
                </Link>


            </div> 

    </Container>
    )
}

export default Navbar

const Container = styled.div`
    margin-bottom: 4rem;
    .navwrapper{
        display: flex;
        justify-content: space-between;
        padding: .5rem 1rem;
        background: rgb(100,56,90);
        height: 4rem;
        position: fixed;
        top: 0;
        width: 100%;
        z-index: 2;
    }
    a{
        text-decoration: none;
   }
    h2{
        text-transform: capitalize;
        color: white;
        font-family: 'Sansita Swashed', cursive;
        font-size: 2rem;
    }

```
The `ButtonContainer` component is a custom button component styled with styled-components. You will be re-using it inside other components.

You already imported it inside `Navbar.js`, go ahead and create it.

`components/Button.js`

```javascript
import styled from 'styled-components';

export const ButtonContainer = styled.button`
    height: 2.8rem;
    width: 6rem;
    font-size: 1.2rem;
    background: rgb(95, 56, 95);
    border: 1px solid;
    color: #fff;
    border-radius: 20px 4px;
    outline: none;
    &:hover{
        background: rgb(90,42,70);
    }

```
Go ahead and add the Navbar component to the index page.

`index.js`

```javascript
import Head from 'next/head'
import Navbar from './components/Navbar'
export default function Home() {
  return (
    <div>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Navbar />


    </div>
  )
}

```
This is how the Navbar looks.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bkas3dsdxenbaqow2kld.png)

---

## Creating the Home page

The Home page will display the products from your Medusa backend.

`components/ProductMenu.js
`

```javascript
import {useEffect, useState} from 'react'
import Product from './Product'
import { medusaClient } from '../medusaclient';

export default function Productmenu() {

    const [allProducts, setAllProducts]= useState([])


    useEffect(() => {
 const product = async () => {
            const response = await medusaClient.products.list();
            setAllProducts(response.products)
        }

        product()
    },[])


    return (
        <div
            className="change-display"
        >


                  {allProducts.map(product =>(


                    <Product key={product.id} product={product} />


                   ))}

        </div>
    )
}

```
`medusaClient.products.list()` retrieves all the products from the server. Now, create the Product page where the products will be listed.

`components/Product.js
`

```javascript
import React from 'react';
import Link from 'next/link'
import Image from 'next/image';
import styled from 'styled-components';

export default function Product({product}) {

    const {id,title, thumbnail, variants} = product;
 return (

            <ProductWrapper> 
                            <div className="display-flex">
                                <div style={{marginTop: '6rem'}}></div>
                            <div className="products">
                                    <div className="product-name">
                                        <h2>{title}</h2>
                                    </div>

                                <Link href={`/components/${id}`}>
                                    <div className="image-container">
                                        <Image src={thumbnail} alt={title} 
                                        width={200} height={200}/>
                                    </div>

                                </Link>

                                <div className="price-cart-container">

                                    <h3> $<strong>{variants[0]?.prices[1]?.amount}</strong></h3>

                                </div>
                            </div>
                            </div> 
            </ProductWrapper>

    )
}

const ProductWrapper = styled.div`

  display: flex;
  justify-content: center;
  padding-bottom: 2rem;

    .products{
        box-shadow: .5px 1px 2px 1.5px  grey;
        width: 21rem;
        transition: 1s all ease;
    }
    .image-container{
 transition: 1s all ease;
        img{
            width: 20rem;
            transition: 1s all ease;
            padding: .6rem;
        }
        text-align: center;
        &:hover img{
            transform: scale(1.1);
            /* padding: .5rem 0; */
        }
    }
  .product-name{
    background: rgb(214, 202, 202);
    text-align: center;
    font-family: 'Montserrat Alternates', sans-serif;
    height: 2.4rem;
  }
  .price-cart-container{
      display: flex;
      justify-content: space-between;

      h3{
          font-style: italic;
          font-size: 1.7rem;
      }
  }

`

```
Add the ProductMenu component to the `index.js` file.

`index.js`

```javascript
import Head from 'next/head'
import Navbar from './components/Navbar'
import Productmenu from './components/ProductMenu'

export default function Home() {
  return (
    <div>
     <Head>
        <title>My store</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Navbar />
      <Productmenu />

    </div>
  )
}

```
Our homepage looks like this


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7f98oykh1af5dyios33f.png)

When someone clicks on any product, you want it to take them to a details page where they can see the description and other information about the product.

Use dynamic routing to do this. Next.js dynamic routing allows you to add custom parameters to your URL and access individual data efficiently. You also want the detail page to be statically generated (generated at build time). Generating the page at build time will make the storefront performant.

`components/[detail].js
`

```javascript
import { useState, useEffect } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import styled from "styled-components";
import { medusaClient } from '../medusaclient';
import Navbar from './Navbar';

export async function getStaticPaths(){
    const res = await medusaClient.products.list()
    const paths = res.products.map(product => ({
        params: {detail: product.id.toString()}
    }))

    return {
        paths,
        fallback: false
    }
}

export async function getStaticProps({params}){
    const id = params.detail
    const res = await fetch(`http://localhost:9000/store/products/${id}`)
    const data = await res.json()

    return {
        props: {
            data
        },
        revalidate: 3600
    }
}

const Detail = ({data}) => {

    const {title, thumbnail, description, variants} = data.product

    return (
        <div>
            <Navbar />

        <DetailContainer className="container" >
                             <div className="company">
                                 <h2>{title}</h2>
                             </div>
                             <div className="image-container">
                                 <Image src={thumbnail} alt={title} 
                                        width={200} height={200}/>
                             </div>
                             <div className="price">
                                <h2><strong>Price: </strong>${variants[0]?.prices[1]?.amount}</h2>

                             </div>
                             <div className="description">
                                 <strong>Product description: </strong>
                                 <p className="product-info">
                                     {description}
                                 </p>
                             </div>
                             <div className="buttons">
                                <Link href="/">
                                    <button type="button" className="back-to-product">
                                        Back to products
                                    </button>
                                </Link> 
                                <button type="button" className="add-to-cart">

                                    add to cart
                                    </button>
                             </div>
                         </DetailContainer>
                    </div>
                     )
    }

export default Detail;

const DetailContainer = styled.div`
    margin-top: 8rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    div{
        text-align: center;
    }
    .company h2{
        text-transform: capitalize;
        font-family: cursive;
        font-size: 2.1rem;
        font-weight: 400;
        color: #474747;

    }
    .description{
            padding-top: 1.2rem;
            p{
                font-size: 1.2rem;
            }
        strong{
            font-size: 1.4rem;
            color: #524e4f
        }
    }
    .price{
        text-align: justify;
        strong{
            color: #524e4f
        }
    }
    .buttons{
        display: block;
        margin-top: 1.4rem;
    }
    .back-to-product{
        margin-right: 2rem;
        padding: .4rem .8rem;
        border-radius: 40px;
        text-transform: capitalize;
        background: transparent;
        font-family: sans-serif;
        font-size: 1.3rem;
        color: #048286;
        border: 2px solid #048286;
        outline: none;
        &:hover{
            background: #048286;
            color: white;
        }
    }
    .add-to-cart{
        margin-right: 2rem;
        padding: .4rem .8rem;
        border-radius: 40px;
        text-transform: capitalize;
        background: transparent;
        font-family: sans-serif;
        font-size: 1.3rem;
        color: #8b7b44;
        border: 2px solid #5f8604;
        outline: none;
        &:hover{
            background: #f3c52c;
            color: black;
        }
    }

`
```
Next, generate each static path as a standalone page at build time.

Click on any product to see its details.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tkmee1c4qk5zmga3i0xf.png)

---

## Building the Cart Functionality

To build the cart functionality, you will check if `cart_id` exists. If it exists, the selected item will be added to the cart using `medusaClient.carts.lineItems.create()`, but if it doesn’t, create a new cart and store the id inside `localStorage`.

Add a modal that pops up a dialogue whenever an item is added to the cart.

`components/[details].js
`

```javascript
import { useState, useEffect } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import styled from "styled-components";
import { medusaClient } from '../medusaclient';
import Navbar from './Navbar';

const addItem = async (cartId, product) => {
    const {cart} = await medusaClient.carts.lineItems.create(cartId, {
        variant_id: product.variants[0].id,
        quantity: 1,

    })
    localStorage.setItem('cartCount', cart.items.length)
    console.log(cart)

}

export async function getStaticPaths(){
    const res = await medusaClient.products.list()
    const paths = res.products.map(product => ({
        params: {detail: product.id.toString()}
    }))

    return {
        paths,
        fallback: false
    }
}

export async function getStaticProps({params}){
    const id = params.detail
    const res = await fetch(`http://localhost:9000/store/products/${id}`)
    const data = await res.json()

    return {
        props: {
            data
        },
        revalidate: 3600
    }
}

const Detail = ({data}) => {
    const [getRegionId, setGetRegionId] = useState("");
    const [modalState, setModalState] = useState(false) 
    const {title, thumbnail, description, variants} = data.product

    useEffect(() => {
        const region = async () => {
            const res = await medusaClient.regions.list();
            setGetRegionId(res.regions[1].id)
        }

        region()
    }, [])

    const addToCart = async () => {
        const cartId = localStorage.getItem('cart_id');
        setModalState(true)
        if(cartId) {
            addItem(cartId, data.product)
        }else{
            const {cart} = await medusaClient.carts.create({region_id: getRegionId})
            localStorage.setItem('cart_id', cart.id)
            addItem(cart.id, data.product)
            console.log(cart)
        }

    }
    return (
        <div>
            <Navbar />
            <div className="modal" style={{display: modalState ? 'block' : 'none'}}>
                <div className='modal-text'>
                    <h3>Item added to cart</h3>
                    <button onClick={() => setModalState(false)}>Ok</button>

                </div>
            </div>

        <DetailContainer className="container" >
                             <div className="company">
                                 <h2>{title}</h2>
                             </div>
                             <div className="image-container">
                                 <Image src={thumbnail} alt={title} 
                                        width={200} height={200}/>
                             </div>
                             <div className="price">
                                <h2><strong>Price: </strong>${variants[0]?.prices[1]?.amount}</h2>

                             </div>
                             <div className="description">
                                 <strong>Product description: </strong>
                                 <p className="product-info">
                                     {description}
                                 </p>
                             </div>
                             <div className="buttons">
                                <Link href="/">
                                    <button type="button" className="back-to-product">
                                        Back to products
                                    </button>
                                </Link> 
                                <button type="button" className="add-to-cart"
                                    onClick={addToCart}
                                    >

                                    add to cart
                                    </button>
                             </div>
                         </DetailContainer>
                    </div>
                     )
    }

export default Detail;

const DetailContainer = styled.div`
    margin-top: 8rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    div{
        text-align: center;
    }
    .company h2{
        text-transform: capitalize;
        font-family: cursive;
        font-size: 2.1rem;
        font-weight: 400;
        color: #474747;

    }
    .description{
            padding-top: 1.2rem;
            p{
                font-size: 1.2rem;
            }
        strong{
            font-size: 1.4rem;
            color: #524e4f
        }
    }
    .price{
        text-align: justify;
        strong{
            color: #524e4f
        }
    }
    .buttons{
        display: block;
        margin-top: 1.4rem;
    }
    .back-to-product{
        margin-right: 2rem;
        padding: .4rem .8rem;
        border-radius: 40px;
        text-transform: capitalize;
        background: transparent;
        font-family: sans-serif;
        font-size: 1.3rem;
        color: #048286;
        border: 2px solid #048286;
        outline: none;
        &:hover{
            background: #048286;
            color: white;
        }
    }
    .add-to-cart{
        margin-right: 2rem;
        padding: .4rem .8rem;
        border-radius: 40px;
        text-transform: capitalize;
        background: transparent;
        font-family: sans-serif;
        font-size: 1.3rem;
        color: #8b7b44;
        border: 2px solid #5f8604;
        outline: none;
        &:hover{
            background: #f3c52c;
            color: black;
        }
    }

`

```
The quantity was set to one for the purpose of this tutorial. However, the value of quantity should be dynamic - based on the customer’s desired quantity.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/87d3ehc5m7t51khsnxm9.png)

---

## Building the Cart Page

The cart page will show all the items that are in the cart. Retrieve the cart items by passing `cart_id` stored inside `localStorage` to `medusaClient.carts.retrieve()`.

`components/Cart.js
`

```javascript
import React, {useState, useEffect} from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { ButtonContainer } from './Button';
import { medusaClient } from '../medusaclient';

export default function Cart() {
    const [cart, setCart] = useState([])
    const id = localStorage.getItem("cart_id");


    useEffect(() =>{
        async function getCart(){

            if(id) {
            await medusaClient.carts.retrieve(id)
                .then(({cart}) => setCart(cart.items))

            }
        }
        getCart()

    },[])

    return(

        <div className="cart-flex">
            <h1>My Cart</h1>

            {cart.map(item => {
                console.log(item)
                const {title, thumbnail, subtotal, quantity, id, unit_price} = item
                return(
                    <div key={id} className="cart-div">

                        <p>{title}</p>
                        <Image src={thumbnail} alt={title} 
                        width={20} height={20}/>
                        <p> unit: {quantity}</p>
                        <p>price: ${unit_price}</p>
                        <p>subtotal: ${subtotal}</p>


                    </div>
                )
            })}
            <div className='return'>
                <Link href="/">
                    <ButtonContainer>Go home</ButtonContainer>

                </Link>
            </div>
        </div>
    )
}

```
Test out the cart. Add a few items to the cart and go to the cart page to view the items.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dskkpcy8r53js3ptp0fb.png)

---

## Conclusion

In this article, you have learned how to build a commerce application from scratch using Medusa and Next.js. Medusa JS Client was used to interact with the medusa server.

Medusa is a powerful modular commerce infrastructure for building performant and scalable ecommerce stores.

You can extend this project:

- [Add payment integration with Stripe](https://docs.medusajs.com/plugins/payment/stripe)
- [Add payment integration with Paystack](https://medusajs.com/blog/next-js-storefront-food-ecommerce-restaurant-medusa-paystack/)
- [Implement checkout workflow](https://docs.medusajs.com/modules/carts-and-checkout/storefront/implement-checkout-flow)

---


That was it for this blog. I hope you learned something new today.

If you did, please like/share so that it reaches others as well.

If you’re a regular reader, thank you, you’re a big part of the reason I’ve been able to share my life/career experiences with you.

Follow [Medusa](https://twitter.com/medusajs) on Twitter for the latest updates and If you found this blog post helpful or interesting, please consider giving Medusa project a star on [GitHub](https://github.com/medusajs/medusa).

If you would like to learn more about Medusa. Read my old blog posts here.

1- [Everything About Medusa - An Open-Source Alternative to Shopify](https://dev.to/tyaga001/everything-about-medusa-an-open-source-alternative-to-shopify-4ald)

2- [Shopify vs. Medusa](https://dev.to/tyaga001/shopify-vs-medusa-1gfm)

If you liked this article, you'll like my [2-1-1 Career Growth Newsletter](https://theankurtyagi.substack.com/) and my [Tweets](https://twitter.com/TheAnkurTyagi)