Home
Blog
Tutorial

How to Implement Customer Profiles in Your Next.js and Medusa Ecommerce Store

Apr 14, 2022 by

Shahed Nasser

Shahed Nasser

Showcasing how to add a customer profile to your Next.js and Medusa ecommerce store.
Medusa is an open source headless commerce platform that aims to provide developers with a great developer experience. Although it provides Next.js and Gatsby storefronts to use, you can also use any storefront of your choice.
To make your developer experience even easier, Medusa provides a client NPM package that you can use with JavaScript and Typescript frameworks.
In this tutorial, you’ll learn how to install and use the Medusa JS Client in your storefront to implement a customer sign-up and profile flow.
You can find the code for this tutorial in this GitHub repository.

Prerequisites

This tutorial assumes you already have a Medusa server installed. If not, please follow along the quickstart guide.
Furthermore, this tutorial uses the Next.js starter to implement the customer profile. However, the tutorial will focus on how to use the Medusa JS Client in particular. So, you can still follow along if you are using a different framework for your storefront.

Install the Medusa JS Client

In your storefront project’s directory, install the Medusa JS client if you don’t have it installed already:
npm install @medusajs/medusa-js
If you’re using our Next.js or Gatsby starters, then you don’t need to install it.

Initialize Client

In the Next.js starter, the client is initialized in
Copy to clipboard
utils/client.js
and you can just import it into your components. However, if you’re using your custom storefront here’s how you can initialize the Medusa client:
const client = new Medusa({ baseUrl: BACKEND_URL });
Where
Copy to clipboard
BACKEND_URL
is the URL to your Medusa server.
You can then use the methods and resources in the client to send all types of requests to your Medusa server which you’ll see later in this tutorial.

Add Styles

This step is optional and can be skipped. To add some styling for the rest of the tutorial, you can create the file
Copy to clipboard
styles/customer.module.css
with this content.

Add Customer to Context

In the Next.js starter, the
Copy to clipboard
StoreContext
holds all variables and methods important to the store like the
Copy to clipboard
cart
object.
In this section, you’ll add a variable and a method to the context:
Copy to clipboard
customer
and
Copy to clipboard
setCustomer
respectively.
In the
Copy to clipboard
defaultStoreContext
variable in
Copy to clipboard
context/store-context.js
add the following:
export const defaultStoreContext = {
...,
customer: null,
setCustomer: async () => {}
}
Then, in the
Copy to clipboard
reducer
function, add a case for the
Copy to clipboard
setCustomer
action:
case "setCustomer":
return {
...state,
customer: action.payload
}
Inside the
Copy to clipboard
StoreProvider
function, add inside the
Copy to clipboard
useEffect
under the cart initialization the following to retrieve the customer if they are logged in and set it in the context:
//try to retrieve customer
client.auth.getSession()
.then(({customer}) => {
setCustomer(customer)
})
.catch((e) => setCustomer(null))
Notice here that you’re using
Copy to clipboard
client.auth.getSession
.
Copy to clipboard
client
has been initialized earlier in the file, so if you’re implementing this in your own storefront you’d need to initialize it before this code snippet.
Then, you use
Copy to clipboard
auth.getSession
which allows you to retrieve the current logged in customer, if there is any.
If the customer is logged in and is returned in the response, you set it in the context, otherwise you set the customer to
Copy to clipboard
null
.
Then, add a new function inside
Copy to clipboard
StoreProvider
to handle setting the customer:
const setCustomer = (customer) => {
dispatch({type: 'setCustomer', payload: customer})
}
This will just dispatch the action to the reducer which will set the customer object in the context.
Finally, add the
Copy to clipboard
setCustomer
function to the
Copy to clipboard
value
prop of
Copy to clipboard
StoreContext.Provider
returned in
Copy to clipboard
StoreProvider
:
return (
<StoreContext.Provider
value={{
...,
setCustomer
}}
>
{children}
</StoreContext.Provider>
)

Create Sign Up and Sign In Pages

In this section, you’ll create the sign up and sign in pages to allow the customer to either create an account or log in with an existing one.

Sign Up Page

Create the file
Copy to clipboard
pages/sign-up.js
with the following content:
import * as Yup from 'yup';
import { useContext, useEffect, useRef } from 'react';
import StoreContext from '../context/store-context';
import { createClient } from "../utils/client"
import styles from "../styles/customer.module.css";
import { useFormik } from "formik";
import { useRouter } from 'next/router';
export default function SignUp() {
const router = useRouter();
const { setCustomer, customer } = useContext(StoreContext)
useEffect(() => {
if (customer) {
router.push("/")
}
}, [customer, router])
const buttonRef = useRef();
const { handleSubmit, handleBlur, handleChange, values, touched, errors } = useFormik({
initialValues: {
email: '',
first_name: '',
last_name: '',
password: ''
},
validationSchema: Yup.object().shape({
email: Yup.string().email().required(),
first_name: Yup.string().required(),
last_name: Yup.string().required(),
password: Yup.string().required()
}),
onSubmit: function (values) {
if (buttonRef.current) {
buttonRef.current.disabled = true;
}
const client = createClient()
client.customers.create({
email: values.email,
first_name: values.first_name,
last_name: values.last_name,
password: values.password
}).then(({ customer }) => {
setCustomer(customer);
})
}
})
return (
<div className={styles.container}>
<main className={styles.main}>
<form onSubmit={handleSubmit} className={styles.signForm}>
<h1>Sign Up</h1>
<div className={styles.inputContainer}>
<label htmlFor="email">Email</label>
<input type="email" name="email" id="email" className={styles.input}
onChange={handleChange} onBlur={handleBlur} value={values.email} />
{errors.email && touched.email && <span className={styles.error}>{errors.email}</span>}
</div>
<div className={styles.inputContainer}>
<label htmlFor="first_name">First Name</label>
<input type="text" name="first_name" id="first_name" className={styles.input}
onChange={handleChange} onBlur={handleBlur} value={values.first_name} />
{errors.first_name && touched.first_name && <span className={styles.error}>{errors.first_name}</span>}
</div>
<div className={styles.inputContainer}>
<label htmlFor="last_name">Last Name</label>
<input type="text" name="last_name" id="last_name" className={styles.input}
onChange={handleChange} onBlur={handleBlur} value={values.last_name} />
{errors.last_name && touched.last_name && <span className={styles.error}>{errors.last_name}</span>}
</div>
<div className={styles.inputContainer}>
<label htmlFor="password">Password</label>
<input type="password" name="password" id="password" className={styles.input}
onChange={handleChange} onBlur={handleBlur} value={values.password} />
{errors.password && touched.password && <span className={styles.error}>{errors.password}</span>}
</div>
<div className={styles.inputContainer}>
<button type="submit" className={styles.btn} ref={buttonRef}>Sign Up</button>
</div>
</form>
</main>
</div>
)
}
In this page, you use Formik and Yup to create and validate the form. This form has 4 fields: email, first name, last name, and password. These fields are required to sign up a customer in Medusa.
The important bit here is the part that uses the Medusa client in the
Copy to clipboard
onSubmit
function passed to
Copy to clipboard
useFormik
:
const client = createClient()
client.customers.create({
email: values.email,
first_name: values.first_name,
last_name: values.last_name,
password: values.password
}).then(({ customer }) => {
setCustomer(customer);
})
You first initialize the Medusa client. You use the utility function in
Copy to clipboard
utils/client.js
to do that, but if you don’t have that in your storefront you can replace it with the initialization mentioned earlier in the tutorial.
Then, you use
Copy to clipboard
client.customers.create
which will send a request to the create customer endpoint. This endpoint requires the
Copy to clipboard
email
,
Copy to clipboard
first_name
,
Copy to clipboard
last_name
, and
Copy to clipboard
password
fields.
If the sign up is successful, the
Copy to clipboard
customer
object will be returned and a session token will be saved in the cookies to keep the user logged in. You use the
Copy to clipboard
customer
object to set the customer in the context.
Notice also that at the beginning of the component you check if the customer is already logged in and redirect them to the home page in that case:
useEffect(() => {
if (customer) {
router.push("/")
}
}, [customer, router])

Sign In Page

Next, create the file
Copy to clipboard
pages/sign-in.js
with the following content:
import * as Yup from 'yup';
import { useContext, useEffect, useRef } from 'react';
import StoreContext from '../context/store-context';
import { createClient } from "../utils/client"
import styles from "../styles/customer.module.css";
import { useFormik } from "formik";
import { useRouter } from 'next/router';
export default function SignIn() {
const router = useRouter();
const { setCustomer, customer } = useContext(StoreContext)
useEffect(() => {
if (customer) {
router.push("/")
}
}, [customer, router])
const buttonRef = useRef();
const { handleSubmit, handleBlur, handleChange, values, touched, errors } = useFormik({
initialValues: {
email: '',
password: ''
},
validationSchema: Yup.object().shape({
email: Yup.string().email().required(),
password: Yup.string().required()
}),
onSubmit: function (values) {
if (buttonRef.current) {
buttonRef.current.disabled = true;
}
const client = createClient()
client.auth.authenticate({
email: values.email,
password: values.password
}).then(({ customer }) => {
setCustomer(customer);
})
}
})
return (
<div className={styles.container}>
<main className={styles.main}>
<form onSubmit={handleSubmit} className={styles.signForm}>
<h1>Sign In</h1>
<div className={styles.inputContainer}>
<label htmlFor="email">Email</label>
<input type="email" name="email" id="email" className={styles.input}
onChange={handleChange} onBlur={handleBlur} value={values.email} />
{errors.email && touched.email && <span className={styles.error}>{errors.email}</span>}
</div>
<div className={styles.inputContainer}>
<label htmlFor="password">Password</label>
<input type="password" name="password" id="password" className={styles.input}
onChange={handleChange} onBlur={handleBlur} value={values.password} />
{errors.password && touched.password && <span className={styles.error}>{errors.password}</span>}
</div>
<div className={styles.inputContainer}>
<button type="submit" className={styles.btn} ref={buttonRef}>Sign In</button>
</div>
</form>
</main>
</div>
)
}
This page also makes use of Formik and Yup to create and validate the form. This form only needs an email and a password.
The important bit is in the
Copy to clipboard
onSubmit
function passed to
Copy to clipboard
useFormik
:
const client = createClient()
client.auth.authenticate({
email: values.email,
password: values.password
}).then(({ customer }) => {
setCustomer(customer);
})
Just like before, you start by initializing the Medusa client. Then, you authenticate the customer using
Copy to clipboard
client.auth.authenticate
which calls the Authenticate Customer endpoint. This endpoint requires 2 parameters:
Copy to clipboard
email
and
Copy to clipboard
password
.
If successful, a customer object is returned which you use to set the customer in the store context using
Copy to clipboard
setCustomer
. A cookie will also be set to maintain the login session for the customer.
Finally, in
Copy to clipboard
components/layout/nav-bar.jsx
, initialize the customer from the
Copy to clipboard
StoreContext
:
const { cart, customer } = useContext(StoreContext)
Then, in the returned JSX add the following to add a link to the new pages:
{!customer && <Link href="/sign-up">Sign Up</Link>}
{!customer && <Link href="/sign-in">Sign In</Link>}
{customer && <Link href="/customer">Profile</Link>}
Notice that you also add a link to the customer profile, which you’ll implement in the next section.

Test it Out

Make sure that the Medusa server is running first. Then, run the server for your storefront:
npm run dev
If you open
Copy to clipboard
localhost:8000
now, you’ll see that there are 2 new links in the navigation bar for sign up and sign in.
You can try clicking on Sign Up and registering as a new user.
Or click Sign In and log in as an existing user.
Once you’re logged in, you should be redirected back to the home page and you should see a Profile link in the navigation bar.

Add Customer Profile

The customer profile will have 3 pages: Edit customer info, view orders, and view addresses.

Create Profile Layout

You’ll start by creating a layout that all profile pages will use.
Create
Copy to clipboard
components/layout/profile.jsx
with the following content:
import { useContext, useEffect } from 'react';
import Link from 'next/link';
import StoreContext from '../../context/store-context';
import styles from "../../styles/customer.module.css";
import { useRouter } from 'next/router';
export default function Profile ({children, activeLink}) {
const router = useRouter()
const { customer } = useContext(StoreContext)
useEffect(() => {
if (!customer) {
router.push('/')
}
}, [customer, router])
return (
<div className={styles.container}>
<main className={styles.main}>
<div className={styles.profile}>
<div className={styles.profileSidebar}>
<Link href="/customer">
<a className={activeLink === 'customer' ? styles.active : ''}>Edit Profile</a>
</Link>
<Link href="/customer/orders">
<a className={activeLink === 'orders' ? styles.active : ''}>Orders</a>
</Link>
<Link href="/customer/addresses">
<a className={activeLink === 'addresses' ? styles.active : ''}>Addresses</a>
</Link>
</div>
<div className={styles.profileContent}>
{children}
</div>
</div>
</main>
</div>
)
}
This first checks the customer and context to see if the user is logged in. Then, it displays a sidebar with 3 links, and the main content displays the children.

Create Edit Profile Page

The edit profile page will be the main customer profile page. Create the file
Copy to clipboard
pages/customer/index.js
with the following content:
import * as Yup from 'yup';
import { useContext, useRef } from 'react';
import Profile from '../../components/layout/profile';
import StoreContext from "../../context/store-context";
import { createClient } from "../../utils/client"
import styles from "../../styles/customer.module.css";
import { useFormik } from 'formik';
export default function CustomerIndex() {
const { customer, setCustomer } = useContext(StoreContext)
const buttonRef = useRef()
const { handleSubmit, handleChange, handleBlur, values, errors, touched } = useFormik({
initialValues: {
email: customer?.email,
first_name: customer?.first_name,
last_name: customer?.last_name,
password: ''
},
validationSchema: Yup.object().shape({
email: Yup.string().email().required(),
first_name: Yup.string().required(),
last_name: Yup.string().required(),
password: Yup.string()
}),
onSubmit: (values) => {
buttonRef.current.disabled = true;
const client = createClient()
if (!values.password) {
delete values['password']
}
client.customers.update(values)
.then(({ customer }) => {
setCustomer(customer)
alert("Account updated successfully")
buttonRef.current.disabled = false;
})
}
})
return (
<Profile activeLink='customer'>
<form onSubmit={handleSubmit}>
<h1>Edit Profile</h1>
<div className={styles.inputContainer}>
<label htmlFor="email">Email</label>
<input type="email" name="email" id="email" className={styles.input}
onChange={handleChange} onBlur={handleBlur} value={values.email} />
{errors.email && touched.email && <span className={styles.error}>{errors.email}</span>}
</div>
<div className={styles.inputContainer}>
<label htmlFor="first_name">First Name</label>
<input type="text" name="first_name" id="first_name" className={styles.input}
onChange={handleChange} onBlur={handleBlur} value={values.first_name} />
{errors.first_name && touched.first_name && <span className={styles.error}>{errors.first_name}</span>}
</div>
<div className={styles.inputContainer}>
<label htmlFor="last_name">Last Name</label>
<input type="text" name="last_name" id="last_name" className={styles.input}
onChange={handleChange} onBlur={handleBlur} value={values.last_name} />
{errors.last_name && touched.last_name && <span className={styles.error}>{errors.last_name}</span>}
</div>
<div className={styles.inputContainer}>
<label htmlFor="password">Password</label>
<input type="password" name="password" id="password" className={styles.input}
onChange={handleChange} onBlur={handleBlur} value={values.password} />
{errors.password && touched.password && <span className={styles.error}>{errors.password}</span>}
</div>
<div className={styles.inputContainer}>
<button type="submit" className={styles.btn} ref={buttonRef}>Save</button>
</div>
</form>
</Profile>
)
}
This is very similar to the sign-up form. You use Formik and Yup to create and validate the form. The form has 4 inputs:
Copy to clipboard
email
,
Copy to clipboard
first_name
,
Copy to clipboard
last_name
, and
Copy to clipboard
password
which is optional.
The important bit here is the part in the
Copy to clipboard
onSubmit
function passed to
Copy to clipboard
useFormik
:
const client = createClient()
if (!values.password) {
delete values['password']
}
client.customers.update(values)
.then(({ customer }) => {
setCustomer(customer)
alert("Account updated successfully")
buttonRef.current.disabled = false;
})
Just like you’ve done before, you start by initializing the Medusa client. Then, if the password is not set, you remove it from the list of values since you’ll be passing it to the server as-is. You should only pass the password if the customer wants to change it.
To update the customer info, you can use
Copy to clipboard
client.customers.update
which sends a request to the Update Customer endpoint. This endpoint accepts a few optional parameters including
Copy to clipboard
email
,
Copy to clipboard
last_name
,
Copy to clipboard
first_name
, and
Copy to clipboard
password
.
If the customer info is updated successfully, a customer object is returned which you use to set the updated customer in the context. You also show an alert to the customer that their account is now updated.
If you click on the Profile link in the navigation bar now, you’ll see the profile page with the sidebar and the edit profile page as the first page.
Try updating any of the information and click Save. You should then see an alert to let you know that it’s been updated successfully.

Orders Page

The next page you’ll create is an orders page which will display the customer’s orders.
Create a new file
Copy to clipboard
pages/customer/orders.js
with the following content:
import { useEffect, useState } from 'react';
import Link from 'next/link';
import Profile from '../../components/layout/profile';
import { createClient } from "../../utils/client"
import { formatMoneyAmount } from '../../utils/prices';
import styles from "../../styles/customer.module.css";
import { useRouter } from 'next/router';
export default function Orders () {
const [orders, setOrders] = useState([])
const [pages, setPages] = useState(0)
const router = useRouter()
const p = router.query.p ? parseInt(router.query.p - 1) : 0
useEffect(() => {
const client = createClient()
client.customers.listOrders({
limit: 20,
offset: 20 * p
}).then((result) => {
setOrders(result.orders)
setPages(Math.ceil(result.count / result.limit))
})
}, [p])
return (
<Profile activeLink='orders'>
<h1>Orders</h1>
<table className={styles.table}>
<thead>
<tr>
<th>ID</th>
<th>Total</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{orders.map((order) => (
<tr key={order.id}>
<td>{order.id}</td>
<td>{formatMoneyAmount({
currencyCode: order.currency_code,
amount: order.total
}, 2)}</td>
<td>{order.status}</td>
</tr>
))}
</tbody>
</table>
<div className={styles.pagination}>
{pages > 0 && p > 1 && (
<Link href={`/customer/orders?p=${p - 1}`}>Previous</Link>
)}
{pages > 1 && p > 0 && p < pages && <span> - </span>}
{pages > 1 && (p + 1) < pages && (
<Link href={`/customer/orders?p=${p + 1}`}>Next</Link>
)}
</div>
</Profile>
)
}
You first create an
Copy to clipboard
orders
state variable which starts out as empty. You also have a
Copy to clipboard
pages
state variable to keep track of the number of pages available. Then, in
Copy to clipboard
useEffect
, you retrieve the orders using the Medusa client:
useEffect(() => {
const client = createClient()
client.customers.listOrders({
limit: 20,
offset: 20 * p
}).then((result) => {
setOrders(result.orders)
setPages(Math.ceil(result.count / result.limit))
})
}, [p])
After initializing the client, you retrieve the orders of the customer using
Copy to clipboard
client.customers.listOrders
. This method sends a request to the Retrieve Customer Orders endpoint. This endpoint accepts 4 fields:
Copy to clipboard
limit
,
Copy to clipboard
offset
,
Copy to clipboard
fields
, and
Copy to clipboard
expand
. Here, you just use
Copy to clipboard
limit
and
Copy to clipboard
offset
.
Copy to clipboard
limit
indicates how many orders should be retrieved per page, and
Copy to clipboard
offset
indicates how many orders to skip from the beginning to get the orders of the current page.
This request returns the list of orders as well as additional fields important for pagination including
Copy to clipboard
count
which indicates the total count of the orders and
Copy to clipboard
limit
which is the current limit set.
You set the
Copy to clipboard
orders
state variable to the orders returned from the method and you set the number of pages based on the
Copy to clipboard
count
and
Copy to clipboard
limit
fields.
Finally, you display the
Copy to clipboard
orders
in a table showing the ID, total, and status. You also show pagination links for “Previous” and “Next” if applicable for the page. This is done for the simplicity of the tutorial.
If you open the orders page now, you’ll see the list of orders for your customer if they have any.

Addresses Page

The last page you’ll create in the profile is the Addresses page which will allow the customer to see their shipping addresses.
Create the file
Copy to clipboard
pages/customer/addresses.js
with the following content:
import Profile from "../../components/layout/profile"
import StoreContext from "../../context/store-context"
import styles from "../../styles/customer.module.css"
import { useContext } from 'react'
export default function Addresses() {
const { customer } = useContext(StoreContext)
return (
<Profile activeLink='addresses'>
<h1>Addresses</h1>
{customer && customer.shipping_addresses.length === 0 && <p>You do not have any addresses</p>}
{customer && customer.shipping_addresses.map((address) => (
<div key={address.id} className={styles.address}>
<span><b>First Name:</b> {address.first_name}</span>
<span><b>Last Name:</b> {address.last_name}</span>
<span><b>Company:</b> {address.company}</span>
<span><b>Address Line 1:</b> {address.address_1}</span>
<span><b>Address Line 2:</b> {address.address_2}</span>
<span><b>City:</b> {address.city}</span>
<span><b>Country:</b> {address.country}</span>
</div>
))}
</Profile>
)
}
You use the
Copy to clipboard
shipping_addresses
field in the
Copy to clipboard
customer
object stored in the context and display the addresses one after the other. You can also access the billing address if the customer has any using
Copy to clipboard
customer.billing_address
.
If you go to the Addresses page now, you’ll see the customer’s shipping addresses listed.

What’s Next

Using the Medusa JS Client, you can easily interact with the APIs to create an even better customer experience in your storefront.
This tutorial did not cover all the aspects of a profile for simplicity. Here’s what else you can implement:
  1. View Single Orders: You can use the orders object you already retrieved on the orders page to show the information of a single order or you can use the Retrieve Order endpoint which is accessible in the client under
    Copy to clipboard
    client.orders.retrieve
    .
  2. Address Management: You can use the Update Shipping Address endpoint accessible in the client under
    Copy to clipboard
    client.customers.addresses.updateAddress
    ; you can use the Add Shipping Address endpoint which is accessible in the client under
    Copy to clipboard
    client.customers.addresses.addAddress
    ; and you can use the Delete Shipping Address endpoint which is accessible in the client under
    Copy to clipboard
    client.customers.addresses.deleteAddress
    .
  3. Saved Payment Methods: You can retrieve the saved payment methods if you use a payment provider that supports saving payment methods like Stripe using the Retrieve Saved Payment Methods endpoint accessible in the client under
    Copy to clipboard
    client.customers.paymentMethods.list
    .
  4. Reset Password: You can add reset password functionality using the Create Reset Password Token endpoint accessible in the client under
    Copy to clipboard
    client.customers.generatePasswordToken
    , then you can reset the password using the Reset Customer Password endpoint accessible in the client under
    Copy to clipboard
    client.customers.resetPassword
    .
You can check out the API documentation for a full list of endpoints to see what more functionalities you can add to your storefront.
Should you have any issues or questions related to Medusa, then feel free to reach out to the Medusa team via Discord.

Share this post