Logo
ManuelSchoebel

Implement add to cart with next.js server actions, medusa.js and strapi.js

We need to enable customers to add products to their cart for the upcoming candy shop. I guess we can agree on this :)

In the context of our headless e-commerce shop the process will be like this

  1. a product url is opened in the browser e.g. /kraeutermischung
  2. We need to request the relevant information about this path from our Strapi.js CMS
  3. We find out that this is a product and request the relevant product data from Medusa.js, especially the variation IDs, since those would be what we will add to the cart
  4. When adding to a cart, we need to check if a cart exists and, if not, create one. The cartId will be stored in a cookie
  5. Once we add to the cart we want to open the cart, which will be a side drawer

This will be done using the next.js 14 app router and server actions.

Fetching the page data

In our catch-all dynamic route [[...slug]], we use the given slug to fetch the data from Strapi.js:

The code snippet above uses a async react server component which uses a getPageInfo fn to fetch data from strapi based on the slug that was requested.

Based on the data we get back we can decide weather it is a product page or some other type, like a content page or a category page.

In our case we render the <ProductPage ...> component since that’s all we have right now.

The function we use to request the page info looks like this:

In essence we are using our fetch client and request data from our Strapi CMS. In our case we use the product content type we created in Strapi. Since we are not yet having other page types we only request those. More will come later.

What we return is the data which we added in Strapi.

{
  "pageType": "ProductPage",
  "product": {
    "storeRef": "kraeutermischung",
    "path": "/kraeutermischung",
    "createdAt": "2024-03-14T10:16:39.762Z",
    "updatedAt": "2024-03-15T21:52:28.901Z",
    "publishedAt": "2024-03-14T10:16:40.408Z",
    "locale": "en",
    "sku": "KK-1001",
    "flavor_profile": {
    //...
}

Fetching the product from medusa

The important part here is the storeRef attribute. This is the handle inside our e-commerce backend Medusa.js. With this information we ca request the product data we need from Medusa.js.

But this data fetching is being done in the <ProductPage ...> component.

The function getProduct we execute in our react server component is using the storeRef to get the necessary product data from Medusa.js. The product coming from Strapi.js also contains the SKU information of the specific product variant behind the url that was requested.

The storeProduct, as we call the product data from our Medusa.js e-commerce backend, contains all variants. With the given SKU from strapi we can filter the variant we need.

The id of the variant is then passed to the <AddToCartBtn ...> component. And clicking this button should add the variant to cart.

The Add to Cart Form

In order to add to cart using next.js server actions we need several things. We need a form that we submit. A form action that triggers the server action. And within the server action we use a function to do a request to the Medusa.js e-commerce service.

Let’s start with the Button component:

The button component is a client component since it will have interactivity and is using hooks like useFormState. And this is only possible on the client side.

We create a formAction using the addItem function which is the actual server action. We bind the selectedVariantId to the formAction wich essentially is equivalent to passing the selectedVariantId as a parameter to the addItem function.

Once the button is clicked the form is submitted and with that the action executed. Note that the execution happens on the server side since the form submit is essentially sending an HTTP POST request to the next.js server side.

Let’s review the server action next:

The used functions here are wrappers around the medusa.js api. So they just do HTTP requests using the openapi-fetch client.

export async function getCart(cartId: string) {
  const response = await client.GET('/store/carts/{id}', {
    params: {
      path: { id: cartId },
    },
    next: {
      tags: [TAGS.cart],
    },
  });
  return response.data?.cart;
}
export async function createCart() {
  const response = await client.POST('/store/carts', {});
  return response.data?.cart;
}
 
export async function addToCart(
  cartId: string,
  item: components['schemas']['StorePostCartsCartLineItemsReq'],
) {
  const response = await client.POST('/store/carts/{id}/line-items', {
    params: {
      path: { id: cartId },
    },
    body: item,
    cache: 'no-store',
  });
}

With that we actually added an item to the cart.

Opening the Cart After a Product was Added

In the UI we want to open the cart once an item was added. Our cart component has a server component part which loads the cart data from medusa and then passes it to the react client component. The client component contains the button to open the cart and the cart modal itself.

import { cookies } from 'next/headers';
import { getCart, transformCart } from '@/lib/medusa';
import { components } from '@/types/medusa';
import { CartModal } from './CartModal';
 
export interface ICart {}
 
async function Cart({}: ICart) {
  const cartId = cookies().get('cartId')?.value;
  let cart;
 
  if (cartId) {
    try {
      cart = await getCart(cartId);
    } catch (e) {
      console.error(e);
    }
  }
 
  return (
    <CartModal cart={transformCart(cart as components['schemas']['Cart'])} />
  );
}
 
export { Cart };

Here we use the getCart function which in turn uses the cache tag cart . When adding to cart the cache for this tag gets revalidated and thus the server component for the cart rerendered. And this allows us to react to the changed data in the cart and open it up.

And that’s adding to cart using next.js 14 with server components, external services and server actions.

The implementation is heavily inspired by the vercel commerce starter kit.

©️ 2024 Digitale Kumpel GmbH. All rights reserved.