How to Build a Wishlist Plugin

In this guide, you'll learn how to build a wishlist plugin in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a framework for customization. The Medusa application's commerce features are built around commerce modules which are available out-of-the-box.

Customers browsing your store may be interested in a product but not ready to buy it yet. They may want to save the product for later or share it with friends and family. A wishlist feature allows customers to save products they like and access them later.

This guide will teach you how to:

  • Install and set up a Medusa application project.
  • Install and set up a Medusa plugin.
  • Implement the wishlist features in the plugin.
    • Features include allowing customers to add products to a wishlist, view and manage their wishlist, and share their wishlist.
  • Test and use the wishlist plugin in your Medusa application.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

Wishlist Plugin Example Repository
Find the full code for this guide in, with the plugin to install.
OpenApi Specs for Postman
Import this OpenApi Specs file into tools like Postman.

Step 1: Install a Medusa Application#

You'll first install a Medusa application that exposes core commerce features through REST APIs. You'll later install the wishlist plugin in this application to test it out.

Start by installing the Medusa application on your machine with the following command:

Terminal
npx create-medusa-app@latest

You'll be asked for the project's name. You can also optionally choose to install the Next.js starter storefront.

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the {project-name}-storefront name.

Why is the storefront installed separatelyThe Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called API routes. Learn more about Medusa's architecture in this documentation.

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Ran into ErrorsCheck out the troubleshooting guides for help.

Step 2: Install a Medusa Plugin Project#

A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. You can add in the plugin API Routes, Workflows, and other customizations, as you'll see in this guide. Afterward, you can test it out locally in a Medusa application, then publish it to npm to install and use it in any Medusa application.

NoteLearn more about plugins in this documentation.

A Medusa plugin is set up in a different project, giving you the flexibility in building and publishing it, while providing you with the tools to test it out locally in a Medusa application.

To create a new Medusa plugin project, run the following command in a directory different than that of the Medusa application:

Where medusa-plugin-wishlist is the name of the plugin's directory and the name set in the plugin's package.json. So, if you wish to publish it to NPM later under a different name, you can change it here in the command or later in package.json.

Once the installation process is done, a new directory named medusa-plugin-wishlist will be created with the plugin project files.

Directory structure of a plugin project


Step 3: Set up Plugin in Medusa Application#

Before you start your development, you'll set up the plugin in the Medusa application you installed in the first step. This will allow you to test the plugin during your development process.

In the plugin's directory, run the following command to publish the plugin to the local package registry:

Plugin project
npx medusa plugin:publish

This command uses Yalc under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in package.json.

Next, you'll install the plugin in the Medusa application from the local registry.

NoteIf you've installed your Medusa project before v2.3.1, you must install yalc as a development dependency first.

Run the following command in the Medusa application's directory to install the plugin:

Medusa application
npx medusa plugin:add medusa-plugin-wishlist

This command installs the plugin in the Medusa application from the local package registry.

Next, register the plugin in the medusa-config.ts file of the Medusa application:

medusa-config.ts
1module.exports = defineConfig({2  // ...3  plugins: [4    {5      resolve: "medusa-plugin-wishlist",6      options: {},7    },8  ],9})

Finally, to ensure your plugin's changes are constantly published to the local registry, simplifying your testing process, keep the following command running in the plugin project during development:

Plugin project
npx medusa plugin:develop

Step 4: Implement Wishlist Module#

To add custom tables to the database, which are called data models, you create a module. A module is a package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

While you can create modules outside of a plugin and install them in the Medusa application, plugins allow you to bundle modules with other customizations, such as API routes and workflows.

In this step, you'll create a Wishlist Module within the wishlist plugin. This module adds custom data models for wishlists and their items, which you'll use in later steps to store a customer's wishlist.

NoteLearn more about modules in this documentation.

Create Module Directory#

A module is created under the src/modules directory of your plugin. So, create the directory src/modules/wishlist.

Diagram showcasing the module directory to create

Create Data Models#

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

NoteLearn more about data models in this documentation.

In the Wishlist Module, you'll create two data models: Wishlist and WishlistItem. The Wishlist model represents a customer's wishlist, while the WishlistItem model represents a product in the wishlist.

Starting with the Wishlist model, create a file src/modules/wishlist/models/wishlist.ts with the following content:

Directory structure after adding the Wishlist model

src/modules/wishlist/models/wishlist.ts
1import { model } from "@medusajs/framework/utils"2import { WishlistItem } from "./wishlist-item"3
4export const Wishlist = model.define("wishlist", {5  id: model.id().primaryKey(),6  customer_id: model.text(),7  sales_channel_id: model.text(),8  items: model.hasMany(() => WishlistItem),9})10.indexes([11  {12    on: ["customer_id", "sales_channel_id"],13    unique: true,14  },15])

The Wishlist model has the following properties:

  • id: A unique identifier for the wishlist.
  • customer_id: The ID of the customer who owns the wishlist.
  • sales_channel_id: The ID of the sales channel where the wishlist is created. In Medusa, product availability can differ between sales channels. This ensures only products available in the customer's sales channel are added to the wishlist.
  • items: A relation to the WishlistItem model, representing the products in the wishlist. You'll add this data model next.
NoteLearn more about data model properties and relations.

You also define a unique index on the customer_id and sales_channel_id columns to ensure a customer can only have one wishlist per sales channel.

NoteLearn more about data model indexes in this documentation.

Next, create the WishlistItem model in the file src/modules/wishlist/models/wishlist-item.ts:

Directory structure after adding the WishlistItem model

src/modules/wishlist/models/wishlist-item.ts
1import { model } from "@medusajs/framework/utils"2import { Wishlist } from "./wishlist"3
4export const WishlistItem = model.define("wishlist_item", {5  id: model.id().primaryKey(),6  product_variant_id: model.text(),7  wishlist: model.belongsTo(() => Wishlist, {8    mappedBy: "items",9  }),10})11.indexes([12  {13    on: ["product_variant_id", "wishlist_id"],14    unique: true,15  },16])

The WishlistItem model has the following properties:

  • id: A unique identifier for the wishlist item.
  • product_variant_id: The ID of the product variant in the wishlist.
  • wishlist: A relation to the Wishlist model, representing the wishlist the item belongs to.

You also define a unique index on the product_variant_id and wishlist_id columns to ensure a product variant is added to the wishlist only once. The wishlist_id column is available as a by-product of the belongsTo relation.

Create Service#

You define data-management methods of your data models in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can perform database operations.

NoteLearn more about services in this documentation.

In this section, you'll create the Wishlist Module's service that's used to manage wishlists and wishlist items. Create the file src/modules/wishlist/service.ts with the following content:

Directory structure after adding the service file

src/modules/wishlist/service.ts
1import { MedusaService } from "@medusajs/framework/utils"2import { Wishlist } from "./models/wishlist"3import { WishlistItem } from "./models/wishlist-item"4
5export default class WishlistModuleService extends MedusaService({6  Wishlist,7  WishlistItem,8}) {}

The WishlistModuleService extends MedusaService from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

So, the WishlistModuleService class now has methods like createWishlists and retrieveWishlist.

NoteFind all methods generated by the MedusaService in this reference.

You'll use this service in a later method to store and manage wishlists and wishlist items in other customizations.

Export Module Definition#

The final piece to a module is its definition, which you export in an index.ts file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file src/modules/wishlist/index.ts with the following content:

Directory structure after adding the module definition file

src/modules/wishlist/index.ts
1import WishlistModuleService from "./service"2import { Module } from "@medusajs/framework/utils"3
4export const WISHLIST_MODULE = "wishlist"5
6export default Module(WISHLIST_MODULE, {7  service: WishlistModuleService,8})

You use the Module function from the Modules SDK to create the module's definition. It accepts two parameters:

  1. The module's name, which is wishlist.
  2. An object with a required property service indicating the module's service.

You'll later use the module's service to manage wishlists and wishlist items in other customizations.

Generate Migrations#

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.

NoteLearn more about migrations in this documentation.

Medusa's CLI tool generates the migrations for you. To generate a migration for the Wishlist Module, run the following command in the plugin project:

Plugin project
npx medusa plugin:db:generate

You'll now have a migrations directory under src/modules/wishlist that holds the generated migration.

Then, to reflect these migrations on the database of the Medusa application using this module, run the following command:

NoteMake sure that npx medusa plugin:develop is running in the plugin project to publish the changes to the local registry.
Medusa application
npx medusa db:migrate

The tables of the Wishlist Module's data models are now created in the database.


The Wishlist Module's data models store IDs of records in data models implemented in Medusa's core commerce modules, such as the ID of a customer or a product variant.

However, modules are isolated to ensure they're re-usable and don't have side effects when integrated into the Medusa application. So, to build associations between modules, you define module links. A Module link associates two modules' data models while maintaining module isolation.

In this section, you'll link the Wishlist data model to the Customer Module's Customer data model, and to the Sales Channel Module's SalesChannel data model. You'll also link the WishlistItem data model to the Product Module's ProductVariant data model.

NoteLearn more about module links in this documentation.

To create the link between the Wishlist data model and the Customer data model, create the file src/modules/wishlist/links/wishlist-customer.ts with the following content:

Directory structure after adding the link file

src/modules/wishlist/links/wishlist-customer.ts
1import { defineLink } from "@medusajs/framework/utils"2import WishlistModule from "../modules/wishlist"3import CustomerModule from "@medusajs/medusa/customer"4
5export default defineLink(6  {7    ...WishlistModule.linkable.wishlist.id,8    field: "customer_id",9  },10  CustomerModule.linkable.customer.id,11  {12    readOnly: true,13  }14)

You define a link using defineLink from the Modules SDK. It accepts three parameters:

  1. The first data model part of the link, which is the Wishlist Module's wishlist data model. A module has a special linkable property that contain link configurations for its data models. You also specify the field that points to the customer.
  2. The second data model part of the link, which is the Customer Module's customer data model.
  3. An object of configurations for the module link. By default, Medusa creates a table in the database to represent the link you define. However, in this guide, you only want this link to retrieve the customer associated with a wishlist. So, you enable readOnly telling Medusa not to create a table for this link.

Next, to create the link between the Wishlist data model and the SalesChannel data model, create the file src/modules/wishlist/links/wishlist-sales-channel.ts with the following content:

Directory structure after adding the link file

src/modules/wishlist/links/wishlist-sales-channel.ts
1import { defineLink } from "@medusajs/framework/utils"2import WishlistModule from "../modules/wishlist"3import SalesChannelModule from "@medusajs/medusa/sales-channel"4
5export default defineLink(6  {7    ...WishlistModule.linkable.wishlist.id,8    field: "sales_channel_id",9  },10  SalesChannelModule.linkable.salesChannel,11  {12    readOnly: true,13  }14)

You define a link between the Wishlist data model and the SalesChannel data model in the same way as the previous link.

Finally, to create the link between the WishlistItem data model and the ProductVariant data model, create the file src/modules/wishlist/links/wishlist-product.ts with the following content:

Directory structure after adding the link file

src/modules/wishlist/links/wishlist-product.ts
1import { defineLink } from "@medusajs/framework/utils"2import WishlistModule from "../modules/wishlist"3import ProductModule from "@medusajs/medusa/product"4
5export default defineLink(6  {7    ...WishlistModule.linkable.wishlistItem.id,8    field: "product_variant_id",9  },10  ProductModule.linkable.productVariant,11  {12    readOnly: true,13  }14)

You define a link between the WishlistItem data model and the ProductVariant data model in the same way as the previous links.

In the next steps, you'll see how these links allow you to retrieve the resources associated with a wishlist or wishlist item.


Step 6: Create Wishlist Workflow#

The first feature you'll add to the wishlist plugin is the ability to create a wishlist for a customer. You'll implement this feature in a workflow.

A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an endpoint.

In this section, you'll create a workflow that creates a wishlist for a customer. Later, you'll execute this workflow from an API route.

NoteLearn more about workflows in this documentation

The workflow has the following steps:

You'll implement the steps before implementing the workflow.

validateCustomerCreateWishlistStep#

The first step in the workflow will validate that a customer doesn't have an existing workflow. If not valid, the step will throw an error, stopping the workflow's execution.

To create the step, create the file src/workflows/steps/validate-customer-create-wishlist.ts with the following content:

Directory structure after adding the step file

src/workflows/steps/validate-customer-create-wishlist.ts
1import { MedusaError } from "@medusajs/framework/utils"2import { createStep } from "@medusajs/framework/workflows-sdk"3
4type ValidateCustomerCreateWishlistStepInput = {5  customer_id: string6}7
8export const validateCustomerCreateWishlistStep = createStep(9  "validate-customer-create-wishlist",10  async ({ customer_id }: ValidateCustomerCreateWishlistStepInput, { container }) => {11    const query = container.resolve("query")12
13    const { data } = await query.graph({14      entity: "wishlist",15      fields: ["*"],16      filters: {17        customer_id: customer_id,18      },19    })20
21    if (data.length) {22      throw new MedusaError(23        MedusaError.Types.NOT_FOUND,24        "Customer already has a wishlist"25      )26    }27
28    // check that customer exists29    const { data: customers } = await query.graph({30      entity: "customer",31      fields: ["*"],32      filters: {33        id: customer_id,34      },35    })36
37    if (customers.length === 0) {38      throw new MedusaError(39        MedusaError.Types.INVALID_DATA,40        "Specified customer was not found"41      )42    }43  }44)

You create a step using createStep from the Workflows SDK. It accepts two parameters:

  1. The step's name, which is validate-customer-create-wishlist.
  2. An async function that executes the step's logic. The function receives two parameters:
    • The input data for the step, which in this case is an object having a customer_id property.
    • An object holding the workflow's context, including the Medusa Container that allows you to resolve framework and commerce tools.

In the step function, you use Query to retrieve the wishlist based on the specified customer ID. If a wishlist exists, you throw an error, stopping the workflow's execution.

You also try to retrieve the customer, and if they don't exist, you throw an error.

createWishlistStep#

The second step in the workflow will create a wishlist for the customer. To create the step, create the file src/workflows/steps/create-wishlist.ts with the following content:

Directory structure after adding the step file

src/workflows/steps/create-wishlist.ts
1import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"2import { WISHLIST_MODULE } from "../../modules/wishlist"3import WishlistModuleService from "../../modules/wishlist/service"4
5type CreateWishlistStepInput = {6  customer_id: string7  sales_channel_id: string8}9
10export const createWishlistStep = createStep(11  "create-wishlist",12  async (input: CreateWishlistStepInput, { container }) => {13    const wishlistModuleService: WishlistModuleService = 14      container.resolve(WISHLIST_MODULE)15
16    const wishlist = await wishlistModuleService.createWishlists(input)17
18    return new StepResponse(wishlist, wishlist.id)19  },20  async (id, { container }) => {21    const wishlistModuleService: WishlistModuleService = 22      container.resolve(WISHLIST_MODULE)23
24    await wishlistModuleService.deleteWishlists(id)25  }26)

This step accepts the IDs of the customer and the sales channel as input. In the step function, you resolve the Wishlist Module's service from the container and use its generated createWishlists method to create the wishlist, passing it the input as a parameter.

Steps that return data must return them in a StepResponse instance. The StepResponse constructor accepts two parameters:

  • The data to return, which in this case is the created wishlist.
  • The data to pass to the compensation function, which in this case is the wishlist's ID.

The compensation function is an optional third parameter of createStep. It defines rollback logic that's executed when an error occurs during the workflow's execution. In the compensation function, you undo the actions you performed in the step function.

The compensation function accepts as a first parameter the data passed as a second parameter to the StepResponse returned by the step function, which in this case is the wishlist's ID. In the compensation function, you resolve the Wishlist Module's service from the container and use its generated deleteWishlists method to delete the wishlist.

NoteLearn more about the generated create and delete methods.

Add createWishlistWorkflow#

You can now add the createWishlistWorkflow to the plugin. Create the file src/workflows/create-wishlist.ts with the following content:

Directory structure after adding the workflow file

src/workflows/create-wishlist.ts
1import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"2import { validateCustomerCreateWishlistStep } from "./steps/validate-customer-create-wishlist"3import { createWishlistStep } from "./steps/create-wishlist"4
5type CreateWishlistWorkflowInput = {6  customer_id: string7  sales_channel_id: string8}9
10export const createWishlistWorkflow = createWorkflow(11  "create-wishlist",12  (input: CreateWishlistWorkflowInput) => {13    validateCustomerCreateWishlistStep({14      customer_id: input.customer_id,15    })16
17    const wishlist = createWishlistStep(input)18
19    return new WorkflowResponse({20      wishlist,21    })22  }23)

You create a workflow using createWorkflow from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. In the workflow, you:

  • Execute the validateCustomerCreateWishlistStep step to validate that the customer doesn't have an existing wishlist.
  • Execute the createWishlistStep step to create the wishlist.
TipA workflow's constructor function has some constraints in implementation, which is why you need to use transform for variable manipulation. Learn more about these constraints in this documentation.

Workflows must return an instance of WorkflowResponse, passing as a parameter the data to return to the workflow's executor. The workflow returns an object having a wishlist property, which is the created wishlist.

You'll execute this workflow in an API route in the next step.


Step 7: Create Wishlist API Route#

Now that you implemented the flow to create a wishlist for a customer, you'll create an API route that exposes this functionality.

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. You'll create a POST API route at the path /store/customers/me/wishlists that executes the workflow from the previous step.

NoteLearn more about API routes in this documentation.

An API route is created in a route.ts file under a sub-directory of the src/api directory. The path of the API route is the file's path relative to src/api.

So, to create the /store/customers/me/wishlists API route, create the file src/api/store/customers/me/wishlists/route.ts with the following content:

Directory structure after adding the route file

src/api/store/customers/me/wishlists/route.ts
1import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"2import { createWishlistWorkflow } from "../../../../../workflows/create-wishlist"3import { MedusaError } from "@medusajs/framework/utils"4
5export async function POST(6  req: AuthenticatedMedusaRequest,7  res: MedusaResponse8) {9  if (!req.publishable_key_context?.sales_channel_ids.length) {10    throw new MedusaError(11      MedusaError.Types.INVALID_DATA,12      "At least one sales channel ID is required to be associated with the publishable API key in the request header."13    )14  }15  const { result } = await createWishlistWorkflow(req.scope)16    .run({17      input: {18        customer_id: req.auth_context.actor_id,19        sales_channel_id: req.publishable_key_context?.sales_channel_ids[0],20      },21    })22
23  res.json({24    wishlist: result.wishlist,25  })26}

Since you export a POST function in this file, you're exposing a POST API route at /store/customers/me/wishlists. The route handler function accepts two parameters:

  1. A request object with details and context about the request, such as authenticated customer details.
  2. A response object to manipulate and send the response.

API routes implemented under the /store path require passing a publishable API key in the header of the request. The publishable API key is created by an admin user and is associated with one or more sales channels. In the route handler function, you validate that the request has at least one sales channel ID associated with the publishable API key. You'll use that sales channel ID with the wishlist you're creating.

Also, API routes implemented under the /store/customers/me path are only accessible by authenticated customers. You access the ID of the authenticated customer using the auth_context.actor_id property of the request object.

In the route handler function, you execute the createWishlistWorkflow, passing the authenticated customer ID and the sales channel ID as input. The workflow returns an object having a result property, which is the data returned by the workflow. You return the created wishlist in the response.

Test API Route#

You'll now test that this API route defined in the plugin is working as expected using the Medusa application you installed in the first step.

NoteMake sure that npx medusa plugin:develop is running in the plugin project to publish the changes to the local registry.

In the Medusa application's directory, run the following command to start the development server:

Retrieve Publishable API Key#

Before sending the request, you need to obtain a publishable API key. So, open the Medusa Admin at http://localhost:9000/app and log in with the user you created earlier.

To access your application's API keys in the admin, go to Settings -> Publishable API Keys. You'll have an API key created by default, which is associated with the default sales channel. You can use this publishable API key in the request header.

In the admin, click on Publishable API key in the sidebar. A table will show your API keys and allow you to create one.

Retrieve Authenticated Customer Token#

Then, you need an authentication token of a registered customer. To create a customer, first, send the following request to the Medusa application:

Code
1curl -X POST 'http://localhost:9000/auth/customer/emailpass/register' \2--header 'Content-Type: application/json' \3--data-raw '{4    "email": "customer@gmail.com",5    "password": "supersecret"6}'

This API route obtains a registration token for the specified email and password in the request body.

Next, use that token to register the customer:

Code
1curl -X POST 'http://localhost:9000/store/customers' \2--header 'Content-Type: application/json' \3-H 'x-publishable-api-key: {api_key}' \4--header 'Authorization Bearer {token}' \5--data-raw '{6    "email": "customer@gmail.com"7}'

Make sure to replace {api_key} with the publishable API key you copied from the settings, and {token} with the token received from the previous request.

This will create a customer. You can now obtain the customer's authentication token by sending the following request:

Code
1curl -X POST 'http://localhost:9000/auth/customer/emailpass' \2--header 'Content-Type: application/json' \3--data-raw '{4    "email": "customer@gmail.com",5    "password": "supersecret"6}'

This API route will return an authentication token for the customer. You'll use this token in the header of the following requests.

Send Request to Create Wishlist#

Finally, send a POST request to the /store/customers/me/wishlists API route to create a wishlist for the authenticated customer:

Code
1curl -X POST 'localhost:9000/store/customers/me/wishlists' \2--header 'x-publishable-api-key: {api_key}' \3--header 'Authorization: Bearer {token}'

Make sure to replace {api_key} with the publishable API key you copied from the settings, and {token} with the authenticated customer token.

You'll receive in the response the created wishlist.


Step 8: Retrieve Wishlist API Route#

In this step, you'll add an API route to retrieve a customer's wishlist. You'll create a GET API route at the path /store/customers/me/wishlists that retrieves the wishlist of the authenticated customer.

So, add to the src/api/store/customers/me/wishlists/route.ts the following:

src/api/store/customers/me/wishlists/route.ts
1export async function GET(2  req: AuthenticatedMedusaRequest,3  res: MedusaResponse4) {5  const query = req.scope.resolve("query")6
7  const { data } = await query.graph({8    entity: "wishlist",9    fields: ["*", "items.*", "items.product_variant.*"],10    filters: {11      customer_id: req.auth_context.actor_id,12    },13  })14
15  if (!data.length) {16    throw new MedusaError(17      MedusaError.Types.NOT_FOUND,18      "No wishlist found for customer"19    )20  }21
22  return res.json({23    wishlist: data[0],24  })25}

In this route handler function, you use Query to retrieve the wishlist of the authenticated customer. For each wishlist, you retrieve its items, and the product variants of those items.

If the wishlist doesn't exist, you throw an error. Otherwise, you return the wishlist in the response.

Test Retrieve Wishlist API Route#

To test the API route, start the Medusa application.

NoteMake sure that npx medusa plugin:develop is running in the plugin project to publish the changes to the local registry.

Then, send a GET request to the /store/customers/me/wishlists API route:

Code
1curl 'localhost:9000/store/customers/me/wishlists' \2--header 'x-publishable-api-key: {api_key}' \3--header 'Authorization: Bearer {token}'

Make sure to replace:

  • {api_key} with the publishable API key you copied from the settings, as explained in the previous step.
  • {token} with the authenticated customer token you received from the previous step.

You'll receive in the response the wishlist of the authenticated customer.


Step 9: Add Item to Wishlist API Route#

Next, you'll add the functionality to add an item to a wishlist. You'll first define a workflow that implements this functionality, then create an API route that executes the workflow.

Add Item to Wishlist Workflow#

The workflow to add an item to a wishlist has the following steps:

The useQueryGraphStep is from Medusa's workflows package. So, you'll only implement the other steps.

validateWishlistSalesChannelStep

The second step in the workflow validates that the wishlist belongs to the sales channel specified in the input.

To create the step, create the file src/workflows/steps/validate-wishlist-sales-channel.ts with the following content:

Directory structure after adding the step file

src/workflows/steps/validate-wishlist-sales-channel.ts
1import { createStep } from "@medusajs/framework/workflows-sdk"2import { InferTypeOf } from "@medusajs/framework/types"3import { Wishlist } from "../../modules/wishlist/models/wishlist"4
5type ValidateWishlistSalesChannelStepInput = {6  wishlist: InferTypeOf<typeof Wishlist>7  sales_channel_id: string8}9
10export const validateWishlistSalesChannelStep = createStep(11  "validate-wishlist-sales-channel",12  async (input: ValidateWishlistSalesChannelStepInput, { container }) => {13    const { wishlist, sales_channel_id } = input14
15    if (wishlist.sales_channel_id !== sales_channel_id) {16      throw new Error("Wishlist does not belong to the current sales channel")17    }18  }19)

This step receives the wishlist object and the sales channel ID as input. In the step function, if the wishlist's sales channel ID doesn't match the sales channel ID in the input, you throw an error.

NoteTo represent a data model in a type, use the InferTypeOf utility.

validateVariantWishlistStep

The next step in the workflow validates that the specified variant is not already in the wishlist.

Create the file src/workflows/steps/validate-variant-wishlist.ts with the following content:

Directory structure after adding the step file

src/workflows/steps/validate-variant-wishlist.ts
1import { InferTypeOf } from "@medusajs/framework/types"2import { Wishlist } from "../../modules/wishlist/models/wishlist"3import { createStep } from "@medusajs/framework/workflows-sdk"4import { MedusaError } from "@medusajs/framework/utils"5
6type ValidateVariantWishlistStepInput = {7  variant_id: string8  sales_channel_id: string9  wishlist: InferTypeOf<typeof Wishlist>10}11
12export const validateVariantWishlistStep = createStep(13  "validate-variant-in-wishlist",14  async ({ 15    variant_id, 16    sales_channel_id,17    wishlist,18  }: ValidateVariantWishlistStepInput, { container }) => {19    // validate whether variant is in wishlist20    const isInWishlist = wishlist.items?.some(21      (item) => item.product_variant_id === variant_id22    )23
24    if (isInWishlist) {25      throw new MedusaError(26        MedusaError.Types.INVALID_DATA,27        "Variant is already in wishlist"28      )29    }30
31    // validate that the variant is available in the specified sales channel32    const query = container.resolve("query")33    const { data } = await query.graph({34      entity: "variant",35      fields: ["product.sales_channels.*"],36      filters: {37        id: variant_id,38      },39    })40
41    const variantInSalesChannel = data[0].product.sales_channels.some(42      (sc) => sc.id === sales_channel_id43    )44
45    if (!variantInSalesChannel) {46      throw new MedusaError(47        MedusaError.Types.INVALID_DATA,48        "Variant is not available in the specified sales channel"49      )50    }51  }52)

This step receives the variant ID, sales channel ID, and wishlist object as input. In the step function, you throw an error if:

  • The variant is already in the wishlist.
  • The variant is not available in the specified sales channel. You use Query to retrieve the sales channels that the variant's product is available in.

createWishlistItemStep

The fourth step in the workflow creates a wishlist item for the specified variant in the wishlist.

Create the file src/workflows/steps/create-wishlist-item.ts with the following content:

Directory structure after adding the step file

src/workflows/steps/create-wishlist-item.ts
1import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"2import WishlistModuleService from "../../modules/wishlist/service"3import { WISHLIST_MODULE } from "../../modules/wishlist"4
5type CreateWishlistItemStepInput = {6  wishlist_id: string7  product_variant_id: string8}9
10export const createWishlistItemStep = createStep(11  "create-wishlist-item",12  async (input: CreateWishlistItemStepInput, { container }) => {13    const wishlistModuleService: WishlistModuleService = 14      container.resolve(WISHLIST_MODULE)15
16    const item = await wishlistModuleService.createWishlistItems(input)17
18    return new StepResponse(item, item.id)19  },20  async (id, { container }) => {21    const wishlistModuleService: WishlistModuleService = 22      container.resolve(WISHLIST_MODULE)23
24    await wishlistModuleService.deleteWishlistItems(id)25  }26)

This step receives the wishlist ID and the variant ID as input. In the step function, you resolve the Wishlist Module's service from the container and use its generated createWishlistItems method to create the wishlist item, passing it the input as a parameter.

You return the created wishlist item and pass the item's ID to the compensation function. In the compensation function, you resolve the Wishlist Module's service from the container and use its generated deleteWishlistItems method to delete the wishlist item if an error occurs in the workflow.

Add Item to Wishlist Workflow

You can now add the createWishlistItemWorkflow to the plugin. Create the file src/workflows/create-wishlist-item.ts with the following content:

Directory structure after adding the workflow file

src/workflows/create-wishlist-item.ts
5import { validateVariantWishlistStep } from "./steps/validate-variant-wishlist"6
7type CreateWishlistItemWorkflowInput = {8  variant_id: string9  customer_id: string10  sales_channel_id: string11}12
13export const createWishlistItemWorkflow = createWorkflow(14  "create-wishlist-item",15  (input: CreateWishlistItemWorkflowInput) => {16    const { data: wishlists } = useQueryGraphStep({17      entity: "wishlist",18      fields: ["*", "items.*"],19      filters: {20        customer_id: input.customer_id,21      },22      options: {23        throwIfKeyNotFound: true,24      },25    })26
27    validateWishlistSalesChannelStep({28      wishlist: wishlists[0],29      sales_channel_id: input.sales_channel_id,30    })31
32
33    validateVariantWishlistStep({34      variant_id: input.variant_id,35      sales_channel_id: input.sales_channel_id,36      wishlist: wishlists[0],37    })38
39    createWishlistItemStep({40      product_variant_id: input.variant_id,41      wishlist_id: wishlists[0].id,42    })43
44    // refetch wishlist45    const { data: updatedWishlists } = useQueryGraphStep({46      entity: "wishlist",47      fields: ["*", "items.*", "items.product_variant.*"],48      filters: {49        id: wishlists[0].id,50      },51    }).config({ name: "refetch-wishlist" })52
53    return new WorkflowResponse({54      wishlist: updatedWishlists[0],55    })56  }57)

You create a createWishlistItemWorkflow. In the workflow, you:

  • Use the useQueryGraphStep to retrieve the wishlist of a customer. Notice that you pass the link definition between a wishlist and a customer as an entry point to Query. This allows you to filter the wishlist by the customer ID.
  • Use the validateWishlistSalesChannelStep step to validate that the wishlist belongs to the sales channel specified in the input.
  • Use the validateVariantWishlistStep step to validate that the variant specified in the input is not already in the wishlist.
  • Use the createWishlistItemStep step to create the wishlist item.
  • Use the useQueryGraphStep again to retrieve the wishlist with the new item added.

You return the wishlist with its items.

Add Item to Wishlist API Route#

You'll now create an API route that executes the createWishlistItemWorkflow to add an item to a wishlist.

Create the file src/api/store/customers/me/wishlists/items/route.ts with the following content:

Directory structure after adding the route file

src/api/store/customers/me/wishlists/items/route.ts
1import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework"2import { createWishlistItemWorkflow } from "../../../../../../workflows/create-wishlist-item"3import { MedusaError } from "@medusajs/framework/utils"4
5type PostStoreCreateWishlistItemType = {6  variant_id: string7}8
9export async function POST(10  req: AuthenticatedMedusaRequest<PostStoreCreateWishlistItemType>,11  res: MedusaResponse12) {13  if (!req.publishable_key_context?.sales_channel_ids.length) {14    throw new MedusaError(15      MedusaError.Types.INVALID_DATA,16      "At least one sales channel ID is required to be associated with the publishable API key in the request header."17    )18  }19  const { result } = await createWishlistItemWorkflow(req.scope)20    .run({21      input: {22        variant_id: req.validatedBody.variant_id,23        customer_id: req.auth_context.actor_id,24        sales_channel_id: req.publishable_key_context?.sales_channel_ids[0],25      },26    })27
28  res.json({29    wishlist: result.wishlist,30  })31}

This route exposes a POST endpoint at /store/customers/me/wishlists/items. Notice that the AuthenticatedMedusaRequest accepts a type parameter indicating the type of the accepted request body. In this case, the request body must have a variant_id property, indicating the ID of the variant to add to the wishlist.

In the route handler function, you execute the createWishlistItemWorkflow workflow, passing the authenticated customer ID, the variant ID, and the sales channel ID as input. You return in the response the updated wishlist.

Add Validation Schema#

To ensure that a variant ID is passed in the body of requests sent to this API route, you'll define a validation schema for the request body.

In Medusa, you create validation schemas using Zod in a TypeScript file under the src/api directory. So, create the file src/api/store/customers/me/wishlists/items/validators.ts with the following content:

Directory structure after adding the validation schema file

src/api/store/customers/me/wishlists/items/validators.ts
1import { z } from "zod"2
3export const PostStoreCreateWishlistItem = z.object({4  variant_id: z.string(),5})

You create an object schema with a variant_id property of type string.

NoteLearn more about creating schemas in Zod's documentation.

You can now replace the PostStoreCreateWishlistItemType type in src/api/store/customers/me/wishlists/items/route.ts with the following:

src/api/store/customers/me/wishlists/items/route.ts
1// ...2import { z } from "zod"3import { PostStoreCreateWishlistItem } from "./validators"4
5type PostStoreCreateWishlistItemType = z.infer<6  typeof PostStoreCreateWishlistItem7>

Finally, to use the schema for validation, you need to apply the validateAndTransformBody middleware on the /store/customers/me/wishlists/items route. A middleware is a function executed before the API route when a request is sent to it.

The validateAndTransformBody middleware is available out-of-the-box in Medusa, allowing you to validate and transform the request body using a Zod schema.

NoteLearn more about middlewares in this documentation.

To apply the middleware, create the file src/api/middlewares.ts with the following content:

Directory structure after adding the middleware file

src/api/middlewares.ts
1import { 2  defineMiddlewares,3  validateAndTransformBody,4} from "@medusajs/framework/http"5import { 6  PostStoreCreateWishlistItem,7} from "./store/customers/me/wishlists/items/validators"8
9export default defineMiddlewares({10  routes: [11    {12      matcher: "/store/customers/me/wishlists/items",13      method: "POST",14      middlewares: [15        validateAndTransformBody(PostStoreCreateWishlistItem),16      ],17    },18  ],19})

In this file, you export the middlewares definition using defineMiddlewares from the Medusa Framework. This function accepts an object having a routes property, which is an array of middleware configurations to apply on routes.

You pass in the routes array an object having the following properties:

  • matcher: The route to apply the middleware on.
  • method: The HTTP method to apply the middleware on for the specified API route.
  • middlewares: An array of the middlewares to apply. You apply the following middleware:
    • validateAndTransformBody: A middleware to ensure the received request body is valid against the Zod schema you defined earlier.

Any request sent to /store/customers/me/wishlists/items will now automatically fail if its body parameters don't match the PostStoreCreateWishlistItem validation schema.

Test API Route#

Start the Medusa application to test out the API route.

NoteMake sure that npx medusa plugin:develop is running in the plugin project to publish the changes to the local registry.

Retrieve Variant ID

To retrieve an ID of a variant to add to the wishlist, send a GET request to the /store/products API route:

Code
1curl 'localhost:9000/store/products' \2--header 'x-publishable-api-key: {api_key}'

Make sure to replace {api_key} with the publishable API key you copied from the settings, as explained in a previous section.

The response will contain a list of products. You can use the id of a product's variant to add to the wishlist.

Add Variant to Wishlist

Then, send a POST request to the /store/customers/me/wishlists/items API route to add the variant to the wishlist:

Code
1curl -X GET 'localhost:9000/store/customers/me/wishlists/items' \2--header 'Content-Type: application/json'  \3--header 'x-publishable-api-key: {api_key}' \4--header 'Authorization: Bearer {token}' \5--data-raw '{6    "variant_id": "{variant_id}"7}'

Make sure to replace:

  • {api_key} with the publishable API key you copied from the settings, as explained in a previous section.
  • {token} with the authenticated customer token, as explained in a previous section.
  • {variant_id} with the ID of the variant you retrieved from the /store/products API route.

You'll receive in the response the updated wishlist with the added item.


Step 10: Remove Item from Wishlist API Route#

In this step, you'll add the functionality to remove an item from a wishlist. You'll first define a workflow that implements this functionality, then create an API route that executes the workflow.

Remove Item from Wishlist Workflow#

The workflow to remove an item from a wishlist has the following steps:

The useQueryGraphStep is from Medusa's workflows package. So, you'll only implement the other steps.

validateItemInWishlistStep

The second step of the workflow validates that the item to remove is in the authenticated customer's wishlist.

To create the step, create the file src/workflows/steps/validate-item-in-wishlist.ts with the following content:

Directory structure after adding the step file

src/workflows/steps/validate-item-in-wishlist.ts
1import { InferTypeOf } from "@medusajs/framework/types"2import { Wishlist } from "../../modules/wishlist/models/wishlist"3import { createStep } from "@medusajs/framework/workflows-sdk"4import { MedusaError } from "@medusajs/framework/utils"5
6type ValidateItemInWishlistStepInput = {7  wishlist: InferTypeOf<typeof Wishlist>8  wishlist_item_id: string9}10
11export const validateItemInWishlistStep = createStep(12  "validate-item-in-wishlist",13  async ({ 14    wishlist, 15    wishlist_item_id,16  }: ValidateItemInWishlistStepInput, { container }) => {17    const item = wishlist.items.find((item) => item.id === wishlist_item_id)18
19    if (!item) {20      throw new MedusaError(21        MedusaError.Types.INVALID_DATA,22        "Item does not exist in customer's wishlist"23      )24    }25  }26)

This step receives the wishlist object and the wishlist item ID as input. In the step function, you find the item in the wishlist by its ID. If the item doesn't exist, you throw an error.

deleteWishlistItemStep

The third step of the workflow deletes the item from the wishlist.

Create the file src/workflows/steps/delete-wishlist-item.ts with the following content:

Directory structure after adding the step file

src/workflows/steps/delete-wishlist-item.ts
1import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"2import WishlistModuleService from "../../modules/wishlist/service"3import { WISHLIST_MODULE } from "../../modules/wishlist"4
5type DeleteWishlistItemStepInput = {6  wishlist_item_id: string7}8
9export const deleteWishlistItemStep = createStep(10  "delete-wishlist-item",11  async ({ wishlist_item_id }: DeleteWishlistItemStepInput, { container }) => {12    const wishlistModuleService: WishlistModuleService = 13      container.resolve(WISHLIST_MODULE)14
15    await wishlistModuleService.softDeleteWishlistItems(wishlist_item_id)16
17    return new StepResponse(void 0, wishlist_item_id)18  },19  async (wishlistItemId, { container }) => {20    const wishlistModuleService: WishlistModuleService = 21      container.resolve(WISHLIST_MODULE)22
23    await wishlistModuleService.restoreWishlistItems([wishlistItemId])24  }25)

This step receives the wishlist item ID as input. In the step function, you resolve the Wishlist Module's service from the container and use its generated softDeleteWishlistItems method to delete the wishlist item.

You pass the deleted wishlist item ID to the compensation function. In the compensation function, you resolve the Wishlist Module's service from the container and use its generated restoreWishlistItems method to restore the wishlist item if an error occurs in the workflow.

NoteLearn more about the softDelete and restore generated methods.

Remove Item from Wishlist Workflow

You can now add the deleteWishlistItemWorkflow to the plugin. Create the file src/workflows/delete-wishlist-item.ts with the following content:

Directory structure after adding the workflow file

src/workflows/delete-wishlist-item.ts
1import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"2import { deleteWishlistItemStep } from "./steps/delete-wishlist-item"3import { useQueryGraphStep } from "@medusajs/medusa/core-flows"4import { validateItemInWishlistStep } from "./steps/validate-item-in-wishlist"5
6type DeleteWishlistItemWorkflowInput = {7  wishlist_item_id: string8  customer_id: string9}10
11export const deleteWishlistItemWorkflow = createWorkflow(12  "delete-wishlist-item",13  (input: DeleteWishlistItemWorkflowInput) => {14    const { data: wishlists } = useQueryGraphStep({15      entity: "wishlist",16      fields: ["*", "items.*"],17      filters: {18        customer_id: input.customer_id,19      },20      options: {21        throwIfKeyNotFound: true,22      },23    })24
25    validateItemInWishlistStep({26      wishlist: wishlists[0],27      wishlist_item_id: input.wishlist_item_id,28    })29
30    deleteWishlistItemStep(input)31
32    // refetch wishlist33    const { data: updatedWishlists } = useQueryGraphStep({34      entity: "wishlist",35      fields: ["*", "items.*", "items.product_variant.*"],36      filters: {37        id: wishlists[0].wishlist.id,38      },39    }).config({ name: "refetch-wishlist" })40
41    return new WorkflowResponse({42      wishlist: updatedWishlists[0],43    })44  }45)

You create a deleteWishlistItemWorkflow. In the workflow, you:

  • Use the useQueryGraphStep to retrieve the wishlist of a customer. Notice that you pass the link definition between a wishlist and a customer as an entry point to Query. This allows you to filter the wishlist by the customer ID.
  • Use the validateItemInWishlistStep step to validate that the item to remove is in the customer's wishlist.
  • Use the deleteWishlistItemStep step to delete the item from the wishlist.
  • Use the useQueryGraphStep again to retrieve the wishlist with the item removed.

You return the wishlist without the removed item.

Remove Item from Wishlist API Route#

You'll now create an API route that executes the deleteWishlistItemWorkflow to remove an item from a wishlist.

Create the file src/api/store/customers/me/wishlists/items/[id]/route.ts with the following content:

Directory structure after adding the route file

src/api/store/customers/me/wishlists/items/[id]/route.ts
1import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework"2import { deleteWishlistItemWorkflow } from "../../../../../../../workflows/delete-wishlist-item"3
4export async function DELETE(5  req: AuthenticatedMedusaRequest,6  res: MedusaResponse7) {8  const { result } = await deleteWishlistItemWorkflow(req.scope)9    .run({10      input: {11        wishlist_item_id: req.params.id,12        customer_id: req.auth_context.actor_id,13      },14    })15
16  res.json({17    wishlist: result.wishlist,18  })19}

This route exposes a DELETE endpoint at /store/customers/me/wishlists/items/:id. The :id parameter in the route path represents the ID of the wishlist item to remove.

In the route handler function, you execute the deleteWishlistItemWorkflow workflow, passing the authenticated customer ID and the wishlist item ID as input. You return in the response the updated wishlist.

Test API Route#

Start the Medusa application to test out the API route.

NoteMake sure that npx medusa plugin:develop is running in the plugin project to publish the changes to the local registry.

Retrieve Wishlist Item ID

To retrieve an ID of a wishlist item to remove, send a GET request to the /store/customers/me/wishlists API route:

Code
1curl 'localhost:9000/store/customers/me/wishlists' \2--header 'x-publishable-api-key: {api_key}' \3--header 'Authorization: Bearer {token}'

Make sure to replace:

  • {api_key} with the publishable API key you copied from the settings, as explained in a previous section.
  • {token} with the authenticated customer token, as explained in a previous section.

The response will contain the wishlist of the authenticated customer. You can use the id of an item in the wishlist to remove.

Remove Item from Wishlist

Then, send a DELETE request to the /store/customers/me/wishlists/items/:id API route to remove the item from the wishlist:

Code
1curl -X DELETE 'localhost:9000/store/customers/me/wishlists/items/{item_id}' \2--header 'x-publishable-api-key: {api_key}' \3--header 'Authorization: Bearer {token}'

Make sure to replace:

  • {api_key} with the publishable API key you copied from the settings, as explained in a previous section.
  • {token} with the authenticated customer token, as explained in a previous section.
  • {item_id} with the ID of the item you retrieved from the /store/customers/me/wishlists API route.

You'll receive in the response the updated wishlist without the removed item.


Step 11: Share Wishlist API Route#

In this step, you'll add the functionality to allow customers to share their wishlist with others. The route will return a token that can be passed to another API route that you'll create in the next step to retrieve the shared wishlist.

To create the token and decode it later, you'll use the jsonwebtoken package. So, run the following command in the plugin project to install the package:

Then, to create the API route, create the file src/api/store/customers/me/wishlists/share/route.ts with the following content:

Directory structure after adding the route file

src/api/store/customers/me/wishlists/share/route.ts
1import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework"2import { MedusaError } from "@medusajs/framework/utils"3import jwt from "jsonwebtoken"4
5export async function POST(6  req: AuthenticatedMedusaRequest,7  res: MedusaResponse8) {9  if (!req.publishable_key_context?.sales_channel_ids.length) {10    throw new MedusaError(11      MedusaError.Types.INVALID_DATA,12      "At least one sales channel ID is required to be associated with the publishable API key in the request header."13    )14  }15
16  const query = req.scope.resolve("query")17
18  const { data } = await query.graph({19    entity: "wishlist",20    fields: ["*"],21    filters: {22      customer_id: req.auth_context.actor_id,23    },24  })25
26  if (!data.length) {27    throw new MedusaError(28      MedusaError.Types.NOT_FOUND,29      "No wishlist found for customer"30    )31  }32
33  if (data[0].sales_channel_id !== req.publishable_key_context.sales_channel_ids[0]) {34    throw new MedusaError(35      MedusaError.Types.INVALID_DATA,36      "Wishlist does not belong to the specified sales channel"37    )38  }39
40  // TODO generate the token41}

This route exposes a POST endpoint at /store/customers/me/wishlists/share. In the route handler function, you use Query to retrieve the wishlist of the authenticated customer. If the customer doesn't have a wishlist, or the wishlist doesn't belong to the sales channel specified in the request's publishable API key, you throw an error.

You'll now generate a token that contains the wishlist ID. To do this, replace the TODO in the route handler function with the following:

src/api/store/customers/me/wishlists/share/route.ts
1const { http } = req.scope.resolve("configModule").projectConfig2
3const wishlistToken = jwt.sign({4  wishlist_id: data[0].id,5}, http.jwtSecret!, {6  expiresIn: http.jwtExpiresIn,7})8
9return res.json({10  token: wishlistToken,11})

You first retrieve the http Medusa configuration which holds configurations related to JWT secrets and expiration times. You then use the jsonwebtoken package to sign a token containing the wishlist ID. You return the token in the response.

Test API Route#

Start the Medusa application to test out the API route.

NoteMake sure that npx medusa plugin:develop is running in the plugin project to publish the changes to the local registry.

Then, send a POST request to the /store/customers/me/wishlists/share API route to generate a share token for the authenticated customer's wishlist:

Code
1curl -X POST 'localhost:9000/store/customers/me/wishlists/share' \2--header 'x-publishable-api-key: {api_key}' \3--header 'Authorization: Bearer {token}'

Make sure to replace:

  • {api_key} with the publishable API key you copied from the settings, as explained in a previous section.
  • {token} with the authenticated customer token, as explained in a previous section.

You'll receive in the response a token that you can pass to the next API route to retrieve the shared wishlist.


Step 12: Retrieve Shared Wishlist API Route#

In this step, you'll add an API route that retrieves a wishlist shared using a token returned by the /store/customers/me/wishlists/share API route.

Create the file src/api/store/wishlists/[token]/route.ts with the following content:

Directory structure after adding the route file

src/api/store/wishlists/[token]/route.ts
1import { MedusaResponse, MedusaStoreRequest } from "@medusajs/framework"2import { MedusaError } from "@medusajs/framework/utils"3import { decode, JwtPayload } from "jsonwebtoken"4
5export async function GET(6  req: MedusaStoreRequest,7  res: MedusaResponse8) {9  if (!req.publishable_key_context?.sales_channel_ids.length) {10    throw new MedusaError(11      MedusaError.Types.INVALID_DATA,12      "At least one sales channel ID is required to be associated with the publishable API key in the request header."13    )14  }15  16  const decodedToken = decode(req.params.token) as JwtPayload17
18  if (!decodedToken.wishlist_id) {19    throw new MedusaError(20      MedusaError.Types.INVALID_DATA,21      "Invalid token"22    )23  }24
25  const query = req.scope.resolve("query")26
27  const { data } = await query.graph({28    entity: "wishlist",29    fields: ["*", "items.*", "items.product_variant.*"],30    filters: {31      id: decodedToken.wishlist_id,32    },33  })34
35  if (!data.length) {36    throw new MedusaError(37      MedusaError.Types.NOT_FOUND,38      "No wishlist found"39    )40  }41
42  if (data[0].sales_channel_id !== req.publishable_key_context.sales_channel_ids[0]) {43    throw new MedusaError(44      MedusaError.Types.INVALID_DATA,45      "Wishlist does not belong to the request's sales channel"46    )47  }48
49  res.json({50    wishlist: data[0],51  })52}

This route exposes a GET endpoint at /store/wishlists/:token. The :token parameter in the route path represents the token generated by the /store/customers/me/wishlists/share API route.

In the route handler function, you decode the token to retrieve the wishlist ID. If the token is invalid, you throw an error.

Then, you use Query to retrieve the wishlist with the ID from the decoded token. If no wishlist is found or the wishlist doesn't belong to the sales channel ID of the current request, you throw an error.

You return in the response the shared wishlist.

Test API Route#

Start the Medusa application to test out the API route.

NoteMake sure that npx medusa plugin:develop is running in the plugin project to publish the changes to the local registry.

Then, send a GET request to the /store/wishlists/:token API route to retrieve the shared wishlist:

Code
1curl 'localhost:9000/store/wishlists/{wishlist_token}' \2--header 'x-publishable-api-key: {api_key}'

Make sure to replace:

  • {wishlist_token} with the token you received from the /store/customers/me/wishlists/share API route.
  • {api_key} with the publishable API key you copied from the settings, as explained in a previous section.

You'll receive in the response the shared wishlist.


Step 13: Show Wishlist Count in Medusa Admin#

In this step, you'll customize the Medusa Admin dashboard to show for each product the number of wishlists it's in.

The Medusa Admin dashboard's pages are customizable to insert widgets of custom content in pre-defined injection zones. You create these widgets as React components that allow admin users to perform custom actions.

NoteLearn more about widgets in this documentation.

Add Method to Retrieve Wishlist Count#

To retrieve the number of wishlists a product is in, you'll add a method to the WishlistModuleService that runs a query to retrieve distinct wishlist IDs containing a product variant.

In src/modules/wishlist/service.ts, add the following imports and method:

src/modules/wishlist/service.ts
1// other imports...2import { InjectManager } from "@medusajs/framework/utils"3import { Context } from "@medusajs/framework/types"4import { EntityManager } from "@mikro-orm/knex"5
6export default class WishlistModuleService extends MedusaService({7  Wishlist,8  WishlistItem,9}) {10  @InjectManager()11  async getWishlistsOfVariants(12    variantIds: string[],13    @MedusaContext() context: Context<EntityManager> = {}14  ): Promise<number> {15    return (await context.manager?.createQueryBuilder("wishlist_item", "wi")16      .select(["wi.wishlist_id"], true)17      .where("wi.product_variant_id IN (?)", [variantIds])18      .execute())?.length || 019  }20}

To perform queries on the database in a method, add the @InjectManager decorator to the method. This will inject a forked MikroORM entity manager that you can use in your method.

Methods with the @InjectManager decorator accept as a last parameter a context object that has the @MedusaContext decorator. The entity manager is injected into the manager property of this paramter.

The method accepts an array of variant IDs as a parameter. In the method, you use the createQueryBuilder to construct a query, passing it the name of the WishlistItem's table. You then select distinct wishlist_ids where the product_variant_id of the wishlist item is in the array of variant IDs.

You execute the query and return the number of distinct wishlist IDs containing the product variants. You'll use this method next.

Create Wishlist Count API Route#

Before creating the widget, you'll create the API route that retrieves the number of wishlists a product is in.

Create the file src/api/store/products/[id]/wishlist/route.ts with the following content:

Directory structure after adding the route file

src/api/store/products/[id]/wishlist/route.ts
1import { MedusaRequest, MedusaResponse } from "@medusajs/framework"2import WishlistModuleService from "../../../../../modules/wishlist/service"3import { WISHLIST_MODULE } from "../../../../../modules/wishlist"4import { MedusaError } from "@medusajs/framework/utils"5
6export async function GET(7  req: MedusaRequest,8  res: MedusaResponse9) {10  const { id } = req.params11
12  const query = req.scope.resolve("query")13  const wishlistModuleService: WishlistModuleService = req.scope.resolve(14    WISHLIST_MODULE15  )16
17  const { data: [product] } = await query.graph({18    entity: "product",19    fields: ["variants.*"],20    filters: {21      id,22    },23  })24
25  if (!product) {26    throw new MedusaError(27      MedusaError.Types.NOT_FOUND,28      `Product with id: ${id} was not found`29    )30  }31
32  const count = await wishlistModuleService.getWishlistsOfVariants(33    product.variants.map((v) => v.id)34  )35
36  res.json({37    count,38  })39}

This route exposes a GET endpoint at /store/products/:id/wishlist. The :id parameter in the route path represents the ID of the product to retrieve the wishlist count for.

In the route handler function, you use Query to retrieve the product and its variants, and throw an error if the product doesn't exist.

Then, you resolve the WishlistModuleService from the Medusa Container and use its getWishlistsOfVariants method to retrieve the number of wishlists the product's variants are in. You return the count in the response.

You'll use this API route in the widget next.

Create Wishlist Count Widget#

You'll now create the widget that will be shown on a product's page in the Medusa Admin.

In the widget, you'll send a request to the API route you created to retrieve the wishlist count for the product. To send the request, you'll use the JS SDK, which is a JavaScript library that simplifies sending requests to Medusa's API routes.

To initialize the JS SDK, create the file src/admin/lib/sdk.ts with the following content:

Directory structure after adding the SDK file

src/admin/lib/sdk.ts
1import Medusa from "@medusajs/js-sdk"2
3export const sdk = new Medusa({4  baseUrl: "http://localhost:9000",5  debug: process.env.NODE_ENV === "development",6  auth: {7    type: "session",8  },9})

You initialize an instance of the JS SDK, which you'll use in the widget to send requests.

NoteLearn more about the JS SDK and configuring it in this documentation.

Then, to create the widget, create the file src/admin/widgets/product-widget.tsx with the following content:

Directory structure after adding the widget file

src/admin/widgets/product-widget.tsx
1import { defineWidgetConfig } from "@medusajs/admin-sdk"2import { Container, Heading, Text } from "@medusajs/ui"3import { useQuery } from "@tanstack/react-query"4import { sdk } from "../lib/sdk"5import { 6  DetailWidgetProps, 7  AdminProduct,8} from "@medusajs/framework/types"9
10type WishlistResponse = {11  count: number12}13
14const ProductWidget = ({ 15  data: product,16}: DetailWidgetProps<AdminProduct>) => {17  const { data, isLoading } = useQuery<WishlistResponse>({18    queryFn: () => sdk.client.fetch(`/admin/products/${product.id}/wishlist`),19    queryKey: [["products", product.id, "wishlist"]],20  })21
22  return (23    <Container className="divide-y p-0">24      <div className="flex items-center justify-between px-6 py-4">25        <Heading level="h2">Wishlist</Heading>26      </div>27      <Text className="px-6 py-4">28        {isLoading ? 29          "Loading..." : `This product is in ${data?.count} wishlist(s).`30        }31      </Text>32    </Container>33  )34}35
36export const config = defineWidgetConfig({37  zone: "product.details.before",38})39
40export default ProductWidget

A widget file must export a React component and a config object created with defineWidgetConfig from the Admin Extension SDK. In the config object, you specify the zone to inject the widget into in the zone property. This widget is injected into a product's page before any other sections.

TipFind all widget injection zones in this reference.

Since the widget is injected into a product's details page, it receives the product's details as a data prop. In the widget, you use Tanstack Query to benefit from features like data caching and invalidation. You use the useQuery hook to send a request to the API route you created to retrieve the wishlist count for the product.

Finally, you display the widget's content using components from Medusa UI, allowing you to align the design of your widget with the Medusa Admin's design system.

Test it Out#

To test it out, start the Medusa application.

NoteMake sure that npx medusa plugin:develop is running in the plugin project to publish the changes to the local registry.

Then:

  1. open the Medusa Admin at localhost:9000/app and log in.

  2. Click on Products in the sidebar, then choose a product from the table.

Click on the "Products" in the sidebar on the right, then choose a product from the table shown in the middle

  1. You should see the widget you created showing the number of wishlists the product is in at the top of the page.

The widget is shown at the top of the product page before other sections


Next Steps#

You've now implemented the wishlist functionality in a Medusa plugin. You can publish that plugin as explained in this documentation to NPM and install it in any Medusa application. This will allow you to re-use your plugin or share it with the community.

If you're new to Medusa, check out the main documentation, where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's Commerce Modules.

For other general guides related to deployment, storefront development, integrations, and more, check out the Development Resources.

Was this page helpful?
Edit this page
Ask Anything
FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break