TABLE OF CONTENTS
Building a Wishlist Feature in Medusa.js: A Step-by-Step Implementation
A wishlist sounds like a small feature until you actually try to add one to a headless commerce stack. On platforms like Shopify or WooCommerce, wishlists usually come from a plugin you install and forget about. Medusa.js does not work that way, and that is exactly the point. Since Medusa is a modular, API first framework, a wishlist has to be built the way you would build any other commerce capability: as a proper module with its own data model, its own service, and a clean link to the customer and product data that already exists in your store.
If you are planning a Medusa.js wishlist feature for your storefront in 2026, this guide walks through the full build, from the data model to the storefront UI, along with the guest user edge cases that most tutorials skip over.
Why a Custom Wishlist Module Makes Sense in Medusa
Medusa deliberately keeps its core commerce modules narrow. There is no built in wishlist concept because saving items for later is a business decision, not a universal commerce primitive. Some stores want wishlists tied to a logged in account only. Others want a guest wishlist that persists in a cookie and merges into the account once the shopper logs in. Building it yourself means you control exactly how that logic behaves instead of fighting a third party plugin’s assumptions.
Teams that are new to Medusa’s module system sometimes underestimate how much structure is already there to lean on. If your team needs a second set of hands on the architecture side, Askan’s Medusa.js development team has worked through this exact pattern across several storefront builds, and it is worth a look before you start from a blank module.
Step 1: Designing the Wishlist Data Model
A wishlist entry needs very little information to function well. At minimum you want a customer reference, a product reference, and a timestamp for when the item was added. Keep the model lean at first. It is much easier to extend a simple data model later than to untangle an overbuilt one.
Inside a custom module, define a Wishlist data model with an id, a customer_id field, a product_id field, and a created_at timestamp. Avoid storing denormalized product details like price or title directly in the wishlist record. Prices change, product titles get updated, and a wishlist that stores stale copies of that data will show shoppers information that no longer matches the storefront.
Step 2: Linking Wishlist Records to Customer and Product Modules
Medusa’s module isolation means your custom Wishlist module cannot reach directly into the Customer or Product modules. Instead, you define module links. A link between the Wishlist module and the Customer module lets you query a customer along with their saved wishlist items in a single request. A second link between Wishlist and Product does the same for retrieving live product data, including current price and stock status, whenever the wishlist is displayed.
The official documentation on extending data models with module links covers this pattern in detail, and the same approach applies whether you are linking to the Customer module or the Product module. Once the links exist, a single query can return a customer’s wishlist with fully current product data attached, without your module ever needing direct access to those tables.
Step 3: Building the Wishlist API Routes
With the data model and links in place, the next step is exposing store API routes. You will typically need four: adding an item, removing an item, listing a customer’s wishlist, and checking whether a specific product is already saved. Keep the add and remove routes idempotent, meaning adding the same product twice should not create duplicate rows, and removing a product that is not on the list should simply return a clean empty response instead of an error.
Authentication matters here more than it might first appear. A logged in customer’s wishlist routes should require a valid customer session, while a guest wishlist needs a separate strategy since there is no customer id to attach records to yet.
Step 4: Handling Guest Wishlists Without Losing Data
This is the part most implementations get wrong. A shopper browsing without an account still wants to save items, and forcing a signup just to use a wishlist button is a fast way to lose that shopper entirely. The cleanest pattern is to generate an anonymous identifier, store it in a cookie, and let the wishlist module accept either a customer_id or this anonymous id. When the shopper eventually logs in or registers, run a merge step that reassigns the anonymous wishlist rows to the new customer_id and removes duplicates.
Test this merge step thoroughly. Edge cases like a returning customer who already has saved items under their account, plus new items saved anonymously on a different device, are where this logic tends to break in production.
Session Based vs Account Linked Wishlists
| Approach | Best For | Trade Off |
|---|---|---|
| Session or cookie based | Guest browsing, low friction saves | Data is lost if cookies are cleared |
| Account linked | Returning, logged in shoppers | Requires signup before saving |
| Hybrid with merge on login | Most storefronts in practice | More engineering effort upfront |
Step 5: Adding the Wishlist UI to the Storefront
On the storefront side, a heart icon on the product card and product detail page is the standard pattern shoppers already expect. Keep the interaction optimistic: toggle the icon state immediately on click, fire the API call in the background, and only roll back the UI if the request actually fails. A wishlist page that lists saved items should reuse your existing product card component rather than building a second version, since that keeps pricing, stock badges, and image handling consistent across the storefront.
For the add to cart flow from the wishlist page itself, pull live variant availability at render time. Nothing frustrates a shopper more than clicking add to cart on a wishlist item that has been out of stock for two weeks.
Testing the Feature Before Launch
Run through the full lifecycle manually before shipping: add as a guest, add as a logged in customer, remove an item, log out and back in to confirm persistence, and clear cookies to confirm the guest wishlist behaves as expected when it disappears. Load test the list endpoint separately if your catalog is large, since a wishlist with fifty saved items should not trigger fifty separate product lookups. Batch the product fetch using the linked query instead.
It also helps to write a small set of automated tests around the merge logic specifically, since that is the part of the feature most likely to regress quietly when someone touches the authentication flow six months later. A unit test that simulates an anonymous wishlist with three items, then logs the same browser session into an account that already has two saved items, and checks the final merged count and contents catches most of the bugs that would otherwise surface as a support ticket from a confused customer.
Common Mistakes Teams Make With Wishlist Modules
The most frequent mistake is treating the wishlist as part of the cart module instead of its own isolated concern. Carts have expiry logic, totals, and checkout state that a wishlist does not need, and bolting wishlist behavior onto the cart module tends to create confusing edge cases around what happens when a cart expires versus when a wishlist item should simply persist indefinitely.
A second common mistake is skipping pagination on the wishlist list endpoint. Most shoppers save a handful of items, but a small number of power users on any storefront will save dozens, and an endpoint that returns everything in one unpaginated response will eventually become the slowest call on the customer account page. Build pagination in from the start even if you do not need it on day one.
The third mistake is forgetting to handle deleted or unpublished products gracefully. If a product a customer saved gets removed from the catalog, the wishlist query should not throw an error or return a broken card. Filter those records out at query time, or show a clearly marked unavailable state, rather than letting a missing product break the entire page render.
Extending the Feature Once It Ships
Once the core wishlist is live, the natural next additions are price drop notifications and back in stock alerts tied to saved items. Both of these reuse the same Wishlist to Product link you already built, since you already have a clean way to look up which customers have a given product saved. A scheduled job that checks current price against the price at the time of saving, or current stock against a threshold, can trigger a notification through Medusa’s event bus without touching the core wishlist logic at all.
Another natural extension is a shareable wishlist link, useful for gifting scenarios where a shopper wants to send their saved list to someone else. This requires a public facing read only route that does not expose the owning customer’s other account details, so keep the public view intentionally minimal rather than reusing the authenticated customer route with weaker permission checks.
Where This Fits Into the Bigger Storefront Picture
A wishlist rarely ships in isolation. Teams building this feature are often mid way through a broader Medusa.js storefront build that also includes faceted search, cart drawer redesigns, and checkout customization. Treating the wishlist module as a template for how your team approaches custom modules generally, meaning small data models, explicit links rather than direct table access, and clean API boundaries, pays off across every feature that follows it.
A wishlist is a small feature on paper, but it touches the customer module, the product module, your storefront state management, and your authentication flow all at once. Building it as a proper Medusa module rather than bolting it on as a quick storefront hack is what keeps it maintainable as the store grows and as new features, like price drop alerts on saved items, get added on top of it later.
Most popular pages
On-Call Culture Done Right: How to Reduce Alert Fatigue Without Reducing Reliability
Engineering teams in high-growth organisations face a paradox that rarely gets spoken about openly. The same monitoring and alerting systems built to protect uptime...
AI-Assisted Code Review: What Works, What Does Not, and How Teams Are Adapting
AI tools have moved quickly from experimental additions into everyday developer workflows. Code review, which has always been one of the most time-consuming parts...
Async Communication in Engineering Teams: When Fewer Meetings Produce Better Code
There is a version of the engineering day that many developers know well. The calendar is split into one-hour blocks. Stand-ups run long. Syncs...


