Checkpoint 1.3: Brand API Route
This checkpoint verifies that you've successfully created the POST /admin/brands API route with validation and middleware.
Verification Questions
Before proceeding, test your understanding:
Why do we execute workflows from API routes instead of calling services directly?
<details> <summary>Answer</summary>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.
</details>What does
<details> <summary>Answer</summary>validateAndTransformBodymiddleware 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.
</details>Why do we use
<details> <summary>Answer</summary>MedusaRequestandMedusaResponseinstead of Express types?These are Medusa-specific types that extend Express types with additional properties like
</details>scope(for dependency injection) andqueryConfig(for filtering/pagination). Using these types gives you type-safe access to Medusa-specific features.What is the
<details> <summary>Answer</summary>scopeobject and how does it work?
</details>scopeis 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.
Implementation Check
Let me verify your implementation. Please share the following:
1. Schema File
Show me your src/api/admin/brands/validators.ts file.
Key things to check:
- Imports
zfrom "@medusajs/framework/zod" - Defines
CreateBrandSchemawithz.object() - Has
namefield withz.string() - Exports schema as named export
2. Route File
Show me your src/api/admin/brands/route.ts file.
Key things to check:
- Imports types:
MedusaRequest,MedusaResponse - Imports workflow:
import { createBrandWorkflow } from "..." - Imports workflow input type:
CreateBrandWorkflowInput - Defines
POSTfunction (must be namedPOSTexactly) - Uses type:
MedusaRequest<CreateBrandWorkflowInput> - Executes workflow:
await createBrandWorkflow(req.scope).run({ input: ... }) - Extracts brand from result:
result.resultorresult.brand - Returns JSON:
res.json({ brand }) - Handles errors with try/catch
3. Middleware File
Show me your src/api/middlewares.ts file.
Key things to check:
- Imports
defineMiddlewares,validateAndTransformBody - Imports
CreateBrandSchema - Exports
default defineMiddlewares() - Has
routesarray - Route config has
matcher: "/admin/brands" - Route config has
method: "POST" - Route config has
middlewaresarray withvalidateAndTransformBody()
4. Server Running
Start your dev server:
npm run devExpected output: Server should start without errors. Check that there are no errors about missing routes or middleware.
Common Issues
Middleware not running / validation not working
Symptom: Invalid data passes through without validation errors
Cause: Middleware not configured correctly
Fix:
- Check that
matcherexactly matches your route:"/admin/brands" - Check that
methodis uppercase:"POST" - Ensure
middlewares.tsis in the correct location:src/api/middlewares.ts - Restart dev server after middleware changes
"Empty array returned" or "brand is undefined"
Symptom: 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).
Route not found / 404 error
Symptom: cURL returns 404
Cause: File not in correct location or not named correctly
Fix:
- Ensure file is at:
src/api/admin/brands/route.ts - Ensure function is exported as
POST(not default export) - Restart dev server
- Check URL is correct:
http://localhost:9000/admin/brands
"Workflow failed" with no specific error
Symptom: Generic workflow failure
Cause: Error in step execution (likely in createBrandStep)
Fix:
- Check server logs for detailed error message
- Verify brand service is accessible in the step
- Verify database connection is working
- Check that migrations ran successfully
TypeScript error: "Property 'validatedBody' does not exist"
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
}Testing Checklist
Verify each of these steps:
- Server starts without errors
- POST request to
/admin/brandssucceeds - Response contains brand object with id and name
- Invalid request (missing name) returns 400 error
- Brand is actually saved (check with GET request or database query)
- Build succeeds:
npm run build
Manual Database Verification (Optional)
If 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.
Architecture Understanding
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:
- Separation of concerns: Each layer has a single responsibility
- Reusability: Workflow can be called from multiple routes (HTTP, GraphQL, CLI)
- Testability: Each layer can be tested independently
- Maintainability: Changes to one layer don't affect others
Next Steps
Once this checkpoint passes:
Lesson 1 Complete! You've built a complete feature from scratch:
- Brand Module (data layer)
- createBrandWorkflow (business logic with rollback)
- POST /admin/brands (HTTP interface with validation)
Commit your work:
git add . git commit -m "Complete Lesson 1: Build custom brand feature"Next: Lesson 2 - Extend Medusa
- Link brands to products using Module Links
- Extend core workflows using Workflow Hooks
- Query linked data across modules
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.