Skip to main content
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:
1

Create Route File

Create a new file in your admin extensions directory:
src/admin/routes/analytics/page.tsx
2

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
3

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

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>
  )
}
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

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

Build docs developers (and LLMs) love