Building real-time personalization with Next.js and Medusa
Tags: medusa, nextjs, serverless, typescript
Published: 2023-06-15 13:02:44

## What is this article about?
Medusa just released its new serverless Product Module. I’ve built a [demo with Medusa and Next.js](https://product-module.medusajs.com/) that showcases personalization using this new module running in a serverless Next.js function. In this post I will explain how the personalization logic is built, so you can start using the module to personalize your own storefronts.

In a nutshell, the Product Module connects to a product database and provides you with a service layer to communicate with the database directly within your Next.js project. This means there’s no need for a separate backend, and you get blazing fast response times. This makes it very suitable for use cases like real-time personalization.

## What will you be building?
You'll learn how to build real-time personalization logic using Next.js API routes and the Medusa Product Module, as showcased in the demo I've built:
{% embed https://www.loom.com/share/16f27ef70b0a40a39b7ca4686e287e51?sid=f205379f-050c-4df6-aad8-6697d422b6f0 %}
After completing this tutorial, you should be able to personalize your own Next.js storefront and apply the concepts to your own use case. I will not go into the frontend implementation in this article, if you'd like to know more about that, let me know.

---

## Medusa: open-source SDK for commerce
Quick background: Medusa is an [open-source SDK for ecommerce](https://medusajs.com/). It consists of modularized commerce logic like carts, products and order management. The modules are incredibly portable and can be tailored exactly to your specific needs.

The Product Module is part of the latest Medusa Recap. If you like this post, please help spread the word by liking the Recap [announcement Tweet](https://twitter.com/intent/like?tweet_id=1668362509076578304). 💜

---

## Get started: install the module
The Product Module is installed from npm into the Next.js project and connects to a Medusa database when imported and initialized. Follow [this guide](https://docs.medusajs.com/modules/products/serverless-module) to get it up and running in no time.

## How to use the module
Create `/api/products/route.ts` and initialize the module:

```typescript
import { initialize as initializeProductModule } from "@medusajs/product";
import { NextRequest, NextResponse } from "next/server";

export async function GET(req: NextRequest) {
  // initialize the module
  const productService = await initializeProductModule();

  // list all products
  const products = await productService.list();
  
  // return the product list json in the response
  return NextResponse.json({ products });
}
```

In this example we first import the Products Module, `NextRequest`, and `NextResponse`. Inside the GET function, we then initialize the module as `productService`. We then call the `.list()` method with an empty object as an argument to retrieve all products from the database. And finally, we return the full product list json in the response.

In your frontend component, you can now easily fetch the products from your created API route:

```typescript
  const response = await fetch('/api/products');
  const data = await response.json();

  // do stuff with data.products
```

## Start personalizing
In the demo, I use two parameters to personalize the products shown on the homepage:
- User location
- Last clicked product category

The user location will decide which products are shown in the first section:
![Products based on user location](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nf06c12vepzbymfg3otx.png)

The last clicked product category will decide what product category is shown on top of the second section:
![Products based on user behavior](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5fcpoyd7172948yutv5m.png)

Let's dig into the first section:

### Personalize on user location
Since the project is deployed on Vercel, we can use the built-in `x-vercel-ip-country` header to get the user's country code. I created a mapper object (`isoAlpha2Countries`) in `lib/utils` that maps the country code to its full country name and continent.

In our product database, I've added continent tags to all products. So there are products tagged with `Europe`, `Africa`, `Asia` and so on. We can use that tag to query products that are relevant to the user's location.

Let's add it to our route:

```typescript
import { initialize as initializeProductModule } from "@medusajs/product";
import { NextRequest, NextResponse } from "next/server";
import { isoAlpha2Countries } from "@/lib/utils";

export async function GET(req: NextRequest) {
  // initialize the module
  const productService = await initializeProductModule();

  // Get the user's country code from the header. 
  const countryCode = req.headers.get("x-vercel-ip-country"); 
	
  // Get the user's continent from the mapper.
  const { country, continent } = isoAlpha2Countries[countryCode];
	
  // List 3 products with a tag that matches the user's continent.
  const personalizedProducts = await productService.list(
    { tags: { value: [continent] } },
    { take: 3 }
  );
  
  // return the product list and country data in the response
  return NextResponse.json({ 
    personalized_section: {
      products: personalizedProducts,
      country,
      continent,
    }
  });
}
```

We can now easily fetch 3 products personalized on the user's location from any frontend component. We can also use the `continent` and `country` variables to personalize the copy above the products.

```typescript
  const response = await fetch('/api/products');
  const data = await response.json();
  const { products, country, continent } = data.personalized_section;

  // do stuff with products, country and continent
```

### Personalize on user behavior
We're gonna personalize the second product section based on user behavior. We'll track what product a user has viewed last, and then show products from that category on top.
This will work as follows:
- When a user enters the site, we'll generate a unique user id using Next.js Middleware and store it in a cookie.
- When a user clicks a product, we'll store the products category name and id in a Vercel KV store.
- In `/api/products/route.ts`, we'll use the user id to retrieve the last viewed category from the KV.
- We then retrieve all products from the Products Module, sort the products with the last viewed category on top, and return the sorted list.


#### Set a user id
Let's start with handling the user id in `src/middleware.ts`:

```typescript
import { v4 as uuidv4 } from "uuid";
import { NextResponse, NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
  // check if the userId cookie exists
  // if not, generate a new userId using uuid
  const userId = request.cookies.get("userId")?.value || uuidv4();

  // initialize a new response
  const response = NextResponse.next();

  // set the userId cookie 
  response.cookies.set("userId", userId);

  return response;
}

// by default, middleware will fire on every request
// use the matcher to match certain routes only
export const config = {
  matcher: ["/", "/product/:handle/"],
};
```

#### Store user behavior in Vercel KV
We now have a userId cookie available to identify unique users. Let's create a new API route in `/api/category-tracker/route.ts` to handle the category tracking in the KV store:

```typescript
import { kv } from "@vercel/kv";
import { NextRequest, NextResponse } from "next/server";

type UserData = {
  categoryId: string;
  categoryName: string;
};

export async function POST(request: NextRequest) {
  // grab the category id and name from the request
  const { categoryId, categoryName } = ((await request.json()) ??
    {}) as UserData;
  
  // return a null response when the category data is missing 
  if (!categoryId || !categoryName) {
    return NextResponse.json(null);
  }

  // create the userData object
  const userData = {
    categoryId,
    categoryName,
  };

  // grab the userId from the cookie we created with the middleware
  const userId = request.cookies.get("userId")?.value!;
  
  // set the userData object to the userId in the KV store
  await kv.set(userId, userData);

  // return an empty response
  return new NextResponse();
}
```

On the frontend, we can now easily do a POST request to `/api/category-tracker` to store the user's last viewed category.

```typescript
  await fetch("/api/category-tracker", {
    method: "POST",
    body: JSON.stringify({
      categoryId,
      categoryName,
    }),
  });
```

For example, you can wrap it in a `trackCategory()` function and pass it as the `onClick` prop for your product card component.

#### Update `/api/products/route.ts`
We can now update the route to fetch the user data, get all products and sort the products based on the last viewed category:

```typescript
import { kv } from "@vercel/kv";
import { initialize as initializeProductModule } from "@medusajs/product";
import { NextRequest, NextResponse } from "next/server";
import { isoAlpha2Countries } from "@/lib/utils";

type UserData = {
  categoryId: string;
  categoryName: string;
};

export async function GET(req: NextRequest) {
  // initialize the module
  const productService = await initializeProductModule();

  // Get the user's country code from the header. 
  const countryCode = req.headers.get("x-vercel-ip-country"); 
	
  // Get the user's continent from the mapper.
  const { country, continent } = isoAlpha2Countries[countryCode];
	
  // List 3 products with a tag that matches the user's continent.
  const personalizedProducts = await productService.list(
    { tags: { value: [continent] } },
    { take: 3 }
  );

  // get the userId from the cookie
  const userId = req.cookies.get("userId").value;

  // get the user's last viewed category form the KV
  const userData = await kv.get<UserData>(userId);
  const {categoryId, categoryName} = userData ?  userData : {} as UserData;
  
  // get all products from the module
  const allProducts = await productService.list();
  
  // sort all products with the last viewed categoryId 
  // on top using a sorting function (not in this example)
  const sortedProducts = sortProducts(allProducts, categoryId);

  
  // return all personalization data in the response
  return NextResponse.json({ 
    personalized_section: {
      products: personalizedProducts,
      country,
      continent,
    },
    all_products_section: {
      category_name: categoryName,
      products: sortedProducts
    },
  });
}
```

We now have easy access to all relevant personalized data in our frontend components!

```typescript
  const response = await fetch('/api/products');
  const data = await response.json();
  const { products: personalizedProducts, country, continent } = data.personalized_section;
  const { products: orderedProducts, category_name: categoryName } = data.all_products_section;

  // do stuff with products, country, continent and categoryName
```

### Concluding
Now you know how to use the Medusa Product Module with Next.js to personalize your storefront! You can apply the same principles to other types of personalization that might be relevant to your business.

If you have any questions, let me know in the comments below!

If you want to dive deeper into the source code of the demo, you can check out the full repo [here](https://github.com/medusajs/product-module-demo).

This module is part of Medusa's latest Recap. If you thought this was interesting, check out our other launches:

{% twitter 1671181706970255361 %}