Subchapter 2.9
checkpoints/checkpoint-ui-route.mdMarkdown13 KBView on GitHub
This checkpoint verifies that you’ve successfully created a brands management page with a data table and pagination.
Before proceeding, test your understanding:
How does the file path determine the URL of a UI route?
The file structure under src/admin/routes/ maps to URLs under /app/. For example:
src/admin/routes/brands/page.tsx → /app/brandssrc/admin/routes/settings/team/page.tsx → /app/settings/teamThe file MUST be named page.tsx (not route.tsx or index.tsx). Nested folders create nested routes.
Why do we use sdk.client.fetch() instead of sdk.admin.brand.list()?
sdk.admin.brand.list() doesn’t exist because the /admin/brands API route is custom, and the JS SDK only has methods for core API routes. For custom API routes, use sdk.client.fetch() which makes a raw HTTP request to any endpoint.
What is the purpose of defineRouteConfig() and what happens without it?
defineRouteConfig() adds the route to the admin sidebar navigation and customizes its appearance (label, icon). Without it, the route still exists and is accessible by URL, but users wouldn’t see a navigation link. They’d have to type the URL manually or have a link from somewhere else.
Let me verify your implementation. Please share the following:
Show me your updated src/api/admin/brands/route.ts file with the GET handler.
Key things to check:
GET functionquery.graph() with:
entity: "brand"req.queryConfigNote: You should have already created this in Checkpoint 2.3. If not, create it now.
Show me the GET /admin/brands configuration in src/api/middlewares.ts.
Key things to check:
"/admin/brands""GET"validateAndTransformQuery() with:
GetBrandsSchema (from createFindParams())defaults and isList: trueNote: You should have already created this in Checkpoint 2.3. If not, create it now.
Show me your src/admin/routes/brands/page.tsx file.
Key things to check:
defineRouteConfig from “@medusajs/admin-sdk”TagSolid) from “@medusajs/icons”Container, Heading, DataTable, etc. from “@medusajs/ui”useQuery from “@tanstack/react-query”sdk from “../../lib/sdk”useState, useMemoBrand type with id, name, productsBrandsResponse type with brands, count, limit, offsetcreateDataTableColumnHelper<Brand>()useState({ pageSize, pageIndex })sdk.client.fetch() with /admin/brands and query paramsBrandsResponseuseDataTable() hook with columns, data, rowCount, paginationnpm run devExpected: You should see a “Brands” menu item with the icon you chose.
Expected:
Expected: Count should be accurate (0 for brands with no products, 1+ for brands with products).
Symptom: Can’t find “Brands” in navigation
Causes and Fixes:
Cause 1: Config not exported
export const config = defineRouteConfig({
label: "Brands",
icon: TagSolid,
})Cause 2: File not named correctly
page.tsx (not route.tsx)Cause 3: File not in correct location
src/admin/routes/brands/page.tsxSymptom: Clicking link results in 404
Cause: File structure incorrect
Fix: Ensure the structure is:
src/admin/routes/brands/page.tsxNOT:
src/admin/routes/brands.tsx ❌
src/admin/routes/brands/index.tsx ❌Symptom: Table renders but shows no brands
Causes and Fixes:
Cause 1: Backend API not working
curl http://localhost:9000/admin/brandsCause 2: Query not fetching data
Cause 3: Data structure mismatch
{ brands: [...] } formatBrandsResponseSymptom: Runtime error accessing products
Cause: Trying to access products.length when products might be undefined
Fix: Use optional chaining in column definition:
columnHelper.accessor("products", {
header: "Products",
cell: ({ getValue }) => {
const products = getValue()
return products?.length || 0
},
})Symptom: Clicking next page doesn’t change data
Causes and Fixes:
Cause 1: offset not calculated correctly
Cause 2: Query key doesn’t include pagination
queryKey: ["brands", limit, offset]Cause 3: Backend not using offset parameter
Symptom: TypeScript error or runtime error
Cause: SDK not initialized
Fix:
src/admin/lib/sdk.ts exists and exports sdkimport { sdk } from "../../lib/sdk"../ matches your file structureSymptom: Table appears unstyled or layout is wrong
Cause: Not using DataTable components correctly
Fix: Use the full DataTable component structure:
<DataTable instance={table}>
<DataTable.Toolbar>
<Heading>Brands</Heading>
</DataTable.Toolbar>
<DataTable.Table />
<DataTable.Pagination />
</DataTable>Symptom: Import error for icons
Cause: Package not installed
Fix: Icons are included with Medusa admin. Check import:
import { TagSolid } from "@medusajs/icons"If still not working, ensure admin dependencies are installed:
npm installSymptom: Table shows 0 products even though links exist
Causes and Fixes:
Cause 1: Backend not including products in response
"products.*"Cause 2: Links not created
Cause 3: Column accessing wrong property
Symptom: Can access http://localhost:9000/app/brands (opens in a new tab) but no sidebar link
Cause: Config not exported or exported incorrectly
Fix: Must export config as named export:
export const config = defineRouteConfig({ ... })NOT:
export default defineRouteConfig({ ... }) ❌Verify each of these steps:
At this point, you should understand:
UI Route structure:
File System URL Sidebar
src/admin/routes/brands/page.tsx → /app/brands → "Brands" link
↓
defineRouteConfig()
- label: "Brands"
- icon: TagSolidData flow for UI routes:
1. User clicks "Brands" in sidebar
│
▼
2. React Router navigates to /app/brands
│
▼
3. BrandsPage component renders
│
▼
4. useQuery fetches data
- sdk.client.fetch("/admin/brands")
- With limit & offset params
│
▼
5. Backend: GET /admin/brands
- Middleware validates query
- Route handler calls query.graph()
- Returns { brands, count, limit, offset }
│
▼
6. Frontend: DataTable renders
- Shows brands in table
- Pagination controls use count & limitComplete feature architecture (all 3 lessons):
┌─────────────────────────────────────────────────┐
│ Admin UI (Lesson 3) │
│ - Widget: Shows brand on product page │
│ - UI Route: Brands management page │
└─────────────────┬───────────────────────────────┘
│ HTTP Requests
▼
┌─────────────────────────────────────────────────┐
│ API Routes (Lesson 1 & 2) │
│ - POST /admin/brands (create) │
│ - GET /admin/brands (list with products) │
└─────────────────┬───────────────────────────────┘
│ Executes
▼
┌─────────────────────────────────────────────────┐
│ Workflows (Lesson 1 & 2) │
│ - createBrandWorkflow (with rollback) │
│ - productsCreated hook (auto-link) │
└─────────────────┬───────────────────────────────┘
│ Uses
▼
┌─────────────────────────────────────────────────┐
│ Modules & Links (Lesson 1 & 2) │
│ - Brand Module (data & service) │
│ - Module Link (brand ↔ product) │
└─────────────────────────────────────────────────┘Once this checkpoint passes:
Lesson 3 Complete! You’ve built a complete admin UI:
ALL LESSONS COMPLETE! 🎉 You’ve built a complete feature:
Backend:
Frontend:
Commit your work:
git add .
git commit -m "Complete Lesson 3: Admin dashboard customization"What’s Next?
You now understand Medusa’s architecture and can build custom features independently:
Consider building:
Learn more:
Congratulations! 🎊 You’ve completed the interactive Medusa learning tutorial. You’re now ready to build production features with Medusa.