DEV Community

Cover image for An open source ecommerce platform for Nuxt.js
Carlos Padilla for Medusa

Posted on • Edited on • Originally published at medusajs.com

An open source ecommerce platform for Nuxt.js

Introduction

Medusa is an open source headless commerce engine in Node.js that allows you to build online stores through its API with just a few commands - link to repo. On the other hand, Nuxt.js is a front-end framework, built on top of Vue.js, that includes some features out-of-the-box such as server-side rendered sites, static sites, file system routing, data fetching, meta tags, SEO, and much more.

Through this guide, you will learn how to set up a starter storefront with Nuxt.js for the frontend part and link it with a Medusa server. To do that, first, you will create a Nuxt.js project and set up some simple components, pages and layout. Then, you will link the Nuxt.js project with the Medusa server to get some products from the API and display them on the home page, a product's page, and a product detail page.

You can find the final code at this GitHub repository.

If you have any problems underway in the setup, please reach out in the Medusa Discord.

Storefront's home page

Image description

Prerequisites

To follow along with this tutorial you need the following:

  • Node.js, it is recommended to have the latest LTS version installed.
  • One of the following package managers installed: npm, yarn, or npx (included by default with npm v5.2+).
  • A Medusa server is seeded with some dummy data to work with, so if this is not the case, please first read the QuickStart guide to set up a Medusa server and after that come back to continue.

Setting up the storefront

⚠️ This tutorial uses Nuxt v2.15.8 because at the time of writing this article there are two versions of Nuxt, but the newer one (Nuxt v3) is yet in beta state.

Install a Nuxt.js project

To install a Nuxt project, you can get started quickly with create-nuxt-app. Open your terminal and run the following command

// npx create-nuxt-app <project-name>
npx create-nuxt-app nuxtjs-storefront
Enter fullscreen mode Exit fullscreen mode

It will ask you some questions. You can choose the options that best fit your development workflow, but to keep this tutorial simple, I recommend installing the project using the following options:

Suggested configuration to install Nuxt.js project

Run Nuxt.js project

Once the Nuxt.js project is created, change to the directory of the storefront

cd nuxtjs-storefront
Enter fullscreen mode Exit fullscreen mode

And then run the following command

yarn dev
Enter fullscreen mode Exit fullscreen mode

This command will run the storefront app by default at http://localhost:3000. To test it, open your browser and go to http://localhost:3000. You will get something like this:

Default page for a new Nuxt.js installation

Later on, you will change the default port to learn how to integrate your frontend with the Medusa server in a port that is not the default.

Make the storefront layout

Before you go into connecting the Medusa server with the storefront, you need to add some components and pages to the storefront. Open the storefront’s project in your preferred IDE.

You should see the following directories:

Directory structure

You will be focusing mainly on the components and pages directories to design the layout for the storefront.

Components

Components are what make up the different parts of your page. They can be reused and imported into your pages, layouts, and even other components.

The storefront you are creating will have the following components:

  • Logo
  • Navbar
  • Footer
  • Product card

Go to the components directory and delete the default components that come with the Nuxt.js installation. Then add the following files

Logo → components/App/Logo.vue

<template>
  <div class="h-16 flex items-center">
    <div class="ml-4 flex lg:ml-0 lg:mr-8">
      <nuxt-link to="/">
        <img class="h-8 w-auto" src="https://i.imgur.com/y3yU55v.png" alt=""/>
      </nuxt-link>
    </div>
  </div>
</template>

<script>
export default {
  name: 'AppLogo'
}
</script>
Enter fullscreen mode Exit fullscreen mode

Navbar → components/App/Navbar.vue

<template>
  <div class="sticky top-0 z-20">
    <header class="relative bg-white">
      <nav class="px-4 sm:px-6 lg:px-8 border-b border-ui-medium flex items-center justify-between">
        <div class="flex items-center">
          <app-logo />
          <div class="hidden lg:flex lg:items-center">
            <div class="hidden flex-grow items-center justify-center lg:flex text-sm font-medium">
              <nuxt-link
                to="/"
                class="block mt-4 mr-4 lg:inline-block lg:mt-0 text-gray-700 hover:text-gray-600 last:mr-0"
              >
                Home
              </nuxt-link>
              <nuxt-link
                to="/products"
                class="block mt-4 mr-4 lg:inline-block lg:mt-0 text-gray-700 hover:text-gray-600 last:mr-0"
              >
                Products
              </nuxt-link>
            </div>
          </div>
        </div>

        <div class="flex items-center justify-end">
          <div class="hidden lg:flex">
            <div class="inline-block relative text-left">
              <div>
                <button
                  class="inline-flex justify-center w-full px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:text-gray-600"
                  type="button"
                >
                  USA / USD
                </button>
              </div>
            </div><div class="relative inline-block text-left">
              <div>
                <button
                  class="inline-flex justify-center w-full px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-600"
                  type="button"
                >
                  Account
                </button>
              </div>
            </div>
          </div><div class="relative inline-block text-left">
            <div>
              <button
                class="inline-flex items-center justify-center w-full py-2 bg-white text-sm font-medium hover:opacity-1/2"
                type="button"
              >
                <svg width="40" height="41" viewBox="0 0 40 41" fill="none" xmlns="http://www.w3.org/2000/svg">
                  <path
                    fill-rule="evenodd"
                    clip-rule="evenodd"
                    d="M14.9968 16.2273C14.9921 16.1189 14.9888 16.0004 14.9877 15.8734C14.9826 15.2497 15.0333 14.4053 15.2648 13.551C15.4962 12.6975 15.9164 11.8043 16.6719 11.123C17.4366 10.4333 18.5016 10 19.9419 10C21.3822 10 22.4472 10.4333 23.212 11.123C23.9674 11.8043 24.3877 12.6975 24.619 13.551C24.8506 14.4053 24.9012 15.2497 24.8961 15.8734C24.8951 16.0004 24.8917 16.1189 24.887 16.2273H27.8836C29.0776 16.2273 30.0056 17.2667 29.8708 18.4531L28.7344 28.4531C28.6196 29.4638 27.7644 30.2273 26.7472 30.2273H13.1366C12.1194 30.2273 11.2643 29.4638 11.1494 28.4531L10.013 18.4531C9.87822 17.2667 10.8062 16.2273 12.0002 16.2273H14.9968ZM23.8859 16.2273C23.8912 16.1186 23.8951 15.9971 23.8962 15.8652C23.9008 15.2957 23.8535 14.5493 23.6538 13.8126C23.454 13.0752 23.1098 12.3775 22.5422 11.8656C21.984 11.3622 21.1673 11 19.9419 11C18.7165 11 17.8999 11.3622 17.3416 11.8656C16.774 12.3775 16.4299 13.0752 16.23 13.8126C16.0303 14.5493 15.983 15.2957 15.9877 15.8652C15.9888 15.9971 15.9926 16.1186 15.9979 16.2273H23.8859ZM12.0002 17.2273H27.8836C28.4806 17.2273 28.9446 17.747 28.8772 18.3402L27.7408 28.3402C27.6834 28.8455 27.2558 29.2273 26.7472 29.2273H13.1366C12.628 29.2273 12.2004 28.8455 12.143 28.3402L11.0066 18.3402C10.9392 17.747 11.4032 17.2273 12.0002 17.2273ZM15.4874 20.0455C15.8388 20.0455 16.1237 19.7605 16.1237 19.4091C16.1237 19.0576 15.8388 18.7727 15.4874 18.7727C15.1359 18.7727 14.851 19.0576 14.851 19.4091C14.851 19.7605 15.1359 20.0455 15.4874 20.0455ZM25.0328 19.4091C25.0328 19.7605 24.7479 20.0455 24.3965 20.0455C24.045 20.0455 23.7601 19.7605 23.7601 19.4091C23.7601 19.0576 24.045 18.7727 24.3965 18.7727C24.7479 18.7727 25.0328 19.0576 25.0328 19.4091Z"
                    fill="black"
                  /></svg>
                <span>0</span>
              </button>
            </div>
          </div>
        </div>
      </nav>
    </header>
  </div>
</template>

<script>
export default {
  name: 'NavBar'
}
</script>
Enter fullscreen mode Exit fullscreen mode

Footer → components/App/Footer.vue

<template>
  <footer>
    <div class="bg-white px-4 pt-24 pb-4 sm:px-6 lg:px-8 border-t border-ui-medium flex items-center justify-between text-sm">
      <div class="flex items-center">
        <a class="mr-3 last:mr-0 text-ui-dark hover:text-gray-700" href="/">Create return</a>
        <a class="mr-3 last:mr-0 text-ui-dark hover:text-gray-700" href="/">FAQ</a>
        <a class="mr-3 last:mr-0 text-ui-dark hover:text-gray-700" href="/">Terms &amp; Conditions</a>
      </div>
      <div class="flex items-center">
        <a href="https://www.github.com/medusajs" class="mr-3 last:mr-0 text-ui-dark hover:text-gray-700">GitHub</a>
        <a href="https://www.twitter.com/medusajs" class="mr-3 last:mr-0 text-ui-dark hover:text-gray-700">Twitter</a>
        <a href="https://discord.gg/ruGn9fmv9q" class="mr-3 last:mr-0 text-ui-dark hover:text-gray-700">Discord</a>
      </div>
    </div>
  </footer>
</template>

<script>
export default {
  name: 'AppFooter'
}
</script>
Enter fullscreen mode Exit fullscreen mode

ProductCard → components/ProductCard.vue

<template>
  <div>
    <nuxt-link :to="`/products/${item.id}`">
      <div
        class="group relative"
      >
        <div class="w-full min-h-auto bg-gray-200 aspect-w-1 aspect-h-1 rounded-md overflow-hidden group-hover:opacity-75 lg:h-80 lg:aspect-none">
          <div class="w-auto h-full object-center object-cover bg-gray-100">
            <img
              alt=""
              :src="item.thumbnail"
            >
          </div>
        </div>
        <div class="mt-4 flex justify-between">
          <h3 class="text-sm text-gray-700 font-normal">
            {{ item.title }}
          </h3>
          <p class="text-sm font-semibold text-gray-900">
            from {{ lowestPrice.amount/100 }} {{ lowestPrice.currency_code.toUpperCase() }}
          </p>
        </div>
      </div>
    </nuxt-link>
  </div>
</template>

<script>
export default {
  name: 'ProductCard',
  props: {
    item: {
      type: Object,
      default () {
        return {
          id: 1,
          title: 'Kitchen Table',
          thumbnail: 'https://picsum.photos/600/600',
          variants: [{ prices: [{ amount: 0 }] }]
        }
      }
    }
  },
  computed: {
    lowestPrice () {
      const lowestPrice = this.item.variants.reduce((acc, curr) => {
        return curr.prices.reduce((lowest, current) => {
          if (lowest.amount > current.amount) {
            return current
          }
          return lowest
        })
      }, { amount: 0 })

      return lowestPrice || { amount: 10, currency_code: 'usd' }
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Pay extra attention to the Logo, Navbar, and Footer components. They need to be inside a folder called App.

Pages

The pages directory contains your storefront views and routes. For this tutorial you will need only 3 pages:

  • Home page
  • Products page
  • Product detail page

On the pages directories, open the index.vue file and replace the code that is already there with this one

Index → /pages/index.vue

    <template>
      <div>
        <div class="bg-ui-light pb-12 lg:pb-0 w-full px-4 sm:px-6 lg:px-12">
          <div class="flex flex-col lg:flex-row items-center max-w-screen-2xl mx-auto">
            <div class="w-auto h-full object-center object-cover p-12">
              <img
                width="600"
                alt=""
                src="https://start.medusajs.com/static/9803c162c71fd1960d9d11253859c701/246b5/hero-merch.webp"
              >
            </div>
            <div>
              <h1 class="text-4xl">
                CLAIM YOUR MERCH
              </h1>
              <p class="mt-2 text-lg font-normal">
                Contribute to Medusa and receive free merch<br>as a token of our appreciation
              </p>
              <button class="btn-ui mt-4 min-w-full lg:min-w-0">
                Learn more
              </button>
            </div>
          </div>
        </div>

        <div
          v-if="products.length"
          class="container mx-auto px-8 py-16"
        >
          <div class="flex items-center justify-between mb-6">
            <p class="text-2xl font-semibold text-gray-700">
              Featured
            </p>
            <nuxt-link
              class="text-ui-dark flex items-center"
              to="/products"
            >
              <span class="mr-2 text-ui-dark">Browse all products</span>
              <svg
                width="16"
                height="8"
                viewBox="0 0 16 8"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path d="M15.3536 4.35355C15.5488 4.15829 15.5488 3.84171 15.3536 3.64645L12.1716 0.464466C11.9763 0.269204 11.6597 0.269204 11.4645 0.464466C11.2692 0.659728 11.2692 0.976311 11.4645 1.17157L14.2929 4L11.4645 6.82843C11.2692 7.02369 11.2692 7.34027 11.4645 7.53553C11.6597 7.7308 11.9763 7.7308 12.1716 7.53553L15.3536 4.35355ZM0 4.5H15V3.5H0V4.5Z" fill="#89959C" />
              </svg>
            </nuxt-link>
          </div>
          <div class="grid grid-cols-4 gap-8">
            <ProductCard
              v-for="product in products"
              :key="product.id"
              :item="product"
            />
          </div>
        </div>
      </div>
    </template>

    <script>
    export default {
      name: 'IndexPage',
        data () {
        return {
          products: [{
            id: 1,
            title: 'Kitchen Table',
            thumbnail: 'https://picsum.photos/600/600',
            variants: [{ prices: [{ amount: 0, currency_code: 'usd' }] }]
          }]
        }
      },
    }
    </script>

    <style>
      .btn-ui {
        @apply py-2 px-4 bg-ui-dark text-white text-sm font-medium rounded-md shadow;
        @apply focus:outline-none focus:ring-2 focus:ring-ui-dark focus:ring-opacity-75 disabled:bg-ui-medium;
      }
    </style>
Enter fullscreen mode Exit fullscreen mode

This page will be the home for your storefront. It is composed of a hero header and a grid configured to show only four products. The only thing to do here once you connect the storefront to the Medusa server will be to put the ProductCard component in a v-for loop to display the products.

Now, you need to create a new directory called products which will hold inside the products page /pages/products/index.vue and the product detail page /pages/products/_id.vue. Add the following code to these pages.

Products page → /pages/products/index.vue

<template>
  <div class="container mx-auto p-8">
    <div class="w-full border-b border-ui-medium pb-6 mb-2 lg:mb-6 flex items-center justify-between">
      <h1 class="font-semibold text-3xl">
        All Products
      </h1>
    </div>

    <div
      v-if="products.length"
      class="grid grid-cols-4 gap-8 "
    >
      <ProductCard
        v-for="product in products"
        :key="product.id"
        :item="product"
      />
    </div>
  </div>
</template>

<script>
export default {
  name: 'ProductsIndex',
    data () {
    return {
      products: [{
        id: 1,
        title: 'Kitchen Table',
        thumbnail: 'https://picsum.photos/600/600',
        variants: [{ prices: [{ amount: 0, currency_code: 'usd' }] }]
      }]
    }
  },
}
</script>
Enter fullscreen mode Exit fullscreen mode

This page is similar to the home page but without the hero header. Here you will show a grid with all the products sent by the Medusa server.

Product detail page → /pages/products/_id.vue

<template>
  <div class="container mx-auto p-8">
    <div class="flex flex-col lg:flex-row">
      <div class="lg:w-3/5 lg:pr-14">
        <div class="flex">
          <div class="hidden lg:flex flex-col items-center mr-4">
            <div class="w-auto h-full object-center object-cover px-4 space-y-4">
              <img
                v-for="image in product.images"
                :key="image.id"
                width="150"
                alt=""
                :src="image.url"
                class="cursor-pointer"
                @click="imageToShow = image.id"
              >
            </div>
          </div>

          <div class="h-auto w-full flex-1 flex flex-col rounded-lg overflow-hidden">
            <div class="w-auto h-full">
              <div
                v-for="image in product.images"
                :key="image.id"
              >
                <div v-if="image.id === imageToShow">
                  <img
                    alt=""
                    :src="image.url"
                    class=" w-full"
                  >
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>

      <div class="mt-8 lg:mt-0 lg:w-2/5 lg:max-w-xl">
        <h1 class="font-semibold text-3xl">
          {{ product.title }}
        </h1>
        <p v-if="product.variants" class="text-lg mt-2 mb-4">
          {{ product.variants[0].prices[0].amount/100 }} {{ product.variants[0].prices[0].currency_code }}
        </p>
        <p v-else>
          10 USD
        </p>
        <p class="font-light">
          {{ product.description }}
        </p>
        <div v-for="option in options" :key="option.id" class="mt-6">
          <div class="text-sm">
            <p class="font-medium mb-2">
              {{ option.title }}
            </p>
            <div>
              <button
                v-for="value in option.values"
                :key="value.id"
                class="bg-ui-dark text-white inline-flex items-center justify-center rounded-sm text-xs h-12 w-12 mr-2 last:mr-0 hover:bg-ui-dark hover:text-white"
              >
                {{ value.value }}
              </button>
            </div>
          </div>
        </div>
        <div class="inline-flex mt-12">
          <button class="btn-ui mr-2 px-12">
            Add to bag
          </button>
          <div class="flex items-center rounded-md px-4 py-2 shadow">
            <button></button>
            <span class="w-8 text-center">1</span>
            <button>+</button>
          </div>
        </div>
        <div class="mt-12">
          <div class="border-t last:border-b border-ui-medium py-6">
            <h3 class="-my-3 flow-root">
              <button
                class="py-3 bg-white w-full flex items-center justify-between text-sm text-gray-400 hover:text-gray-500"
                type="button"
                @click="showDetails = !showDetails"
              >
                <span class="font-medium text-gray-900">Details</span>
                <span class="ml-6 flex items-center">
                  <span></span>
                </span>
              </button>
            </h3>
            <div v-if="showDetails" class="pt-6">
              <div class="space-y-4 text-ui-dark text-sm">
                <ul class="list-inside list-disc space-y-2">
                  <li>Weight: {{ product.weight ? `${product.weight} g` : 'Unknown' }}</li>
                  <li>Width: {{ product.width ? `${product.width} cm` : 'Unknown' }}</li>
                  <li>Height: {{ product.height ? `${product.height} cm` : 'Unknown' }}</li>
                </ul>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ProductDetail',
  data () {
    return {
      showDetails: false,
      imageToShow: 'default_image',
      product: {
        id: 1,
        title: 'Medusa Coffee Mug',
        description: 'Every programmer\'s best friend.',
        thumbnail: '',
        variants: [{ prices: [{ amount: 0, currency_code: 'usd' }] }],
        images: [
          { id: 'default_image', url: 'https://picsum.photos/600/400' },
          { id: 'another_image', url: 'https://picsum.photos/600/400?id=100' }
        ]
      }
    }
  },
  computed: {
    lowestPrice () {
      const lowestPrice = this.product.variants.reduce((acc, curr) => {
        return curr.prices.reduce((lowest, current) => {
          if (lowest.amount > current.amount) {
            return current
          }
          return lowest
        })
      }, { amount: 0 })

      return lowestPrice || { amount: 10, currency_code: 'usd' }
    },
    options () {
      if (this.product.options) {
        return this.product.options.map((option) => {
          option.values = option.values.reduce((acc, curr) => {
            if (!acc.find(val => val.value === curr.value)) {
              return [...acc, { ...curr }]
            }
            return acc
          }, [])

          return option
        })
      }
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

On this page, you will display all the information related to a specific product. For example, sizes, images, price, description, variants, etc...

⚠️ The three pages include a product object on the data function to showcase the design while there isn’t a server to send requests. Once you connect the storefront with the Medusa server, the data coming from the server will replace the data in the product object.

Layouts

Layouts are a great help when you want to have a basic structure for your Nuxt app. For example, to include a navbar and footer that will be shown on all the pages of the app. By default, a Nuxt project doesn't come with layouts, but it is easy to add them to your project.

To have a default layout on your storefront, create a layouts directory in the root of the project, and inside it, add a new file called default.vue with the following code:

<template>
  <div class="min-h-screen flex flex-col">
    <app-navbar />

    <main class="flex-1">
      <Nuxt />
    </main>

    <app-footer />
  </div>
</template>

<script>
export default {
  name: 'DefaultLayout'
}
</script>
Enter fullscreen mode Exit fullscreen mode

Because the layout file was named default.vue, the layout will automatically be applied to all the pages on the storefront.

Styling

Replace the content of windi.config.ts in the root of your Nuxt.js project with the following:

import { defineConfig } from '@windicss/plugin-utils'

export default defineConfig({
  /**
   * Write windi classes in html attributes.
   * @see https://windicss.org/features/attributify.html
   */
  attributify: true,
  theme: {
    extend: {
      fontSize: {
        '2xs': '0.5rem'
      },
      maxWidth: {
        '1/4': '25%',
        '1/2': '50%',
        '3/4': '75%'
      },
      maxHeight: {
        review: 'calc(100vh - 10rem)'
      },
      boxShadow: {
        DEFAULT:
          '0 2px 5px 0 rgba(60, 66, 87, 0.08), 0 0 0 1px rgba(60, 66, 87, 0.16), 0 1px 1px rgba(0, 0, 0, 0.12)',
        error:
          '0 2px 5px 0 rgba(255, 155, 155, 0.08), 0 0 0 1px rgba(255, 155, 155, 0.70), 0 1px 1px rgba(0, 0, 0, 0.12)'
      },
      colors: {
        green: {
          DEFAULT: '#56FBB1'
        },
        blue: {
          DEFAULT: '#0A3149'
        },
        ui: {
          light: '#F7F7FA',
          DEFAULT: '#EEF0F5',
          medium: '#D9DFE8',
          dark: '#89959C'
        }
      }
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

Change the Default Port

Now, you will change the port where the storefront app runs by default (port 3000). To do that, open the nuxt.config.js file and add the following right after the ssr property

server: {
  port: 3333
},
Enter fullscreen mode Exit fullscreen mode

After that, run the following command to see in the browser what you have achieved with the components, pages, and layout that you just set up until this part of the tutorial.

yarn dev
Enter fullscreen mode Exit fullscreen mode

Open your browser and go to the URL localhost:3000. You should see something like this:

Storefront flow

The storefront just shows static data for now. You will link the storefront with the Medusa server in the next section.

Link Medusa server with storefront

To link the server with the storefront, first, open up your Medusa project in your IDE, then open the .env file where all your environment variables are set up.

Add the variable STORE_CORS with the value of the URL where your storefront will be running. Remember that you changed the default port on the storefront, therefore the URL is http://localhost:3333.

STORE_CORS=http://localhost:3333
Enter fullscreen mode Exit fullscreen mode

After this, your Medusa server will be ready to receive a request from your storefront and send back responses if everything works as expected.

⚠️ The medusa server is running by default in port 9000, so the URL to use on the storefront to send requests is http://localhost:9000.

Testing connection with Medusa server

To be able to list the products on the home page you need to test if you can send requests from your storefront to the Medusa server and receive some data to show on the front-end.

⚠️ Be sure the @nuxtjs/axios module is installed in your storefront project. If it’s not, then you need to install it following these instructions.

Once the project has the axios module, you need to change the base URL for the axios module that you’ll use to make the requests to the server.

Open the nuxt.config.js file and look for the axios property. Change the baseURL property to match the URL where the medusa server will be running:

axios: {
  baseURL: 'http://localhost:9000/'
},
Enter fullscreen mode Exit fullscreen mode

With this change, you don’t have to type the full URL each time you need to make an HTTP request to the Medusa server. So, instead of this:

$axios.$get('http://localhost:9000/store/products')
Enter fullscreen mode Exit fullscreen mode

You will do this:

$axios.$get('/store/products')
Enter fullscreen mode Exit fullscreen mode

If the server URL changes in the future, you only need to come back to one place and update that just one time, and everything will be working fine.

To fetch data from the API, this tutorial uses the fetch function that Nuxt.js offers as part of the core.

Open the file /pages/index.vue and add the fetch function in the script section:

async fetch () {
    try {
      const { products } = await this.$axios.$get('/store/products')
            console.log(products)
      this.products = products
    } catch (e) {
      // eslint-disable-next-line no-console
      console.log('The server is not responding')
    }
  }
Enter fullscreen mode Exit fullscreen mode

This function receives just one parameter $axios which is a service that allows making an HTTP request to the Medusa server. So, inside the function, a request is sent to the endpoint /store/products to obtain the list of products from the Medusa server. Then, the list of products is returned.

To test this out, run the following command in the terminal to start the medusa server:

medusa develop
Enter fullscreen mode Exit fullscreen mode

And start the storefront server:

yarn dev
Enter fullscreen mode Exit fullscreen mode

Open your browser and go to the URL localhost:3000. Then, open the Web Developer Tools.

If you find something like this in the console tab then your connection to the Medusa server is working. Otherwise, check that you follow all the steps and are not missing anything.

Results got from Medusa server

🧠 Remember, the URL where your storefront app is running has to be the same that was set up on STORE_CORS variable on the medusa server.

Display Products on the Home Page

Now is time to render the products result returned from the Medusa server on the storefront.

In the same file /pages/index.vue, update the fetch function to the following,

async fetch () {
    try {
        const { products } = await this.$axios.$get('/store/products')
        this.products = products.splice(0, 4)
    } catch(e) {
        // eslint-disable-next-line no-console
        console.log('The server is not responding')
  }
}
Enter fullscreen mode Exit fullscreen mode

With this update, the data coming back from the server replaces the products array with only four products to display on the homepage.

The v-for applied on the ProductCard iterates the products array and passes to the component, as a prop, a product with all the properties specified on the Medusa API for that endpoint.

If you check the storefront on the browser it should look something like this:

Products displayed on home page

Display Products on The Products Page

In the navigation bar, there is a “Products” link. If you click on it, you will be redirected to the products page, but there will be only one static product. Let’s fix that to display all the products in your Medusa server on the page.

Open the /pages/products/index.vue file, go to the script section and add the following fetch function

async fetch () {
  try {
    const { products } = await this.$axios.$get('/store/products')
    this.products = products
  } catch (e) {
    // eslint-disable-next-line no-console
    console.log('The server is not responding')
  }
}
Enter fullscreen mode Exit fullscreen mode

Check the products page in your browser and you should get something like this:

Products page

Display Product Details

The last page to update is the product detail page. If you click on any product on the home page or the products page, it will take you to the product’s details page, but you won’t see any details at all. To fix it, you need to request a specific product to the Medusa server so you can get all the product info.

Open the file /pages/products/_id.vue and add the following fetch function

aasync fetch () {
  try {
    const { product } = await this.$axios.$get(`/store/products/${this.$route.params.id}`)
    this.product = product
    this.imageToShow = this.product.images[0].id
  } catch (e) {
    // eslint-disable-next-line no-console
    console.log('The server is not responding')
  }
},
Enter fullscreen mode Exit fullscreen mode

If you go again to your browser and click on any product, you will be taken to the product details page like before, but this time you’ll see all details rendered on the page.

Product detail page

Conclusion

As you learned in this tutorial it is very easy to make a storefront from zero with Nuxt.js and integrate it with your Medusa server.

The next steps for you would be to check the Medusa API to learn about all the different requests that you can call from your storefront to turn your Nuxt.js storefront into a full-fledged online store.

For example, you can implement the Cart functionality. The process would involve making the pages or components on the Nuxt.js app and then making the respective requests to the Medusa server to get the data to render on the storefront.

If you have any issues or questions related to Medusa, then feel free to reach out to the Medusa team via Discord.

Top comments (0)