Building real-time personalization with Next.js and Medusa

victorgerbrands

Victor Gerbrands

Posted on June 15, 2023

Building real-time personalization with Next.js and Medusa

What is this article about?

Medusa just released its new serverless Product Module. I’ve built a demo with Medusa and Next.js 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:


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. 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. 💜


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 to get it up and running in no time.

How to use the module

Create /api/products/route.ts and initialize the module:



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 });
}


Enter fullscreen mode Exit fullscreen mode

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:



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

  // do stuff with data.products


Enter fullscreen mode Exit fullscreen mode

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

The last clicked product category will decide what product category is shown on top of the second section:
Products based on user behavior

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:



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,
    }
  });
}


Enter fullscreen mode Exit fullscreen mode

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.



  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


Enter fullscreen mode Exit fullscreen mode

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:



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/"],
};


Enter fullscreen mode Exit fullscreen mode

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:



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();
}


Enter fullscreen mode Exit fullscreen mode

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



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


Enter fullscreen mode Exit fullscreen mode

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:



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
    },
  });
}


Enter fullscreen mode Exit fullscreen mode

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



  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


Enter fullscreen mode Exit fullscreen mode

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.

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

💖 💪 🙅 🚩
victorgerbrands
Victor Gerbrands

Posted on June 15, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related