4.3. Admin UI Routes

In this chapter, you’ll learn how to create a UI route in the admin dashboard.

What is a UI Route?#

The Medusa Admin dashboard is customizable, allowing you to add new pages, called UI routes. You create a UI route as a React component showing custom content that allows admin users to perform custom actions.

For example, you can add a new page to show and manage product reviews, which aren't available natively in Medusa.


How to Create a UI Route?#

You create a UI route in a page.tsx file under a sub-directory of src/admin/routes directory. The file's path relative to src/admin/routes determines its path in the dashboard. The file’s default export must be the UI route’s React component.

For example, create the file src/admin/routes/custom/page.tsx with the following content:

Example of UI route file in the application's directory structure

src/admin/routes/custom/page.tsx
1import { Container, Heading } from "@medusajs/ui"2
3const CustomPage = () => {4  return (5    <Container className="divide-y p-0">6      <div className="flex items-center justify-between px-6 py-4">7        <Heading level="h2">This is my custom route</Heading>8      </div>9    </Container>10  )11}12
13export default CustomPage

You add a new route at http://localhost:9000/app/custom. The CustomPage component holds the page's content, which currently only shows a heading.

In the route, you use Medusa UI, a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it.

Important: The UI route component must be created as an arrow function.

Test the UI Route#

To test the UI route, start the Medusa application:

Then, after logging into the admin dashboard, open the page http://localhost:9000/app/custom to see your custom page.


Show UI Route in the Sidebar#

To add a sidebar item for your custom UI route, export a configuration object in the UI route's file:

src/admin/routes/custom/page.tsx
1import { defineRouteConfig } from "@medusajs/admin-sdk"2import { ChatBubbleLeftRight } from "@medusajs/icons"3import { Container, Heading } from "@medusajs/ui"4
5const CustomPage = () => {6  return (7    <Container className="divide-y p-0">8      <div className="flex items-center justify-between px-6 py-4">9        <Heading level="h2">This is my custom route</Heading>10      </div>11    </Container>12  )13}14
15export const config = defineRouteConfig({16  label: "Custom Route",17  icon: ChatBubbleLeftRight,18})19
20export default CustomPage

The configuration object is created using defineRouteConfig from the Medusa Framework. It accepts the following properties:

  • label: the sidebar item’s label.
  • icon: an optional React component used as an icon in the sidebar.

The above example adds a new sidebar item with the label Custom Route and an icon from the Medusa UI Icons package.

Nested UI Routes#

Consider that alongside the UI route above at src/admin/routes/custom/page.tsx you create a nested UI route at src/admin/routes/custom/nested/page.tsx that also exports route configurations:

Example of nested UI route file in the application's directory structure

src/admin/routes/custom/nested/page.tsx
1import { defineRouteConfig } from "@medusajs/admin-sdk"2import { Container, Heading } from "@medusajs/ui"3
4const NestedCustomPage = () => {5  return (6    <Container className="divide-y p-0">7      <div className="flex items-center justify-between px-6 py-4">8        <Heading level="h2">This is my nested custom route</Heading>9      </div>10    </Container>11  )12}13
14export const config = defineRouteConfig({15  label: "Nested Route",16})17
18export default NestedCustomPage

This UI route is shown in the sidebar as an item nested in the parent "Custom Route" item. Nested items are only shown when the parent sidebar items (in this case, "Custom Route") are clicked.

Caveats

Some caveats for nested UI routes in the sidebar:

  • Nested dynamic UI routes, such as one created at src/admin/routes/custom/[id]/page.tsx aren't added to the sidebar as it's not possible to link to a dynamic route. If the dynamic route exports route configurations, a warning is logged in the browser's console.
  • Nested routes in settings pages aren't shown in the sidebar to follow the admin's design conventions.
  • The icon configuration is ignored for the sidebar item of nested UI routes to follow the admin's design conventions.

Route Under Existing Admin Route#

You can add a custom UI route under an existing route. For example, you can add a route under the orders route:

src/admin/routes/orders/nested/page.tsx
1import { defineRouteConfig } from "@medusajs/admin-sdk"2import { Container, Heading } from "@medusajs/ui"3
4const NestedOrdersPage = () => {5  return (6    <Container className="divide-y p-0">7      <div className="flex items-center justify-between px-6 py-4">8        <Heading level="h1">Nested Orders Page</Heading>9      </div>10    </Container>11  )12}13
14export const config = defineRouteConfig({15  label: "Nested Orders",16  nested: "/orders",17})18
19export default NestedOrdersPage

The nested property passed to defineRouteConfig specifies which route this custom route is nested under. This route will now show in the sidebar under the existing "Orders" sidebar item.


Create Settings Page#

To create a page under the settings section of the admin dashboard, create a UI route under the path src/admin/routes/settings.

For example, create a UI route at src/admin/routes/settings/custom/page.tsx:

Example of settings UI route file in the application's directory structure

src/admin/routes/settings/custom/page.tsx
1import { defineRouteConfig } from "@medusajs/admin-sdk"2import { Container, Heading } from "@medusajs/ui"3
4const CustomSettingPage = () => {5  return (6    <Container className="divide-y p-0">7      <div className="flex items-center justify-between px-6 py-4">8        <Heading level="h1">Custom Setting Page</Heading>9      </div>10    </Container>11  )12}13
14export const config = defineRouteConfig({15  label: "Custom",16})17
18export default CustomSettingPage

This adds a page under the path /app/settings/custom. An item is also added to the settings sidebar with the label Custom.


Path Parameters#

A UI route can accept path parameters if the name of any of the directories in its path is of the format [param].

For example, create the file src/admin/routes/custom/[id]/page.tsx with the following content:

Example of UI route file with path parameters in the application's directory structure

src/admin/routes/custom/[id]/page.tsx
1import { useParams } from "react-router-dom"2import { Container, Heading } from "@medusajs/ui"3
4const CustomPage = () => {5  const { id } = useParams()6
7  return (8    <Container className="divide-y p-0">9      <div className="flex items-center justify-between px-6 py-4">10        <Heading level="h1">Passed ID: {id}</Heading>11      </div>12    </Container>13  )14}15
16export default CustomPage

You access the passed parameter using react-router-dom's useParams hook.

If you run the Medusa application and go to http://localhost:9000/app/custom/123, you'll see 123 printed in the page.


Set UI Route Breadcrumbs#

The Medusa Admin dashboard shows breadcrumbs at the top of each page, if specified. This allows users to navigate through your custom UI routes.

To set the breadcrumbs of a UI route, export a handle object with a breadcrumb property in the UI route's file:

src/admin/routes/custom/page.tsx
1import { Container, Heading } from "@medusajs/ui"2
3const CustomPage = () => {4  return (5    <Container className="divide-y p-0">6      <div className="flex items-center justify-between px-6 py-4">7        <Heading level="h2">This is my custom route</Heading>8      </div>9    </Container>10  )11}12
13export default CustomPage14
15export const handle = {16  breadcrumb: () => "Custom Route",17}

The breadcrumb's value is a function that returns the breadcrumb label as a string, or a React JSX element.

Set Breadcrumbs for Nested UI Routes#

If you set a breadcrumb for a nested UI route, and you open the route in the Medusa Admin, you'll see the breadcrumbs starting from its parent route to the nested route.

For example, if you have the following UI route at src/admin/routes/custom/nested/page.tsx that's nested under the previous one:

src/admin/routes/custom/nested/page.tsx
1import { Container, Heading } from "@medusajs/ui"2
3const NestedCustomPage = () => {4  return (5    <Container className="divide-y p-0">6      <div className="flex items-center justify-between px-6 py-4">7        <Heading level="h2">This is my nested custom route</Heading>8      </div>9    </Container>10  )11}12
13export default NestedCustomPage14
15export const handle = {16  breadcrumb: () => "Nested Custom Route",17}

Then, when you open the nested route at http://localhost:9000/app/custom/nested, you'll see the breadcrumbs as Custom Route > Nested Custom Route. Each breadcrumb is clickable, allowing users to navigate back to the parent route.

Set Breadcrumbs Dynamically#

In some use cases, you may want to show a dynamic breadcrumb for a UI route. For example, if you have a UI route that displays a brand's details, you can set the breadcrumb to show the brand's name dynamically.

To do that, you can:

  1. Define and export a loader function in the UI route file that fetches the data needed for the breadcrumb.
  2. Receive the data in the breadcrumb function and return the dynamic label.

For example, create a UI route at src/admin/routes/brands/[id]/page.tsx with the following content:

src/admin/routes/brands/[id]/page.tsx
1import { Container, Heading } from "@medusajs/ui"2import { LoaderFunctionArgs, UIMatch, useLoaderData } from "react-router-dom"3import { sdk } from "../../../lib/sdk"4
5type BrandResponse = {6  brand: {7    name: string8  }9}10
11const BrandPage = () => {12  const { brand } = useLoaderData() as Awaited<BrandResponse>13
14  return (15    <Container className="divide-y p-0">16      <div className="flex items-center justify-between px-6 py-4">17        <Heading level="h2">{brand.name}</Heading>18      </div>19    </Container>20  )21}22
23export default BrandPage24
25export async function loader({ params }: LoaderFunctionArgs) {26  const { id } = params27  const { brand } = await sdk.client.fetch<BrandResponse>(`/admin/brands/${id}`)28
29  return {30    brand,31  }32}33
34export const handle = {35  breadcrumb: (36    { data }: UIMatch<BrandResponse>37  ) => data.brand.name || "Brand",38}

In the loader function, you retrieve the brands from a custom API route and return them.

Then, in the handle.breadcrumb function, you receive data prop containing the brand information returned by the loader function. You can use this data to return a dynamic breadcrumb label.

When you open the UI route at http://localhost:9000/app/brands/123, the breadcrumb will show the brand's name, such as Acme.

Tip: You also use the useLoaderData hook to access the data returned by the loader function in the UI route component. Learn more in the Routing Customizations chapter.

Admin Components List#

To build admin customizations that match the Medusa Admin's designs and layouts, refer to this guide to find common components.


More Routes Customizations#

For more customizations related to routes, refer to the Routing Customizations chapter.

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