Documentation Index Fetch the complete documentation index at: https://mintlify.com/medusajs/medusa/llms.txt
Use this file to discover all available pages before exploring further.
Custom routes allow you to add entirely new pages to the admin dashboard, complete with sidebar navigation and nested routing.
Overview
Routes in the Medusa admin dashboard are powered by React Router 6. You can create custom routes to:
Add new pages for custom functionality
Create nested routes under existing sections
Add items to the sidebar navigation
Build multi-page workflows
Creating a Custom Route
Basic Route
Create a new route by defining a React component and exporting a route configuration:
Create Route File
Create a new file in your admin extensions directory: src/admin/routes/analytics/page.tsx
Define the Component
// src/admin/routes/analytics/page.tsx
import { Container , Heading } from "@medusajs/ui"
const AnalyticsPage = () => {
return (
< Container >
< Heading > Analytics Dashboard </ Heading >
< div className = "mt-4" >
< p > Your custom analytics content here </ p >
</ div >
</ Container >
)
}
export default AnalyticsPage
Export Route Configuration
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChartBar } from "@medusajs/icons"
export const config = defineRouteConfig ({
label: "Analytics" ,
icon: ChartBar ,
})
export default AnalyticsPage
The route will be automatically registered and accessible at /analytics.
Route Configuration Options
The defineRouteConfig function accepts the following options:
interface RouteConfig {
/**
* Label to display in the sidebar
*/
label ?: string
/**
* Icon component to display next to the label
*/
icon ?: ComponentType
/**
* Nest this route under an existing route
*/
nested ?: NestedRoutePosition
/**
* Control the order in the sidebar (lower appears first)
*/
rank ?: number
/**
* i18n namespace for label translation
*/
translationNs ?: string
}
Route File Structure
The file path determines the route path:
src/admin/routes/
├── custom-page/
│ └── page.tsx → /custom-page
├── settings/
│ └── custom/
│ └── page.tsx → /settings/custom
└── analytics/
├── page.tsx → /analytics
└── [id]/
└── page.tsx → /analytics/:id
Dynamic Routes
Use [param] syntax for dynamic route segments:
// src/admin/routes/reports/[reportId]/page.tsx
import { useParams } from "react-router-dom"
import { Container , Heading } from "@medusajs/ui"
const ReportDetailPage = () => {
const { reportId } = useParams ()
return (
< Container >
< Heading > Report { reportId } </ Heading >
{ /* Fetch and display report data */ }
</ Container >
)
}
export default ReportDetailPage
Nested Routes
Nest your custom routes under existing dashboard sections:
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Package } from "@medusajs/icons"
const BrandManagementPage = () => {
return (
< div >
< h1 > Brand Management </ h1 >
{ /* Your content */ }
</ div >
)
}
export const config = defineRouteConfig ({
label: "Brands" ,
icon: Package ,
nested: "/products" , // Nest under Products section
rank: 1 // Appear first in the nested list
})
export default BrandManagementPage
Available nested positions (from packages/admin/admin-shared/src/extensions/routes/constants.ts:1):
/orders
/products
/inventory
/customers
/promotions
/price-lists
Data Loading
Use React Router loaders for data fetching:
import { LoaderFunctionArgs } from "react-router-dom"
import { useLoaderData } from "react-router-dom"
import { Container , Heading } from "@medusajs/ui"
type Report = {
id : string
name : string
data : any []
}
// Loader function
export async function loader ({ params } : LoaderFunctionArgs ) {
const response = await fetch ( `/admin/reports/ ${ params . reportId } ` )
const report : Report = await response . json ()
return { report }
}
// Component
const ReportDetailPage = () => {
const { report } = useLoaderData () as { report : Report }
return (
< Container >
< Heading > { report . name } </ Heading >
{ /* Render report data */ }
</ Container >
)
}
export default ReportDetailPage
Using the Admin SDK
Fetch data using the Medusa Admin SDK:
import { useMedusa } from "@medusajs/dashboard"
import { useQuery } from "@tanstack/react-query"
import { Container , Heading } from "@medusajs/ui"
const CustomProductsPage = () => {
const { client } = useMedusa ()
const { data , isLoading } = useQuery ({
queryKey: [ "custom-products" ],
queryFn : async () => {
const response = await client . products . list ()
return response . products
}
})
if ( isLoading ) {
return < div > Loading... </ div >
}
return (
< Container >
< Heading > Custom Products View </ Heading >
< div className = "grid grid-cols-3 gap-4 mt-4" >
{ data ?. map ( product => (
< div key = { product . id } className = "border p-4 rounded" >
< h3 > { product . title } </ h3 >
</ div >
)) }
</ div >
</ Container >
)
}
export default CustomProductsPage
Navigation
Programmatic Navigation
Use React Router hooks for navigation:
import { useNavigate } from "react-router-dom"
import { Button } from "@medusajs/ui"
const CustomPage = () => {
const navigate = useNavigate ()
const handleNavigate = () => {
navigate ( "/products" )
}
return (
< Button onClick = { handleNavigate } >
Go to Products
</ Button >
)
}
Links
import { Link } from "react-router-dom"
const CustomPage = () => {
return (
< div >
< Link to = "/orders" className = "text-ui-fg-interactive hover:underline" >
View Orders
</ Link >
</ div >
)
}
Layouts
Custom Layout
Create a layout for multiple related routes:
// src/admin/routes/analytics/layout.tsx
import { Outlet } from "react-router-dom"
import { Container } from "@medusajs/ui"
const AnalyticsLayout = () => {
return (
< Container >
< nav className = "flex gap-4 border-b pb-4 mb-4" >
< Link to = "/analytics/sales" > Sales </ Link >
< Link to = "/analytics/customers" > Customers </ Link >
< Link to = "/analytics/products" > Products </ Link >
</ nav >
< Outlet /> { /* Child routes render here */ }
</ Container >
)
}
export default AnalyticsLayout
Settings Routes
Add custom settings pages:
// src/admin/routes/settings/custom-settings/page.tsx
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Cog } from "@medusajs/icons"
import { Container , Heading } from "@medusajs/ui"
const CustomSettingsPage = () => {
return (
< Container >
< Heading > Custom Settings </ Heading >
{ /* Settings form */ }
</ Container >
)
}
export const config = defineRouteConfig ({
label: "Custom Settings" ,
icon: Cog ,
})
export default CustomSettingsPage
Settings routes are automatically grouped in the Settings section.
Route Guards
Protect routes with authentication or permissions:
import { useAuth } from "@medusajs/dashboard"
import { Navigate } from "react-router-dom"
const ProtectedRoute = () => {
const { user } = useAuth ()
if ( ! user || user . role !== "admin" ) {
return < Navigate to = "/" replace />
}
return (
< div >
< h1 > Admin Only Content </ h1 >
</ div >
)
}
export default ProtectedRoute
Complete Example
Here’s a complete example with data fetching, forms, and navigation:
// src/admin/routes/brands/page.tsx
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Package } from "@medusajs/icons"
import { Container , Heading , Button , Table } from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { useNavigate } from "react-router-dom"
type Brand = {
id : string
name : string
description : string
}
const BrandsPage = () => {
const navigate = useNavigate ()
const { data : brands , isLoading } = useQuery ({
queryKey: [ "brands" ],
queryFn : async () => {
const response = await fetch ( "/admin/brands" )
return response . json () as Promise < Brand []>
}
})
if ( isLoading ) {
return < div > Loading... </ div >
}
return (
< Container >
< div className = "flex items-center justify-between mb-4" >
< Heading > Brand Management </ Heading >
< Button onClick = { () => navigate ( "/brands/create" ) } >
Create Brand
</ Button >
</ div >
< Table >
< Table.Header >
< Table.Row >
< Table.HeaderCell > Name </ Table.HeaderCell >
< Table.HeaderCell > Description </ Table.HeaderCell >
< Table.HeaderCell ></ Table.HeaderCell >
</ Table.Row >
</ Table.Header >
< Table.Body >
{ brands ?. map (( brand ) => (
< Table.Row key = { brand . id } >
< Table.Cell > { brand . name } </ Table.Cell >
< Table.Cell > { brand . description } </ Table.Cell >
< Table.Cell >
< Button
variant = "secondary"
onClick = { () => navigate ( `/brands/ ${ brand . id } ` ) }
>
Edit
</ Button >
</ Table.Cell >
</ Table.Row >
)) }
</ Table.Body >
</ Table >
</ Container >
)
}
export const config = defineRouteConfig ({
label: "Brands" ,
icon: Package ,
nested: "/products" ,
rank: 10
})
export default BrandsPage
Best Practices
Use Meaningful Route Paths
Choose route paths that clearly describe the page content (e.g., /reports/sales instead of /r/s).
Always show loading indicators while fetching data to improve user experience.
Use error boundaries and show helpful error messages when data loading fails.
Use TanStack Query for data fetching to get automatic caching, refetching, and optimistic updates.
Next Steps