How to Show Products on the Storefront
In this document, you’ll learn how to show products in your storefront using the Store REST APIs.
Overview
Using the products store REST APIs, you can display products on your storefront along with their different details.
Scenario
You want to add or use the following storefront functionalities:
- List products with filters.
- Display product prices.
- Search products.
- Retrieve details of a single product by ID or by handle.
Prerequisites
Medusa Components
It's assumed that you already have a Medusa backend installed and set up. If not, you can follow the quickstart guide to get started.
It's also assumed you already have a storefront set up. It can be a custom storefront or one of Medusa’s storefronts. If you don’t have a storefront set up, you can install the Next.js Starter Template.
JS Client
This guide includes code snippets to send requests to your Medusa backend using Medusa’s JS Client, among other methods.
If you follow the JS Client code blocks, it’s assumed you already have Medusa’s JS Client installed and have created an instance of the client.
Medusa React
This guide also includes code snippets to send requests to your Medusa backend using Medusa React, among other methods.
If you follow the Medusa React code blocks, it's assumed you already have Medusa React installed and have used MedusaProvider higher in your component tree.
@medusajs/product Module
This guide also includes code snippets to utilize the @medusajs/product
Copy to Clipboard module in your storefront, among other methods.
If you follow the @medusajs/product
Copy to Clipboard code blocks, it's assumed you already have the @medusajs/product installed.
List Products
You can list available products using the List Products endpoint:
import { useProducts } from "medusa-react"
import { Product } from "@medusajs/medusa"
const Products = () => {
const { products, isLoading } = useProducts()
return (
<div>
{isLoading && <span>Loading...</span>}
{products && !products.length && <span>No Products</span>}
{products && products.length > 0 && (
<ul>
{products.map((product: Product) => (
<li key={product.id}>{product.title}</li>
))}
</ul>
)}
</div>
)
}
export default Products
import {
initialize as initializeProductModule,
} from "@medusajs/product"
// in an async function, or you can use promises
async () => {
// ...
const productService = await initializeProductModule()
const products = await productService.list()
console.log(products.length)
}
This endpoint does not require any parameters. You can pass it parameters related to pagination, filtering, and more as explained in the API reference.
The request returns an array of product objects along with pagination parameters.
Filtering Retrieved Products
The List Products endpoint accepts different query parameters that allow you to filter through retrieved results.
For example, you can filter products by a category ID:
import { useProducts } from "medusa-react"
import { Product } from "@medusajs/medusa"
const Products = () => {
const { products, isLoading } = useProducts({
category_id: ["cat_123"],
})
return (
<div>
{isLoading && <span>Loading...</span>}
{products && !products.length && <span>No Products</span>}
{products && products.length > 0 && (
<ul>
{products.map((product: Product) => (
<li key={product.id}>{product.title}</li>
))}
</ul>
)}
</div>
)
}
export default Products
import {
initialize as initializeProductModule,
} from "@medusajs/product"
// in an async function, or you can use promises
async () => {
// ...
const productService = await initializeProductModule()
const products = await productService.list({
category_ids: ["cat_123"],
})
console.log(products)
}
This will retrieve only products that belong to that category.
Expand Categories
To expand the categories of each product, you can pass categories
Copy to Clipboard to the expand
Copy to Clipboard query parameter:
import { useProducts } from "medusa-react"
import { Product } from "@medusajs/medusa"
const Products = () => {
const { products, isLoading } = useProducts({
expand: "categories",
})
return (
<div>
{isLoading && <span>Loading...</span>}
{products && !products.length && <span>No Products</span>}
{products && products.length > 0 && (
<ul>
{products.map((product: Product) => (
<li key={product.id}>{product.title}</li>
))}
</ul>
)}
</div>
)
}
export default Products
import {
initialize as initializeProductModule,
} from "@medusajs/product"
// in an async function, or you can use promises
async () => {
// ...
const productService = await initializeProductModule()
const products = await productService.list({}, {
relations: ["categories"],
})
console.log(products)
}
You can learn more about the expand parameter in the API reference
Product Pricing Parameters
By default, the prices are retrieved based on the default currency associated with a store. You can use the following query parameters to ensure you are retrieving correct pricing based on the customer’s context:
region_id
Copy to Clipboard: The ID of the customer’s region.cart_id
Copy to Clipboard: The ID of the customer’s cart.currency_code
Copy to Clipboard: The code of the currency to retrieve prices for.
It’s recommended to always include the cart and region’s IDs when you’re listing or retrieving a single product’s details, as it’ll show you the correct pricing fields as explained in the next section.
For example:
import { useProducts } from "medusa-react"
import { Product } from "@medusajs/medusa"
const Products = () => {
const { products, isLoading } = useProducts({
cart_id,
region_id,
})
return (
<div>
{isLoading && <span>Loading...</span>}
{products && !products.length && <span>No Products</span>}
{products && products.length > 0 && (
<ul>
{products.map((product: Product) => (
<li key={product.id}>{product.title}</li>
))}
</ul>
)}
</div>
)
}
export default Products
Display Product Price
Each product object in the retrieved array has a variants
Copy to Clipboard array. Each item in the variants
Copy to Clipboard array is a product variant object.
Product prices are available for each variant in the product. Each variant has a prices
Copy to Clipboard array with all the available prices in the context. However, when displaying the variant’s price, you’ll use the following properties inside a variant object:
original_price
Copy to Clipboard: The original price of the product variant.calculated_price
Copy to Clipboard: The calculated price, which can be based on prices defined in a price list.original_tax
Copy to Clipboard: The tax amount applied to the original price, if any.calculated_tax
Copy to Clipboard: The tax amount applied to the calculated price, if any.original_price_incl_tax
Copy to Clipboard: The price after applying the tax amount on the original price.calculated_price_incl_tax
Copy to Clipboard: The price after applying the tax amount on the calculated price
Typically, you would display the calculated_price_incl_tax
Copy to Clipboard as the price of the product variant.
You must pass one of the pricing parameters to the request to retrieve these values. Otherwise, their value will be null
Copy to Clipboard.
Prices in Medusa are stored as the currency's smallest unit. So, for currencies that are not zero-decimal, the amount is stored multiplied by a 100
Copy to Clipboard. You can learn more about this in the Product conceptual guide.
So, to show the correct price, you would need to convert it to its actual price with a method like this:
To display it along with a currency, it’s recommended to use JavaScript’s Intl.NumberFormat. For example:
Ideally, you would retrieve the value of the currency
Copy to Clipboard property from the selected region’s currency_code
Copy to Clipboard attribute.
Medusa React provides utility methods such as formatVariantPrice
Copy to Clipboard that handles this logic for you.
Here’s an example of how you can calculate the price with and without Medusa React:
import React, { useEffect, useState } from "react"
import Medusa from "@medusajs/medusa-js"
const medusa = new Medusa({
baseUrl: "<YOUR_BACKEND_URL>",
maxRetries: 3,
})
function Products() {
const [products, setProducts] = useState([])
useEffect(() => {
medusa.products.list({
// TODO assuming region is already defined somewhere
region_id: region.id,
})
.then(({ products, limit, offset, count }) => {
// ignore pagination for sake of example
setProducts(products)
})
})
const convertToDecimal = (amount) => {
return Math.floor(amount) / 100
}
const formatPrice = (amount) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
// TODO assuming region is already defined somewhere
currency: region.currency_code,
}).format(convertToDecimal(amount))
}
return (
<ul>
{products.map((product) => (
<>
{product.variants.map((variant) => (
<li key={variant.id}>{
formatPrice(variant.calculated_price_incl_tax)
}</li>
))}
</>
))}
</ul>
)
}
export default Products
import { formatVariantPrice, useProducts } from "medusa-react"
import { Product, ProductVariant } from "@medusajs/medusa"
const Products = () => {
const { products, isLoading } = useProducts({
region_id: region.id, // assuming already defined somewhere
})
return (
<div>
{isLoading && <span>Loading...</span>}
{products && !products.length && (
<span>No Products</span>
)}
{products && products.length > 0 && (
<ul>
{products.map((product: Product) => (
<>
{product.variants.map(
(variant: ProductVariant) => (
<li key={variant.id}>
{formatVariantPrice({
variant,
// assuming already defined somewhere
region,
})}
</li>
))}
</>
))}
</ul>
)}
</div>
)
}
export default Products
Search Products
The Search functionality requires either installing a search plugin or creating a search service.
You can search products using the Search Products endpoint:
This endpoint requires the query parameter q
Copy to Clipboard being the term to search products for. The search plugin or service you’re using determine how q
Copy to Clipboard will be used to search the products. It also accepts pagination parameters as explained in the API reference.
The request returns a hits
Copy to Clipboard array holding the result items. The structure of the items depends on the plugin you’re using.
Retrieve a Product by ID
You can retrieve the details of a single product by its ID using the Get a Product endpoint:
import {
initialize as initializeProductModule,
} from "@medusajs/product"
// in an async function, or you can use promises
async () => {
// ...
const productService = await initializeProductModule()
const products = await productService.list({
id: productId,
})
console.log(products[0])
}
This endpoint requires the product’s ID to be passed as a path parameter. You can also pass query parameters such as cart_id
Copy to Clipboard and region_id
Copy to Clipboard which are relevant for pricing as explained in the Product Pricing Parameters section. You can check the full list of accepted parameters in the API reference.
The request returns a product object. You can display its price as explained in the Display Product Price section.
You can also retrieve the product's categories by passing the expand
Copy to Clipboard query parameter similar to the explanation in this section.
Retrieve Product by Handle
On the storefront, you may use the handle of a product as its page’s path. For example, instead of displaying the product’s details on the path /products/prod_123
Copy to Clipboard, you can display it on the path /products/shirt
Copy to Clipboard, where shirt
Copy to Clipboard is the handle of the product. This type of URL is human-readable and is good for Search Engine Optimization (SEO)
You can retrieve the details of a product by its handle by sending a request to the List Products endpoint, passing the handle
Copy to Clipboard as a filter:
import { useProducts } from "medusa-react"
const Products = () => {
const { products, isLoading } = useProducts({
handle,
})
return (
<div>
{isLoading && <span>Loading...</span>}
{products && !products.length && (
<span>Product does not exist</span>
)}
{products && products.length > 0 && products[0].title}
</div>
)
}
export default Products
import {
initialize as initializeProductModule,
} from "@medusajs/product"
// in an async function, or you can use promises
async () => {
// ...
const productService = await initializeProductModule()
const products = await productService.list({
handle,
})
console.log(products[0])
}
As the handle
Copy to Clipboard of each product is unique, when you pass the handle as a filter you’ll either:
- receive an empty
products
Copy to Clipboard array, meaning the product doesn’t exist; - or you’ll receive a
products
Copy to Clipboard array with one item being the product you’re looking for. In this case, you can access the product at index0
Copy to Clipboard.
As explained earlier, make sure to pass the product pricing parameters to display the product's price.
You can also retrieve the product's categories by passing the expand
Copy to Clipboard query parameter as explained in the Expand Categories section.