- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
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.
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:
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.
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.
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.
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.
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:
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.
Run the following command in the Medusa application's directory to install the plugin:
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:
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:
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.
Create Module Directory#
A module is created under the src/modules
directory of your plugin. So, create the directory src/modules/wishlist
.
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.
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:
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 theWishlistItem
model, representing the products in the wishlist. You'll add this data model next.
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.
Next, create the WishlistItem
model in the file 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 theWishlist
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.
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:
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
.
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:
You use the Module
function from the Modules SDK to create the module's definition. It accepts two parameters:
- The module's name, which is
wishlist
. - 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.
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:
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:
npx medusa plugin:develop
is running in the plugin project to publish the changes to the local registry.The tables of the Wishlist Module's data models are now created in the database.
Step 5: Link Wishlist Data Models with Core Models#
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.
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:
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:
- The first data model part of the link, which is the Wishlist Module's
wishlist
data model. A module has a speciallinkable
property that contain link configurations for its data models. You also specify the field that points to the customer. - The second data model part of the link, which is the Customer Module's
customer
data model. - 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:
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:
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.
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:
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:
- The step's name, which is
validate-customer-create-wishlist
. - 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.
- The input data for the step, which in this case is an object having a
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:
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.
Add createWishlistWorkflow#
You can now add the createWishlistWorkflow
to the plugin. Create the file src/workflows/create-wishlist.ts
with the following content:
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.
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.
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:
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:
- A request object with details and context about the request, such as authenticated customer details.
- 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.
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.
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:
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:
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:
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:
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:
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.
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:
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:
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.
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:
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:
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:
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:
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:
You create an object schema with a variant_id
property of type string
.
You can now replace the PostStoreCreateWishlistItemType
type in src/api/store/customers/me/wishlists/items/route.ts
with the following:
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.
To apply the middleware, create the file src/api/middlewares.ts
with the following content:
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.
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:
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:
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:
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:
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.
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:
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:
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.
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:
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:
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:
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:
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.
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:
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:
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.
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:
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.
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:
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_id
s 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:
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:
You initialize an instance of the JS SDK, which you'll use in the widget to send requests.
Then, to create the widget, create the file src/admin/widgets/product-widget.tsx
with the following content:
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.
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.
npx medusa plugin:develop
is running in the plugin project to publish the changes to the local registry.Then:
-
open the Medusa Admin at
localhost:9000/app
and log in. -
Click on Products in the sidebar, then choose a product from the table.
- You should see the widget you created showing the number of wishlists the product is in at the top of the page.
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.