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:
Why do we use
<details> <summary>Answer</summary>+brand.*in the fields parameter?The
</details>+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".What does
<details> <summary>Answer</summary>req.queryConfigcontain?
</details>req.queryConfigcontains pre-processed query parameters likefields,limit,offset,order, and filters. Middleware parses the query string and transforms it into this structured format. You can pass it directly toquery.graph()to apply user-requested filtering and pagination without manually parsing the query string.Why return count, limit, and offset in the API response?
<details> <summary>Answer</summary>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.
</details>
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
GETfunction (must be namedGETexactly) - 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
createFindParamsfrom "@medusajs/medusa/api/utils/validators" - Defines
GetBrandsSchema = createFindParams() - Route configuration:
- Matcher:
"/admin/brands" - Method:
"GET" - Uses
validateAndTransformQuery()with:- Schema:
GetBrandsSchema - Options:
defaultsarray includes brand fields and products relation - Options:
isList: true
- Schema:
- Matcher:
Example:
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
defaultsto middleware config
Cause 3: Module not registered properly
- Fix: Check
medusa-config.tshas 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:
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:
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():
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
export const GetBrandsSchema = createFindParams()And using validateAndTransformQuery() (not validateAndTransformBody()):
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:
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)
// In a service method
product = await productService.retrieve(id, {
fields: "+brand.*"
})Method 2: query.graph() (Complex queries)
// 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:
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
Commit your work:
git add . git commit -m "Complete Lesson 2: Extend Medusa with links and hooks"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.