How I Created a Comic Book Store with Medusa, Gatsby, PayPal, and MeiliSearch
Tags: javascript, webdev, programming, opensource
Published: 2022-05-24 09:59:52

[Medusa](https://medusajs.com) is an open source headless commerce platform targeted toward developers. It can be used to build fully-fledged online stores. It has a lot of [essential ecommerce features](https://github.com/medusajs/medusa#features) including automated RMA flows, plug-and-play integrations, product and order management, and much more.

In this tutorial, you’ll learn how to create a comic book store with Medusa. You’ll also be adding important ecommerce features to your store including a search engine using MeiliSearch and a payment provider using PayPal.

You can find the full code for this tutorial on [this GitHub repository](https://github.com/shahednasser/medusa-comic-store).

![Comic Book Store](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c7sfxf71c9hevlmh43o9.gif)

## Architecture Overview

Before getting started with the tutorial, here’s a short overview of Medusa’s architecture in case you’re not familiar with it. You can go ahead and skip to the next section if you are.

Medusa is made up of 3 primary components:

1. The **Headless Server** is the core of your ecommerce store. It takes care of handling all logic, ecommerce features, and data. All other components connect to the server using [REST APIs](https://docs.medusajs.com/api/store/).
2. The **Medusa Admin** is the user interface that store operators can use to view and manage their store’s data (for example, products and orders). Medusa provides an intuitive ready-made Admin panel that you can use. Alternatively, you can build your own and connect to the server using the REST APIs.
3. The **Storefront** is the online store where customers view products and make purchases. Medusa provides two starter storefronts, one built with [Next.js](https://docs.medusajs.com/starters/nextjs-medusa-starter) and one with [Gatsby](https://docs.medusajs.com/starters/gatsby-medusa-starter). You can also build a storefront with any framework of your choice and connect to the server using REST APIs.

In this tutorial, you’ll learn about setting up each and how to use them.

## Prerequisites

Before you start you’ll need the following requirements installed:

1. [Node](https://nodejs.org/en/) v14 or higher.
2. [Postgres](https://www.postgresql.org/) with an empty database created.
3. [MeiliSearch](https://docs.meilisearch.com/learn/getting_started/quick_start.html) for the search engine.
4. A [PayPal](https://www.paypal.com) developer account.
5. [MinIO](https://docs.min.io/minio/baremetal/) for file storage. You can alternatively use [S3](https://docs.medusajs.com/add-plugins/s3) or [DigitalOcean Spaces](https://docs.medusajs.com/add-plugins/spaces).

## Install Server

To install the Medusa server, you need to first install the Medusa CLI:

```bash
npm install -g @medusajs/medusa-cli
```

Then, run the following command to install the Medusa server in a new directory `comic-store`:

```bash
medusa new comic-store
```

### Install Plugins

The next step is to install the plugins you’ll be using on your Medusa server. For this tutorial, you need the plugins for the PayPal, MeiliSearch, and MinIO integrations.

Run the following command inside the `comic-store` directory to install the 3 plugins:

```bash
npm install medusa-file-minio medusa-plugin-meilisearch medusa-payment-paypal
```

> Replace `medusa-file-minio` with the file service you’re using if it’s not MinIO.
> 

Make sure in `package.json` that the versions for `@medusajs/medusa`, `medusa-interfaces` and `@medusajs/medusa-cli` are greater than or equal to `1.3.0`. If not, update them with the following command:

```bash
npm install @medusajs/medusa@latest medusa-interfaces@latest @medusajs/medusa-cli@latest
```

### Add Environment Variables

Medusa gives you the freedom to handle your environment variables based on your server. In this tutorial, you’ll be adding all environment variables in a `.env` variable.

Open the `.env` file. Add the following variables:

```bash
#PostgreSQL Database URL
DATABASE_URL=

#MinIO configurations
MINIO_ACCESS_KEY=
MINIO_SECRET_KEY=
MINIO_BUCKET=
MINIO_SERVER=

#PayPal Configurations
PAYPAL_SANDBOX=true
PAYPAL_CLIENT_ID=
PAYPAL_CLIENT_SECRET=
PAYPAL_AUTH_WEBHOOK_ID=

#MeiliSearch Configurations
MEILISEARCH_HOST=http://127.0.0.1:7700
MEILISEARCH_API_KEY=
```

These environment variables are important for configurations related to the database, MinIO, PayPal, and MeiliSearch.

`DATABASE_URL` is the URL to connect to your PostgreSQL database schema. It should be of the format `postgres://<USERNAME>:<PASSWORD>@<HOST>/<DB_NAME>`.

You can refer to our documentation to learn how to retrieve the necessary variables for [MinIO](https://docs.medusajs.com/add-plugins/minio) and [MeiliSearch](https://docs.medusajs.com/add-plugins/meilisearch).

> If you’re using a different file service than MinIO please refer to the documentation of your file service to learn what variables you need.
> 

For PayPal, you can refer to [PayPal’s documentation](https://developer.paypal.com/api/rest/) to retrieve the Client ID, Client Secret, and Webhook ID. You also turned sandbox mode on for testing by setting `PAYPAL_SANDBOX` to `true`.

### Configure Server

You need to pass these environment variables to the server configurations.

Server configurations are all in `medusa-config.js`. This includes database, plugins, and more configurations.

 Open `medusa-config.js`. Add the following to the beginning of the file:

```jsx
const dotenv = require('dotenv');
let ENV_FILE_NAME = '';
switch (process.env.NODE_ENV) {
	case 'prod':
		ENV_FILE_NAME = '.env';
		break;
	case 'test':
		ENV_FILE_NAME = '.env.test';
		break;
	default:
		ENV_FILE_NAME = '.env';
		break;
}

dotenv.config({ path: process.cwd() + '/' + ENV_FILE_NAME });
```

This allows you to load environment variables from a `.env` file.

Next, in the `plugins` array, add the following 3 plugins at the end of the array:

```jsx
const plugins = [
  //...
  {
    resolve: `medusa-payment-paypal`,
    options: {
      sandbox: process.env.PAYPAL_SANDBOX,
      client_id: process.env.PAYPAL_CLIENT_ID,
      client_secret: process.env.PAYPAL_CLIENT_SECRET,
      auth_webhook_id: process.env.PAYPAL_AUTH_WEBHOOK_ID
    }
  },
  {
    resolve: `medusa-file-minio`,
    options: {
        endpoint: process.env.MINIO_SERVER,
        bucket: process.env.MINIO_BUCKET,
        access_key_id: process.env.MINIO_ACCESS_KEY,
        secret_access_key: process.env.MINIO_SECRET_KEY,
    }
  },
  {
    resolve: `medusa-plugin-meilisearch`,
    options: {
      config: {
        host: process.env.MEILISEARCH_HOST,
        apiKey: process.env.MEILISEARCH_API_KEY
      },
      settings: {
        products: {
          searchableAttributes: ["title", "description", "variant_sku"],
          displayedAttributes: ["title", "description", "variant_sku"],
        },
      },
    },
  }
];
```

This loads the 3 plugins you installed earlier and passes the necessary options for each.

Finally, change the database configurations in `projectConfig` in the exported function to use your PostgreSQL database instead of an SQLite database:

```jsx
module.exports = {
  projectConfig: {
    //...
    database_url: DATABASE_URL,
    database_type: "postgres",
		//**comment out or remove these lines:**
    // database_database: "./medusa-db.sql",
    // database_type: "sqlite",
  },
	//...
};
```

### Migrate and Seed Database

The final step before running your server is migrating and seeding your database. Migration means adding the necessary tables into your database schema to make it work with Medusa.

Seeding means adding dummy data into your database to quickly get started.

Run the following command to migrate and seed your database:

```bash
npm run seed
```

This will connect to your database using the URL you passed to the environment variable `DATABASE_URL`. Make sure that you already created the database before running this command.

### Run the Server

Make sure that the MeiliSearch and MinIO services are running. Then, run your server with the following command:

```bash
npm start
```

This will run your server on the port `9000`. You should keep the server running for the entire tutorial, as the Medusa admin and storefront depend on the server.

## Setup Medusa Admin

In this section, you’ll install the [Medusa Admin](https://docs.medusajs.com/admin/quickstart), add products to it, and enable PayPal as a payment provider.

### Install Admin

In your terminal and in a different directory than the `comic-store` directory, run the following command:

```bash
git clone https://github.com/medusajs/admin comic-admin
```

Then, change to the newly created `comic-admin` directory and install the necessary dependencies:

```bash
cd comic-admin
npm install
```

Make sure the Medusa server is still running. Then, run the following command to start the admin:

```bash
npm start
```

This will start your Medusa admin on the port `7000` by default. Open it in your browser and you should see a login screen.

![Admin Login](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ntmwk9naznm5cjkibepn.png)

You can use the default email “admin@medusa-test.com” and password “supersecret” to log in.

### Add Products

After you log in, choose from the sidebar “Products”. You’ll see a few products that were added when you seeded your database.

![Products](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q19k2jycxdek7f2kzhqu.png)

Go ahead and delete those by clicking on the 3 dots for each one then Delete.

Next, add products to your comic book store by clicking the “New Product” button at the top right.

You need to fill out the fields related to the product info.

![Product Form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v3dmj8s5cvbrh877ncmu.png)

Add as many products as you want before moving on to the next step.

![New Products](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dab0r1jznq7edr97bkkn.png)

### Enable PayPal

To enable PayPal as a payment provider, click on Settings, then choose Regions.

For each region you want to add PayPal as a payment provider, click on the “Payment Providers” input and choose “paypal”, then click Save.

![Enable PayPal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zk508ln26itnqi65i45l.png)

## Setup the Storefront

The last step is to set up the storefront. This section covers installing the [Gatsby storefront](https://docs.medusajs.com/starters/gatsby-medusa-starter), making some customizations to it, adding MeiliSearch bar, and adding the UI for PayPal.

### Install Storefront

In your terminal and in a different directory than the `comic-store` and `comic-admin` directories, run the following command:

```bash
gatsby new comic-storefront https://github.com/medusajs/gatsby-starter-medusa
```

This will install the Gatsby storefront in a new directory `comic-storefront`.

Then, change to the `comic-storefront` directory and rename `.env.template` to `.env.development`:

```bash
mv .env.template .env.development
```

### Add Environment Variables

You need to add environment variables to use MeiliSearch and PayPal on your storefront. In `.env.development` add the following variables:

```bash
#MeiliSearch Configurations
GATSBY_MEILISEARCH_HOST=
GATSBY_MEILISEARCH_API_KEY=

#PayPal Configurations
GATSBY_PAYPAL_CLIENT_ID=
```

The values for these configurations are the same as those you used on your server.

### Run Gatsby Storefront

Make sure that the Medusa server is running. Then run the following command to start the Gatsby storefront:

```bash
npm start
```

This will run your storefront on `localhost:8000`. Open it in your browser. You should see a hero image and the products you added.

![Storefront](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qhvt2ugmuwags2asapav.png)

### Customize Storefront

The hero banner is a static one that is added to the code. You’ll now customize it to show something related to your comic book store.

Open `src/pages/index.js`. You should find in the returned JSX the component `StaticImage` followed by a div. Change them to the following:

```jsx
<StaticImage
  src="../images/hero.png"
  alt="A black Medusa hoodie and a white Medusa coffee mug"
  placeholder="tracedSVG"
  className="w-full lg:w-1/2 h-auto lg:my-5"
/>
<div className="lg:ml-7">
  <h1 className="text-4xl">The Best Comic Books</h1>
  <p className="mt-2 text-lg font-normal">
    Buy the best Marvel and DC Comic Books!
  </p>
</div>
```

This changes the text and image used. You can download the new image from [here](https://www.kindpng.com/imgv/wbhoox_transparent-comic-book-pow-png-comic-clipart-png/). Place it at `src/images` with the name `hero.png`.

If you open your storefront now you should see the hero image updated.

![Updated Hero](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/54432hmtyratxjp5yg44.png)

### Add Search Bar

In this section, you’ll add a search bar to search products using MeiliSearch.

In your terminal, run the following command to install some necessary dependencies:

```bash
npm install react-instantsearch-dom @meilisearch/instant-meilisearch
```

Then, create the file `src/components/header/search.jsx` with the following content:

```jsx
import {
  Highlight,
  Hits,
  InstantSearch,
  SearchBox,
  connectStateResults
} from "react-instantsearch-dom"

import React from "react"
import { instantMeiliSearch } from "@meilisearch/instant-meilisearch"

const searchClient = instantMeiliSearch(
  process.env.GATSBY_MEILISEARCH_HOST,
  process.env.GATSBY_MEILISEARCH_API_KEY
)

const Search = () => {
  const Results = connectStateResults(({ searchState, searchResults, children }) =>
    searchState && searchState.query && searchResults && searchResults.nbHits !== 0 ? (
      <div className="absolute top-full w-full p-2 bg-gray-200 shadow-md">
        {children}
      </div>
    ) : (
      <div></div>
    )
  );

  return (
    <div className="relative">
      <InstantSearch indexName="products" searchClient={searchClient}>
        <SearchBox submit={null} reset={null} />
        <Results>
          <Hits hitComponent={Hit} />
        </Results>
      </InstantSearch>
    </div>
  )
}

const Hit = ({ hit }) => {
  return (
    <div key={hit.id} className="relative">
      <div className="hit-name">
        <Highlight attribute="title" hit={hit} tagName="mark" />
      </div>
    </div>
  )
}

export default Search;
```

This creates a search client using the method `instantMeiliSearch` that is exported from the dependency `@meilisearch/instant-meilisearch` which you just installed. You pass the method the environment variables you added earlier for the configurations.

The `Search` component then displays a search bar using components from `react-instantsearch-dom`. When the user enters a query and there are results, each result is rendered using the `Hit` component.

If you want to learn more about how you can customize the UI of the search bar and its options you can check out the documentation of [React InstantSearch by Algolia](https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/react/).

Next, you’ll add the search bar to the navigation bar. To do that, open `index.jsx` and import the `Search` component at the beginning of the file:

```jsx
import Search from "./search"
```

Then, in the returned JSX add the `Search` component before `RegionPopover`:

```jsx
//...
<Search />
<RegionPopover regions={mockData.regions} />
//...
```

Save all changes and open the storefront now. You should see a search bar in the navigation bar. Try to enter the name of one of your products and you should see it in the result.

![Search Engine](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0jnfppxjhw6a5coc7yyu.png)

### Add PayPal UI

In this section, you’ll add the UI necessary to use PayPal as a payment method.

In your terminal use the following command to install PayPal’s React library:

```bash
npm install @paypal/react-paypal-js
```

Then, create the file `src/components/payment/paypal-payment/index.jsx` with the following content:

```jsx
import { PayPalButtons, PayPalScriptProvider } from "@paypal/react-paypal-js";
import React, { useMemo, useState } from "react";

import { navigate } from "gatsby"
import { useCart } from "../../../hooks/use-cart"
import { useMedusa } from "../../../hooks/use-medusa";

const paypalClientId = process.env.GATSBY_PAYPAL_CLIENT_ID || ""
 const PaypalPayment = () => {
   const { 
     cart,
     actions: { completeCart, setPaymentSession },
   } = useCart()
   const [errorMessage, setErrorMessage] = useState(undefined)
   const [processing, setProcessing] = useState(false)
   const client = useMedusa()
   const paypalSession = useMemo(() => {
     if (cart.payment_sessions) {
       return cart.payment_sessions.find(s => s.provider_id === "paypal")
     }
     return null
   }, [cart.payment_sessions])
   if (!paypalSession) {
     return null
   }
   const completeOrder = async (authorizationOrder) => {
     const cart = await setPaymentSession("paypal")
     if (!cart) {
       setProcessing(false)
       return
     }
     await client.carts.updatePaymentSession(cart.id, "paypal", {
       data: {
         data: {
           ...authorizationOrder
         }
       }
     });
     const order = await completeCart(cart.id)
     if (!order || order.object !== "order") {
       setProcessing(false)
       return
     }
     setProcessing(false)
     navigate("/order-confirmed", { state: { order } })
   }
   const handlePayment = (data, actions) => {
     actions.order.authorize().then((authorization) => {
       if (authorization.status !== 'COMPLETED') {
         setErrorMessage(`An error occurred, status: ${authorization.status}`);
         setProcessing(false);
         return;
       }
       completeOrder(authorization)
     })
   }
   return (
     <PayPalScriptProvider options={{ 
       "client-id": paypalClientId,
       "currency": cart.region.currency_code.toUpperCase(),
       "intent": "authorize"
     }}>
         {errorMessage && (
           <span className="text-rose-500 mt-4">{errorMessage}</span>
         )}
         <PayPalButtons 
           style={{ layout: "horizontal" }}
           onApprove={handlePayment}
           disabled={processing}
         />
     </PayPalScriptProvider>
   )
 }
 export default PaypalPayment;
```

To briefly explain this code snippet:

- You render a PayPal button that allows customers to pay with PayPal using components from `@paypal/react-paypal-js` which you just installed. You pass the component `PayPalScriptProvider` the PayPal client ID from the environment variables.
- When the button is clicked, the method `handlePayment` is executed which initiates authorization with PayPal using the method `actions.order.authorize()`. This opens PayPal’s payment portal in a new window.
- After the customer successfully completes payment, the fulfillment callback function passed to `then` is executed. If there are any errors in the authorization, an error message will be shown. Otherwise, the `completeOrder` method will be called.
- In the `completeOrder` method, PayPal is first set as the payment session of the current cart. Then, it is updated on the server with data that is received from PayPal after the customer authorized the payment.
- Finally, the order is placed and the customer is redirected to the `order-confirmed` page where they can see a summary of their order details.

Next, in `src/components/payment/index.jsx` add an import for the `PaypalPayment` component at the beginning of the file:

```jsx
import PaypalPayment from "./paypal-payment"
```

Then, in the returned JSX you’ll find a switch statement that renders components based on the ID of the payment provider. Add a new case to the switch statement before the `default` case. This renders `PaypalPayment` when the ID of the payment provider available for the customer is `paypal`:

```jsx
switch (ps.provider_id) {
  case "stripe":
    //...
  case "manual":
    //...
  case "paypal":
    return <PaypalPayment />
  default:
    return null
}
```

Save all changes before moving on to test the entire flow.

## Test Checkout Flow

In this section, you’ll test placing an order on the storefront, then viewing the details on the admin and capturing payment.

Make sure that all 3 components (Medusa server, Medusa admin, and storefront) are running. Then, on your storefront, choose a product and it to cart.

![Product Page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ssja3se7cyrg0c6w4pht.png)

Then, click on the cart icon and click on the “Checkout” button in the popup.

You’ll be taken to a one-page checkout where you have to enter your details and choose a shipping method.

![Shipping Details](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p38re0olkhv4n4mnc2kt.png)

Once you reach the last step of the checkout you should see the available payment methods in the current region including PayPal.

> If you can’t see PayPal, make sure the correct region is selected at the top right of the navigation bar.

![PayPal Buttons](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s0hh1oe6zguqxgnttjp1.png)

Try paying with PayPal by clicking on the first PayPal button. A new page will open where you’re asked to login to PayPal and authorize the payment.

Once you authorize the payment with a sandbox account, you’ll be taken back to the storefront. If the authorization was successful, you’ll shortly be redirected to the Order Summary page.

![Order Summary](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/94vsbmph5x1l9g894m19.png)

On your Medusa admin, click on Orders in the sidebar. You should see a new order.

![Orders on Admin](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f6olx3or6hxbj55dre4d.png)

Click on the order. You’ll see the order details including the items ordered and payment details.

![Order Details on Admin](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bc7a6hlt6cq8pe2obesp.png)

To capture the payment, click on the “Capture payment” button.

## What’s Next?

You just created a comic book store using Medusa that has a search engine using MeiliSearch and PayPal as a payment provider. 

There’s much more that you can do with your ecommerce store:

- Check more plugins that you can add to add more features to your ecommerce store, including using [Strapi](https://docs.medusajs.com/add-plugins/strapi) for CMS.
- Customize your Medusa Server by adding [custom endpoints](https://docs.medusajs.com/advanced/backend/endpoints/add-storefront).
- Deploy [the server on Heroku](https://docs.medusajs.com/how-to/deploying-on-heroku) and the [Medusa Admin](https://docs.medusajs.com/how-to/deploying-admin-on-netlify) and [Storefront](https://docs.medusajs.com/how-to/deploying-gatsby-on-netlify) on Netlify.

> Should you have any issues or questions related to Medusa, then feel free to reach out to the Medusa team via [Discord](https://discord.gg/F87eGuwkTp).