# Checkpoint 2.3: Querying Linked Records
This checkpoint verifies that you've successfully created a GET /admin/brands API route that queries brands with their linked products using Query.graph().
## Verification Questions
Before proceeding, test your understanding:
1. **Why do we use `+brand.*` in the fields parameter?**
Answer
The `+` means "include these fields IN ADDITION to the default fields". Without `+`, you would replace the default fields entirely. The `.*` means "include all fields from the brand relation". So `+brand.*` says "give me all default product fields PLUS all brand fields".
2. **What does `req.queryConfig` contain?**
Answer
`req.queryConfig` contains pre-processed query parameters like `fields`, `limit`, `offset`, `order`, and filters. Middleware parses the query string and transforms it into this structured format. You can pass it directly to `query.graph()` to apply user-requested filtering and pagination without manually parsing the query string.
3. **Why return count, limit, and offset in the API response?**
Answer
This follows REST pagination best practices. The frontend needs this metadata to:
- Show total count: "Showing 10 of 50 brands"
- Implement "Load More" or page navigation
- Calculate total pages: `Math.ceil(count / limit)`
- Request next page: `offset + limit`
Without this metadata, the frontend can't build proper pagination UI.
## Implementation Check
Let me verify your implementation. Please share the following:
### 1. API Route File
Show me your `src/api/admin/brands/route.ts` file (the updated version with GET handler).
**Key things to check**:
- [ ] Defines `GET` function (must be named `GET` exactly)
- [ ] Uses types: `MedusaRequest`, `MedusaResponse`
- [ ] Resolves Query service: `req.scope.resolve("query")`
- [ ] Calls `query.graph()` with:
- `entity: "brand"`
- Spreads `req.queryConfig`
- [ ] Destructures result: `{ data: brands, metadata: { count, take, skip } = {} }`
- [ ] Returns JSON with brands, count, limit (take), offset (skip)
### 2. Middleware Configuration
Show me the GET /admin/brands middleware configuration in `src/api/middlewares.ts`.
**Key things to check**:
- [ ] Imports `createFindParams` from "@medusajs/medusa/api/utils/validators"
- [ ] Defines `GetBrandsSchema = createFindParams()`
- [ ] Route configuration:
- Matcher: `"/admin/brands"`
- Method: `"GET"`
- Uses `validateAndTransformQuery()` with:
- Schema: `GetBrandsSchema`
- Options: `defaults` array includes brand fields and products relation
- Options: `isList: true`
Example:
```typescript
validateAndTransformQuery(
GetBrandsSchema,
{
defaults: ["id", "name", "products.*"],
isList: true,
}
)
```
## Common Issues
### "Empty array returned" even though brands exist
**Symptom**: API returns empty brands array
**Causes and Fixes**:
**Cause 1**: Entity name incorrect
- **Fix**: Use `entity: "brand"` (lowercase, singular)
**Cause 2**: Middleware not configured with defaults
- **Fix**: Add `defaults` to middleware config
**Cause 3**: Module not registered properly
- **Fix**: Check `medusa-config.ts` has brand module
### "metadata is undefined"
**Symptom**: Error accessing count, take, skip
**Cause**: query.graph() doesn't return metadata (should always return it)
**Fix**:
Use default values in destructuring:
```typescript
const {
data: brands,
metadata: { count, take, skip } = {}
} = await query.graph({ ... })
res.json({
brands,
count: count || 0,
limit: take || 15,
offset: skip || 0,
})
```
### "products field not included" in response
**Symptom**: Brand objects don't have products array
**Cause**: Middleware defaults don't include products
**Fix**:
Add to defaults in middleware:
```typescript
validateAndTransformQuery(
GetBrandsSchema,
{
defaults: ["id", "name", "products.*"],
isList: true,
}
)
```
### "Validation error: invalid query parameter"
**Symptom**: 400 error when using query parameters
**Cause**: Middleware not configured or using wrong validator
**Fix**:
Ensure you're using `createFindParams()`:
```typescript
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
export const GetBrandsSchema = createFindParams()
```
And using `validateAndTransformQuery()` (not `validateAndTransformBody()`):
```typescript
validateAndTransformQuery(GetBrandsSchema, { ... })
```
### Products array empty even though links exist
**Symptom**: brands return but products array is empty
**Causes and Fixes**:
**Cause 1**: Link not created properly
- **Fix**: Check Checkpoint 2.2 - verify links exist in database
**Cause 2**: products.* not in defaults
- **Fix**: Add `"products.*"` to defaults array
**Cause 3**: Link direction is backwards
- **Fix**: Review link definition in Checkpoint 2.1
### "Cannot read property 'result' from undefined"
**Symptom**: Error accessing query result
**Cause**: Incorrect destructuring of query.graph() result
**Fix**:
Use `data` for the result array:
```typescript
const { data: brands } = await query.graph({ ... })
// NOT: const { result: brands }
```
## Architecture Understanding
At this point, you should understand:
**Two ways to query linked data**:
**Method 1: Fields Parameter** (Simple queries)
```typescript
// In a service method
product = await productService.retrieve(id, {
fields: "+brand.*"
})
```
**Method 2: query.graph()** (Complex queries)
```typescript
// In API routes
const { data } = await query.graph({
entity: "brand",
fields: ["id", "name", "products.*"],
filters: { ... },
pagination: { ... }
})
```
**Query.graph() data flow**:
```
Request: GET /admin/brands?limit=10&offset=0
│
▼
┌──────────────┐
│ Middleware │ ← Parses query string
│ validates │ Transforms to queryConfig
└──────┬───────┘
│ req.queryConfig = {
│ fields: ["id", "name", "products.*"],
│ take: 10,
│ skip: 0
│ }
▼
┌──────────────┐
│ Route Handler│
│ query.graph()│ ← Applies queryConfig
└──────┬───────┘
│
▼
┌──────────────┐
│ Database │
│ + Link │ ← Joins brand and product tables
│ Layer │
└──────┬───────┘
│
▼
Response: { brands: [...], count, limit, offset }
```
**Why this matters**:
- **Flexibility**: Clients control what data they need
- **Performance**: Only fetch requested fields
- **Pagination**: Handle large datasets efficiently
- **Consistency**: Same query patterns across all entities
## Next Steps
Once this checkpoint passes:
1. **Lesson 2 Complete!** You've extended Medusa's core functionality:
- Module Link defined (brand ↔ product relationship)
- Workflow Hook consuming productsCreated
- Query capability for linked records
2. **Commit your work**:
```bash
git add .
git commit -m "Complete Lesson 2: Extend Medusa with links and hooks"
```
3. **Next: Lesson 3** - Customize Admin Dashboard
- Create Widget to show brand on product page
- Create UI Route for brands management page
- Use React Query and Medusa UI components
**Ready for Lesson 3?** Now that the backend is complete, we'll build the admin UI to manage brands visually.