Subchapter 2.8
checkpoints/checkpoint-querying.mdMarkdown8 KBView on GitHub
This checkpoint verifies that you’ve successfully created a GET /admin/brands API route that queries brands with their linked products using Query.graph().
Before proceeding, test your understanding:
Why do we use +brand.* in the fields parameter?
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”.
What does req.queryConfig contain?
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.
Why return count, limit, and offset in the API response?
This follows REST pagination best practices. The frontend needs this metadata to:
Math.ceil(count / limit)offset + limitWithout this metadata, the frontend can’t build proper pagination UI.
Let me verify your implementation. Please share the following:
Show me your src/api/admin/brands/route.ts file (the updated version with GET handler).
Key things to check:
GET function (must be named GET exactly)MedusaRequest, MedusaResponsereq.scope.resolve("query")query.graph() with:
entity: "brand"req.queryConfig{ data: brands, metadata: { count, take, skip } = {} }Show me the GET /admin/brands middleware configuration in src/api/middlewares.ts.
Key things to check:
createFindParams from “@medusajs/medusa/api/utils/validators”GetBrandsSchema = createFindParams()"/admin/brands""GET"validateAndTransformQuery() with:
GetBrandsSchemadefaults array includes brand fields and products relationisList: trueExample:
validateAndTransformQuery(
GetBrandsSchema,
{
defaults: ["id", "name", "products.*"],
isList: true,
}
)Symptom: API returns empty brands array
Causes and Fixes:
Cause 1: Entity name incorrect
entity: "brand" (lowercase, singular)Cause 2: Middleware not configured with defaults
defaults to middleware configCause 3: Module not registered properly
medusa-config.ts has brand moduleSymptom: 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,
})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,
}
)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, { ... })Symptom: brands return but products array is empty
Causes and Fixes:
Cause 1: Link not created properly
Cause 2: products.* not in defaults
"products.*" to defaults arrayCause 3: Link direction is backwards
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 }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:
Once this checkpoint passes:
Lesson 2 Complete! You’ve extended Medusa’s core functionality:
Commit your work:
git add .
git commit -m "Complete Lesson 2: Extend Medusa with links and hooks"Next: Lesson 3 - Customize Admin Dashboard
Ready for Lesson 3? Now that the backend is complete, we’ll build the admin UI to manage brands visually.