# Checkpoint 3.2: Brands UI Route
This checkpoint verifies that you've successfully created a brands management page with a data table and pagination.
## Verification Questions
Before proceeding, test your understanding:
1. **How does the file path determine the URL of a UI route?**
Answer
The file structure under `src/admin/routes/` maps to URLs under `/app/`. For example:
- `src/admin/routes/brands/page.tsx` → `/app/brands`
- `src/admin/routes/settings/team/page.tsx` → `/app/settings/team`
The file MUST be named `page.tsx` (not `route.tsx` or `index.tsx`). Nested folders create nested routes.
2. **Why do we use `sdk.client.fetch()` instead of `sdk.admin.brand.list()`?**
Answer
`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.
3. **What is the purpose of `defineRouteConfig()` and what happens without it?**
Answer
`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.
## Implementation Check
Let me verify your implementation. Please share the following:
### 1. Backend API Route (with GET handler)
Show me your updated `src/api/admin/brands/route.ts` file with the GET handler.
**Key things to check**:
- [ ] Defines `GET` function
- [ ] Resolves query service
- [ ] Calls `query.graph()` with:
- `entity: "brand"`
- Spreads `req.queryConfig`
- [ ] Returns JSON with brands, count, limit, offset
**Note**: You should have already created this in Checkpoint 2.3. If not, create it now.
### 2. Backend Middleware Configuration
Show me the GET /admin/brands configuration in `src/api/middlewares.ts`.
**Key things to check**:
- [ ] Route matcher: `"/admin/brands"`
- [ ] Method: `"GET"`
- [ ] Uses `validateAndTransformQuery()` with:
- `GetBrandsSchema` (from `createFindParams()`)
- Options with `defaults` and `isList: true`
**Note**: You should have already created this in Checkpoint 2.3. If not, create it now.
### 3. UI Route File
Show me your `src/admin/routes/brands/page.tsx` file.
**Key things to check**:
- [ ] Imports `defineRouteConfig` from "@medusajs/admin-sdk"
- [ ] Imports icon (e.g., `TagSolid`) from "@medusajs/icons"
- [ ] Imports UI components: `Container`, `Heading`, `DataTable`, etc. from "@medusajs/ui"
- [ ] Imports `useQuery` from "@tanstack/react-query"
- [ ] Imports `sdk` from "../../lib/sdk"
- [ ] Imports React hooks: `useState`, `useMemo`
- [ ] Defines `Brand` type with id, name, products
- [ ] Defines `BrandsResponse` type with brands, count, limit, offset
- [ ] Creates columns using `createDataTableColumnHelper()`
- [ ] Defines at least 3 columns: id, name, products (showing count)
- [ ] Component has pagination state: `useState({ pageSize, pageIndex })`
- [ ] Calculates offset from pagination state
- [ ] useQuery:
- Calls `sdk.client.fetch()` with `/admin/brands` and query params
- Query key includes limit and offset
- Types response as `BrandsResponse`
- [ ] Uses `useDataTable()` hook with columns, data, rowCount, pagination
- [ ] Renders DataTable with Toolbar, Table, and Pagination
- [ ] Exports config with label and icon
- [ ] Default exports component
### 4. Test: Access UI Route
1. Ensure dev server is running: `npm run dev`
2. Open admin: http://localhost:9000/app
3. Look for "Brands" in the sidebar navigation
**Expected**: You should see a "Brands" menu item with the icon you chose.
### 5. Test: View Brands Page
1. Click the "Brands" menu item
2. View the brands table
**Expected**:
- Page displays with "Brands" heading
- Table shows columns: ID, Name, Products (count)
- Table shows all brands you've created
- Products column shows the number of products linked to each brand
### 6. Test: Product Count Accuracy
1. Look at the Products column for each brand
2. Verify the count matches the actual number of products linked
**Expected**: Count should be accurate (0 for brands with no products, 1+ for brands with products).
## Common Issues
### Route not showing in sidebar
**Symptom**: Can't find "Brands" in navigation
**Causes and Fixes**:
**Cause 1**: Config not exported
- **Fix**: Ensure you export config:
```typescript
export const config = defineRouteConfig({
label: "Brands",
icon: TagSolid,
})
```
**Cause 2**: File not named correctly
- **Fix**: Must be named `page.tsx` (not `route.tsx`)
**Cause 3**: File not in correct location
- **Fix**: Should be at `src/admin/routes/brands/page.tsx`
### "404 Not Found" when accessing /app/brands
**Symptom**: Clicking link results in 404
**Cause**: File structure incorrect
**Fix**:
Ensure the structure is:
```
src/admin/routes/brands/page.tsx
```
NOT:
```
src/admin/routes/brands.tsx ❌
src/admin/routes/brands/index.tsx ❌
```
### Table shows empty / no data
**Symptom**: Table renders but shows no brands
**Causes and Fixes**:
**Cause 1**: Backend API not working
- **Fix**: Test API directly: `curl http://localhost:9000/admin/brands`
- If API returns data, issue is in frontend
- If API returns empty, issue is in backend (see Checkpoint 2.3)
**Cause 2**: Query not fetching data
- **Fix**: Check browser DevTools Console for errors
- Check Network tab - is request being made?
**Cause 3**: Data structure mismatch
- **Fix**: Check that API returns `{ brands: [...] }` format
- Ensure useQuery is typed as `BrandsResponse`
### "Cannot read property 'length' of undefined"
**Symptom**: Runtime error accessing products
**Cause**: Trying to access products.length when products might be undefined
**Fix**:
Use optional chaining in column definition:
```typescript
columnHelper.accessor("products", {
header: "Products",
cell: ({ getValue }) => {
const products = getValue()
return products?.length || 0
},
})
```
### Pagination not working / always shows same data
**Symptom**: Clicking next page doesn't change data
**Causes and Fixes**:
**Cause 1**: offset not calculated correctly
- **Fix**: Ensure offset = pageIndex * pageSize
**Cause 2**: Query key doesn't include pagination
- **Fix**: Include offset in queryKey:
```typescript
queryKey: ["brands", limit, offset]
```
**Cause 3**: Backend not using offset parameter
- **Fix**: Verify middleware passes offset to query.graph()
### "Cannot use sdk.client.fetch"
**Symptom**: TypeScript error or runtime error
**Cause**: SDK not initialized
**Fix**:
1. Ensure `src/admin/lib/sdk.ts` exists and exports sdk
2. Import correctly: `import { sdk } from "../../lib/sdk"`
3. Check the number of `../` matches your file structure
### Table styling looks broken
**Symptom**: Table appears unstyled or layout is wrong
**Cause**: Not using DataTable components correctly
**Fix**:
Use the full DataTable component structure:
```tsx
Brands
```
### "Cannot find module '@medusajs/icons'"
**Symptom**: Import error for icons
**Cause**: Package not installed
**Fix**:
Icons are included with Medusa admin. Check import:
```typescript
import { TagSolid } from "@medusajs/icons"
```
If still not working, ensure admin dependencies are installed:
```bash
npm install
```
### Products count shows 0 for all brands
**Symptom**: Table shows 0 products even though links exist
**Causes and Fixes**:
**Cause 1**: Backend not including products in response
- **Fix**: Check middleware defaults include `"products.*"`
**Cause 2**: Links not created
- **Fix**: Verify links exist (see Checkpoint 2.2)
**Cause 3**: Column accessing wrong property
- **Fix**: Ensure column accessor matches API response structure
### Route accessible by URL but not in sidebar
**Symptom**: Can access http://localhost:9000/app/brands but no sidebar link
**Cause**: Config not exported or exported incorrectly
**Fix**:
Must export config as named export:
```typescript
export const config = defineRouteConfig({ ... })
```
NOT:
```typescript
export default defineRouteConfig({ ... }) ❌
```
## Testing Checklist
Verify each of these steps:
- [ ] Backend GET /admin/brands API working (test with cURL)
- [ ] Route appears in sidebar navigation with icon
- [ ] Clicking "Brands" navigates to /app/brands
- [ ] Table displays with proper styling
- [ ] Table shows all brands with columns: ID, Name, Products
- [ ] Products column shows accurate count
- [ ] Pagination controls appear (if 15+ brands)
- [ ] Pagination works (can navigate pages)
- [ ] No console errors in browser DevTools
## Architecture Understanding
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: TagSolid
```
**Data 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 & limit
```
**Complete 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) │
└─────────────────────────────────────────────────┘
```
## Next Steps
Once this checkpoint passes:
1. **Lesson 3 Complete!** You've built a complete admin UI:
- SDK initialized for API calls
- Product Brand Widget on product pages
- Brands UI Route with data table and pagination
2. **ALL LESSONS COMPLETE!** 🎉 You've built a complete feature:
**Backend**:
- Brand Module (data model, service)
- createBrandWorkflow (with rollback)
- POST /admin/brands (create brand API)
- Module Link (brand ↔ product)
- Workflow Hook (auto-link on product creation)
- GET /admin/brands (list brands with products)
**Frontend**:
- Product Brand Widget (show brand on product page)
- Brands UI Route (manage brands with table)
3. **Commit your work**:
```bash
git add .
git commit -m "Complete Lesson 3: Admin dashboard customization"
```
4. **What's Next?**
**You now understand Medusa's architecture** and can build custom features independently:
- Module → Workflow → API Route pattern
- Module Links for cross-module relationships
- Workflow Hooks for extending core functionality
- Admin customization with Widgets and UI Routes
**Consider building**:
- Categories Module (similar to Brand)
- Product Reviews feature
- Wishlists
- Custom shipping methods
- Inventory alerts
**Learn more**:
- Advanced Workflow Patterns
- Complex Admin Components
- Storefront Integration
- Testing your features
**Congratulations!** 🎊 You've completed the interactive Medusa learning tutorial. You're now ready to build production features with Medusa.