Building real-time personalization with Next.js and Medusa
Victor Gerbrands
Posted on June 15, 2023
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 });
}
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
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:
The last clicked product category will decide what product category is shown on top of the second section:
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,
}
});
}
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
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/"],
};
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();
}
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,
}),
});
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
},
});
}
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
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:
Posted on June 15, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.