Subchapter 2.5
checkpoints/checkpoint-api-route.mdMarkdown9 KBView on GitHub
This checkpoint verifies that you’ve successfully created the POST /admin/brands API route with validation and middleware.
Before proceeding, test your understanding:
Why do we execute workflows from API routes instead of calling services directly?
Workflows provide orchestration, rollback, and transaction management. If you call services directly from routes, you have to manually handle rollback logic when errors occur. Workflows handle this automatically through compensation functions. This becomes crucial as your business logic grows more complex with multiple steps.
What does validateAndTransformBody middleware do?
It validates incoming request body against a Zod schema BEFORE your route handler runs. If validation fails, it automatically returns a 400 error with validation details. If validation succeeds, it transforms the data according to the schema and passes the validated data to your handler. This ensures your handler only receives valid data.
Why do we use MedusaRequest and MedusaResponse instead of Express types?
These are Medusa-specific types that extend Express types with additional properties like scope (for dependency injection) and queryConfig (for filtering/pagination). Using these types gives you type-safe access to Medusa-specific features.
What is the scope object and how does it work?
scope is Medusa’s dependency injection container scoped to the current request. You pass it to workflows when executing them (e.g., workflow(req.scope).run()), and use it to resolve services (e.g., scope.resolve("query")). Each request gets its own scope, ensuring proper isolation and allowing request-specific configuration.
Let me verify your implementation. Please share the following:
Show me your src/api/admin/brands/validators.ts file.
Key things to check:
z from “@medusajs/framework/zod”CreateBrandSchema with z.object()name field with z.string()Show me your src/api/admin/brands/route.ts file.
Key things to check:
MedusaRequest, MedusaResponseimport { createBrandWorkflow } from "..."CreateBrandWorkflowInputPOST function (must be named POST exactly)MedusaRequest<CreateBrandWorkflowInput>await createBrandWorkflow(req.scope).run({ input: ... })result.result or result.brandres.json({ brand })Show me your src/api/middlewares.ts file.
Key things to check:
defineMiddlewares, validateAndTransformBodyCreateBrandSchemadefault defineMiddlewares()routes arraymatcher: "/admin/brands"method: "POST"middlewares array with validateAndTransformBody()Start your dev server:
npm run devExpected output: Server should start without errors. Check that there are no errors about missing routes or middleware.
Symptom: Invalid data passes through without validation errors
Cause: Middleware not configured correctly
Fix:
matcher exactly matches your route: "/admin/brands"method is uppercase: "POST"middlewares.ts is in the correct location: src/api/middlewares.tsSymptom: API returns empty response or undefined brand
Cause: Not extracting brand from workflow result correctly
Fix: Workflow results are nested:
const { result } = await workflow.run({ input: req.validatedBody })
const brand = result.result // Note: double .result
res.json({ brand })The first .result is the workflow execution result, the second .result is from WorkflowResponse(brand).
Symptom: cURL returns 404
Cause: File not in correct location or not named correctly
Fix:
src/api/admin/brands/route.tsPOST (not default export)http://localhost:9000/admin/brandsSymptom: Generic workflow failure
Cause: Error in step execution (likely in createBrandStep)
Fix:
Symptom: Build fails with TS error
Cause: Missing type for validated body
Fix: Use generic type parameter:
export const POST = async (
req: MedusaRequest<CreateBrandWorkflowInput>,
res: MedusaResponse
) => {
const input = req.validatedBody // TypeScript knows this is CreateBrandWorkflowInput
}Verify each of these steps:
/admin/brands succeedsnpm run buildIf you want to verify the brand was actually saved:
# Connect to your database
psql your_database_name
# Query brands table
SELECT * FROM brand;You should see the Nike brand you created.
At this point, you should understand the full three-layer pattern:
┌─────────────────────────────────────────────────┐
│ API Route (HTTP Interface) │
│ - Validates input │
│ - Executes workflow │
│ - Returns response │
└─────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Workflow (Business Logic Orchestration) │
│ - Coordinates steps │
│ - Handles rollback │
│ - Manages transactions │
└─────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Module (Data Layer) │
│ - Provides CRUD operations │
│ - Isolated from other modules │
└─────────────────────────────────────────────────┘Why this matters:
Once this checkpoint passes:
Lesson 1 Complete! You’ve built a complete feature from scratch:
Commit your work:
git add .
git commit -m "Complete Lesson 1: Build custom brand feature"Next: Lesson 2 - Extend Medusa
Ready for Lesson 2? This is where it gets really interesting - you’ll learn how to extend Medusa’s core functionality without forking the codebase.