# Lesson 1: Build Custom Features with Medusa
## Learning Objectives
By the end of this lesson, you will:
- **Understand** the Module → Workflow → API Route architecture
- **Create** a Brand Module with data model and service
- **Implement** createBrandWorkflow with rollback logic
- **Expose** POST /admin/brands API route with validation
- **Test** your custom feature using cURL
**Time**: 45-60 minutes
## Architecture Overview: The Three-Layer Pattern
Before we start coding, let's understand **why** Medusa uses this layered architecture.
### The Pattern
Every custom feature in Medusa follows this flow:
```
┌─────────────────────────────────────────────────┐
│ API Route (HTTP Interface) │
│ - Accepts requests │
│ - Validates input │
│ - Executes workflow │
│ - Returns response │
└─────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Workflow (Business Logic Orchestration) │
│ - Coordinates steps │
│ - Handles rollback │
│ - Manages transactions │
└─────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Module (Data Layer) │
│ - Defines data models │
│ - Provides CRUD operations │
│ - Isolated from other modules │
└─────────────────────────────────────────────────┘
```
### Why This Pattern?
**Separation of Concerns**: Each layer has one responsibility
- API routes handle HTTP concerns (validation, serialization)
- Workflows handle business logic (orchestration, rollback)
- Modules handle data (CRUD, database)
**Reusability**: Workflows can be called from:
- Multiple API routes
- Other workflows
- Scheduled jobs
- Event subscribers
**Testability**: Each layer can be tested independently
**Consistency**: All features follow the same pattern
**Documentation**: [Learn more about Medusa Architecture](https://docs.medusajs.com/learn/introduction/architecture)
---
## What We're Building
In this lesson, we'll build a brands feature that allows admin users to create brands via an API endpoint.
**Features**:
- Create a `brand` table in the database
- Provide methods to manage brands (create, retrieve, update, delete)
- Expose POST /admin/brands endpoint to create brands
- Include validation and error handling
- Add rollback logic if errors occur
**By the end**, you'll be able to:
```bash
curl -X POST 'http://localhost:9000/admin/brands' \
-H 'Authorization: Bearer {token}' \
--data '{ "name": "Acme" }'
```
And get back:
```json
{
"brand": {
"id": "brand_123",
"name": "Acme",
"created_at": "2024-01-16T...",
"updated_at": "2024-01-16T..."
}
}
```
Let's start!
---
## Part 1: Create the Brand Module
### What is a Module?
A **Module** is a reusable package of functionality for a single domain. Think of it as a mini-application within Medusa that:
- Defines data models (tables in the database)
- Provides a service with CRUD methods
- Is isolated from other modules (no direct dependencies)
Medusa comes with built-in modules like:
- **Product Module**: Manages products, variants, options
- **Cart Module**: Manages shopping carts
- **Customer Module**: Manages customers
We're creating a **Brand Module** for managing brands.
**Documentation**: [Modules Guide](https://docs.medusajs.com/learn/fundamentals/modules)
### Step 1.1: Create Module Directory
Create the directory structure for the Brand Module:
```bash
mkdir -p src/modules/brand/models
```
**Why this structure?**
- Modules MUST be in `src/modules/`
- Data models MUST be in a `models/` subdirectory
- Medusa auto-discovers modules in this structure
### Step 1.2: Create Brand Data Model
A **data model** represents a table in the database. We use Medusa's Data Model Language (DML) to define it.
Create `src/modules/brand/models/brand.ts`:
```typescript
import { model } from "@medusajs/framework/utils"
export const Brand = model.define("brand", {
id: model.id().primaryKey(),
name: model.text(),
})
```
**Let's break this down**:
1. **`model.define("brand", { ... })`**:
- First arg: Table name in database (use snake-case)
- Second arg: Schema definition (columns)
2. **`id: model.id().primaryKey()`**:
- Creates a primary key column
- Auto-generates unique IDs
3. **`name: model.text()`**:
- Creates a text column for the brand name
**What about timestamps?**
Medusa automatically adds `created_at`, `updated_at`, and `deleted_at` columns!
**What about linkable()?**
Don't add `.linkable()` manually - Medusa adds it automatically. This is a common mistake!
**Documentation**: [Data Models Guide](https://docs.medusajs.com/learn/fundamentals/data-models)
### Step 1.3: Create Module Service
The **service** is the interface to your module's functionality. It provides methods to manage your data models.
Create `src/modules/brand/service.ts`:
```typescript
import { MedusaService } from "@medusajs/framework/utils"
import { Brand } from "./models/brand"
class BrandModuleService extends MedusaService({
Brand,
}) {
// Methods are auto-generated! No code needed here.
}
export default BrandModuleService
```
**What's happening here?**
`MedusaService({ Brand })` **generates** these methods automatically:
- `createBrands(data)` - Create one or more brands
- `retrieveBrand(id, config)` - Get a brand by ID
- `listBrands(filters, config)` - List brands with filters
- `updateBrands(id, data)` - Update a brand
- `deleteBrands(id)` - Delete a brand
- `softDeleteBrands(id)` - Soft delete (sets deleted_at)
- `restoreBrands(id)` - Restore soft-deleted brand
- `listAndCountBrands(filters, config)` - List with total count
You get all of these for free!
**Can you add custom methods?**
Yes! Add them inside the class body. But for basic CRUD, the generated methods are sufficient.
**Documentation**: [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference)
### Step 1.4: Export Module Definition
Every module must export a definition that tells Medusa:
- The module's name
- The module's main service
Create `src/modules/brand/index.ts`:
```typescript
import { Module } from "@medusajs/framework/utils"
import BrandModuleService from "./service"
export const BRAND_MODULE = "brand"
export default Module(BRAND_MODULE, {
service: BrandModuleService,
})
```
**Key points**:
1. **Module name MUST be camelCase**: "brand" ✓, "brand-module" ✗
- Using dashes will cause runtime errors!
2. **Export `BRAND_MODULE` constant**: Makes it easy to reference reliably elsewhere
3. **`Module()` creates the definition**: Registers the service with Medusa
### Step 1.5: Register Module in Config
Medusa needs to know about your custom module. Add it to `medusa-config.ts`:
```typescript
module.exports = defineConfig({
// ... existing config
modules: [
{
resolve: "./src/modules/brand",
},
],
})
```
**What if I already have a modules array?**
Add your module to the existing array:
```typescript
modules: [
{
resolve: "./src/modules/existing",
},
{
resolve: "./src/modules/brand", // Add this
},
],
```
### Step 1.6: Generate and Run Migrations
A **migration** is a file that defines database changes. It ensures your module is reusable and makes team collaboration smooth.
Run these commands:
```bash
npx medusa db:generate brand
npx medusa db:migrate
```
**What do these do?**
1. **`db:generate brand`**: Creates a migration file for the Brand Module
- Looks at your data models
- Generates SQL to create the `brand` table
- Saves it in `src/migrations/`
2. **`db:migrate`**: Runs all pending migrations
- Executes the SQL against your database
- Creates the `brand` table with columns: `id`, `name`, `created_at`, `updated_at`, `deleted_at`
**Documentation**: [Migrations Guide](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration)
---
## Checkpoint 1.1: Verify Module Creation
Before proceeding, let's verify the module is working.
### Verification Questions
Answer these to test your understanding:
1. **What does `MedusaService()` do?**
Click to reveal answer
It generates CRUD methods for your data models automatically.
2. **Why is the module name "brand" and not "brand-module"?**
Click to reveal answer
Module names must be camelCase. Dashes cause runtime resolution errors.
3. **What happens if you forget to run migrations?**
Click to reveal answer
The `brand` table won't exist in the database, so service methods will fail.
### Implementation Check
Run these commands and share the output:
1. **Check migrations succeeded**:
```bash
npx medusa db:migrate
```
Expected: "No pending migrations" or "Migrations complete"
2. **Check build succeeds**:
```bash
npm run build
```
Expected: No TypeScript errors
3. **Show me your files**:
- `src/modules/brand/models/brand.ts`
- `src/modules/brand/service.ts`
- `src/modules/brand/index.ts`
### Common Issues
**"Cannot find module 'brand'"**
- **Cause**: Module not registered in `medusa-config.ts`
- **Fix**: Add `{ resolve: "./src/modules/brand" }` to modules array
**"Module name must be camelCase"**
- **Cause**: Used dashes in module name
- **Fix**: Use "brand" not "brand-module" in `BRAND_MODULE`
**"Table brand already exists"**
- **Cause**: Migration already run or table manually created
- **Fix**: Drop the table or use a different name
**Build errors**
- Check all imports are correct
- Ensure TypeScript is happy with your code
- Share the error message for help debugging
### Testing Checklist
- [ ] Migration succeeded without errors
- [ ] Build succeeds (`npm run build`)
- [ ] Module registered in `medusa-config.ts`
- [ ] Service exports properly
---
## Part 2: Create the Brand Workflow
### What is a Workflow?
A **Workflow** orchestrates multiple operations that need to complete together. If any operation fails, the workflow automatically rolls back all previous operations.
**Why workflows?**
Imagine you're creating a brand AND uploading its logo to S3:
**Without Workflow** (Fragile):
```typescript
// Create brand
const brand = await brandService.createBrands({ name: "Acme" })
// Upload logo
await s3.upload(brand.id, logo) // What if this fails?
// Now you have a brand in DB but no logo!
// Manual cleanup required...
```
**With Workflow** (Robust):
```typescript
const workflow = createWorkflow("create-brand-with-logo", function (input) {
const brand = createBrandStep(input)
const upload = uploadLogoStep({ brandId: brand.id, logo: input.logo })
return new WorkflowResponse(brand)
})
// If upload fails, workflow automatically:
// 1. Calls uploadLogoStep compensation (cleanup S3)
// 2. Calls createBrandStep compensation (delete brand)
// 3. Returns error
// No orphaned data!
```
**Key Benefits**:
- **Automatic rollback**: Compensation functions undo changes
- **Transaction safety**: All or nothing
- **Retry logic**: Can retry failed steps
- **Composability**: Workflows can call other workflows
**Documentation**: [Workflows Guide](https://docs.medusajs.com/learn/fundamentals/workflows)
### Step 2.1: Create Brand Step
A **step** is the atomic unit of work in a workflow. Each step has:
- A step function (performs the action)
- A compensation function (undoes the action on error)
Create `src/workflows/steps/create-brand.ts`:
```typescript
import {
createStep,
StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { BRAND_MODULE } from "../modules/brand"
import BrandModuleService from "../modules/brand/service"
export type CreateBrandStepInput = {
name: string
}
export const createBrandStep = createStep(
"create-brand-step",
async (input: CreateBrandStepInput, { container }) => {
const brandModuleService: BrandModuleService = container.resolve(
BRAND_MODULE
)
const brand = await brandModuleService.createBrands(input)
return new StepResponse(brand, brand.id)
},
async (brandId, { container }) => {
if (!brandId) {
return
}
const brandModuleService: BrandModuleService = container.resolve(
BRAND_MODULE
)
await brandModuleService.deleteBrands(brandId)
}
)
```
**Let's break this down**:
**1. Step Function (2nd parameter)**:
```typescript
async (input: CreateBrandStepInput, { container }) => {
// Resolve the Brand Module service from Medusa container
const brandModuleService = container.resolve(BRAND_MODULE)
// Create the brand using the service
const brand = await brandModuleService.createBrands(input)
// Return StepResponse(data, compensationData)
return new StepResponse(brand, brand.id)
}
```
- **`input`**: Data passed to the step
- **`container`**: Medusa container - registry of all services, modules, tools
- **`container.resolve()`**: Gets a registered service by name
- **`StepResponse(data, compensationData)`**:
- `data`: Returned to the workflow (the brand object)
- `compensationData`: Passed to compensation function (brand ID)
**2. Compensation Function (3rd parameter)**:
```typescript
async (brandId, { container }) => {
if (!brandId) {
return
}
const brandModuleService: BrandModuleService = container.resolve(
BRAND_MODULE
)
await brandModuleService.deleteBrands(brandId)
}
```
- Receives the `compensationData` from StepResponse (brand ID)
- Undoes what the step did (deletes the brand)
- Called automatically if an error occurs later in the workflow
**Key Concept: The Medusa Container**
The **Medusa container** is a dependency injection container that holds:
- Core modules (Product, Cart, Customer, etc.)
- Custom modules (Brand)
- Services (logger, database, etc.)
- Framework tools (Link, Query, etc.)
You access them via `container.resolve()`:
```typescript
const brandService = container.resolve("brand")
const logger = container.resolve("logger")
const link = container.resolve("link")
```
**Documentation**: [Workflow Steps](https://docs.medusajs.com/learn/fundamentals/workflows#1-create-the-steps) | [Medusa Container](https://docs.medusajs.com/learn/fundamentals/medusa-container)
### Step 2.2: Create Brand Workflow
Now we compose the step into a workflow:
Create the workflow in `src/workflows/create-brand.ts`:
```typescript
import {
createWorkflow,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { createBrandStep } from "./steps/create-brand.ts"
type CreateBrandWorkflowInput = {
name: string
}
export const createBrandWorkflow = createWorkflow(
"create-brand",
function (input: CreateBrandWorkflowInput) {
const brand = createBrandStep(input)
return new WorkflowResponse(brand)
}
)
```
**CRITICAL: Workflow Constructor Rules**
The workflow constructor function has strict constraints:
```typescript
// ✅ CORRECT
createWorkflow("name", function (input) {
const result = myStep(input) // No await!
return new WorkflowResponse(result)
})
// ❌ WRONG - Will break!
createWorkflow("name", async function (input) { // No async!
const result = await myStep(input) // No await!
if (input.condition) { ... } // No conditionals!
return new WorkflowResponse(result)
})
```
**Why these rules?**
Workflows are **declarative**, not imperative. The constructor function:
- Runs at **load time**, not execution time
- Defines the **graph of steps**, not the execution
- Cannot have runtime logic (conditionals, loops)
**For runtime logic, use**:
- `when()` - Conditional step execution
- `transform()` - Data transformation
- `parallelize()` - Parallel execution
**Common Mistake**: Using `async` or `await`
```typescript
// ❌ WRONG
const brand = await createBrandStep(input) // No await!
// ✅ CORRECT
const brand = createBrandStep(input) // Step returns immediately
```
**Documentation**: [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows)
---
## Checkpoint 1.2: Verify Workflow
### Verification Questions
1. **Why can't you use `await` in the workflow constructor?**
Click to reveal answer
The workflow constructor runs at load time to define the step graph, not at execution time. Steps are executed later by the workflow engine.
2. **What does the compensation function do?**
Click to reveal answer
It undoes what the step did if an error occurs later in the workflow, maintaining data consistency.
3. **Why pass `brand.id` as the second parameter to `StepResponse`?**
Click to reveal answer
This data is passed to the compensation function so it knows which brand to delete if rollback is needed.
### Implementation Check
1. **Check build succeeds**:
```bash
npm run build
```
Expected: No TypeScript errors
2. **Show me your file**:
- `src/workflows/create-brand.ts`
### Common Issues
**"Async function not allowed"**
- **Cause**: Used `async` keyword in workflow constructor
- **Fix**: Remove `async`:
```typescript
// ❌ Wrong
createWorkflow("name", async (input) => { ... })
// ✅ Correct
createWorkflow("name", function (input) { ... })
```
**"Cannot use await"**
- **Cause**: Used `await` to call step
- **Fix**: Remove `await`:
```typescript
// ❌ Wrong
const brand = await createBrandStep(input)
// ✅ Correct
const brand = createBrandStep(input)
```
**"Arrow functions not allowed"**
- **Cause**: Used arrow function for workflow constructor
- **Fix**: Use `function` keyword:
```typescript
// ❌ Wrong
createWorkflow("name", (input) => { ... })
// ✅ Correct
createWorkflow("name", function (input) { ... })
```
### Testing Checklist
- [ ] Build succeeds (`npm run build`)
- [ ] No TypeScript errors
- [ ] Workflow uses `function`, not arrow function
- [ ] No `async` keyword in workflow constructor
- [ ] No `await` when calling steps
---
## Part 3: Create the API Route
### What is an API Route?
An **API Route** is a REST endpoint that exposes your features to clients:
- Admin dashboard
- Storefront
- Mobile apps
- Third-party integrations
**Key Principle**: Routes are THIN
- Validate input
- Execute workflow
- Return response
**All business logic belongs in workflows!**
**Documentation**: [API Routes Guide](https://docs.medusajs.com/learn/fundamentals/api-routes)
### Step 3.1: Create Validation Schema
We use **Zod** to validate request bodies. Create `src/api/admin/brands/validators.ts`:
```typescript
import { z } from "@medusajs/framework/zod"
export const PostAdminCreateBrand = z.object({
name: z.string(),
})
export type PostAdminCreateBrandType = z.infer
```
**What's happening?**
- **Zod schema**: Defines expected request body shape
- **`z.string()`**: Name must be a string
- **`z.infer`**: Extracts TypeScript type from schema
**Why separate file?**
- Keeps route file clean
- Makes schemas reusable
- Follows Medusa conventions
**Documentation**: [API Validation Guide](https://docs.medusajs.com/learn/fundamentals/api-routes/validation)
### Step 3.2: Create the API Route
The route path is determined by file location. For `/admin/brands`, create `src/api/admin/brands/route.ts`:
```typescript
import {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { createBrandWorkflow } from "../../../workflows/create-brand"
import { PostAdminCreateBrandType } from "./validators"
export const POST = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const { result } = await createBrandWorkflow(req.scope)
.run({
input: req.validatedBody,
})
res.json({ brand: result })
}
```
**Let's break this down**:
**1. Route Handler Export**:
```typescript
export const POST = async (req, res) => { ... }
```
- Export function named after HTTP method (POST, GET, DELETE)
- Medusa automatically registers this as `POST /admin/brands`
**2. Request Type**:
```typescript
req: MedusaRequest
```
- `MedusaRequest`: Type-safe request object
- `T` is the validated body type
- Access validated body via `req.validatedBody`
**3. Execute Workflow**:
```typescript
const { result } = await createBrandWorkflow(req.scope).run({
input: req.validatedBody,
})
```
- `req.scope`: The Medusa container
- `.run()`: Executes the workflow
- `input`: Data passed to workflow
- `result`: Data returned by workflow
**4. Return Response**:
```typescript
res.json({ brand: result })
```
- Returns JSON response to client
**Path Convention**:
```
File path: src/api/admin/brands/route.ts
Route path: POST /admin/brands
File path: src/api/admin/brands/[id]/route.ts
Route path: POST /admin/brands/:id
File path: src/api/store/products/route.ts
Route path: GET /store/products
```
**Documentation**: [Route Parameters](https://docs.medusajs.com/learn/fundamentals/api-routes/parameters)
### Step 3.3: Add Validation Middleware
**Middlewares** are functions that run before the route handler. They're useful for:
- Validation
- Authentication
- Custom parsing
Medusa provides `validateAndTransformBody` to validate request bodies using Zod schemas.
Create or update `src/api/middlewares.ts`:
```typescript
import {
defineMiddlewares,
validateAndTransformBody,
} from "@medusajs/framework/http"
import { PostAdminCreateBrand } from "./admin/brands/validators"
export default defineMiddlewares({
routes: [
{
matcher: "/admin/brands",
method: "POST",
middlewares: [
validateAndTransformBody(PostAdminCreateBrand),
],
},
],
})
```
**What's happening?**
**1. Define Middlewares**:
```typescript
export default defineMiddlewares({ routes: [...] })
```
- Must export default from `src/api/middlewares.ts`
- Medusa auto-loads this file
**2. Route Configuration**:
```typescript
{
matcher: "/admin/brands", // Route path
method: "POST", // HTTP method
middlewares: [...] // Middlewares to apply
}
```
**3. Validation Middleware**:
```typescript
validateAndTransformBody(PostAdminCreateBrand)
```
- Validates request body against Zod schema
- Returns 400 error if validation fails
- Populates `req.validatedBody` if validation succeeds
**Common Mistake**: Typo in filename
- MUST be `middlewares.ts` (plural)
- NOT `middleware.ts` (singular)
- Typo causes middleware to be ignored silently!
**Documentation**: [Middlewares Guide](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares) | [Validation Middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/validation)
---
## Checkpoint 1.3: Test the API Route
### Verification Questions
1. **Why is business logic in workflows, not routes?**
Click to reveal answer
Routes are entry points. Workflows can be reused from multiple routes, scheduled jobs, event subscribers. Keeps logic centralized and testable.
2. **What happens if validation fails?**
Click to reveal answer
The `validateAndTransformBody` middleware returns a 400 error with details about what failed. The route handler never runs.
3. **Why pass `req.scope` to the workflow?**
Click to reveal answer
`req.scope` is the Medusa container. The workflow needs it to resolve services and modules.
### Implementation Check
1. **Check build succeeds**:
```bash
npm run build
```
2. **Show me your files**:
- `src/api/admin/brands/validators.ts`
- `src/api/admin/brands/route.ts`
- `src/api/middlewares.ts`
### Test the API
Now let's test the complete feature!
**Step 1: Start the development server**
```bash
npm run dev
```
**Step 2: Get admin authentication token**
Since `/admin/brands` requires authentication, get a token first:
```bash
curl -X POST 'http://localhost:9000/auth/user/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
"email": "admin@medusa-test.com",
"password": "supersecret"
}'
```
Replace with your admin email/password.
**Don't have an admin user?** Create one:
```bash
npx medusa user -e admin@test.com -p supersecret
```
**Step 3: Create a brand**
Using the token from step 2:
```bash
curl -X POST 'http://localhost:9000/admin/brands' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
"name": "Acme"
}'
```
**Expected Response**:
```json
{
"brand": {
"id": "brand_01HQXYZ...",
"name": "Acme",
"created_at": "2024-01-16T10:30:00.000Z",
"updated_at": "2024-01-16T10:30:00.000Z"
}
}
```
### Common Issues
**401 Unauthorized**
- **Cause**: Token expired or invalid credentials
- **Fix**: Get fresh token from `/auth/user/emailpass`
**Empty array returned `[]`**
- **Cause**: Middleware file typo - probably named `middleware.ts` instead of `middlewares.ts`
- **Fix**: Rename to `src/api/middlewares.ts` (plural)
**400 Validation error**
- **Cause**: Request body doesn't match Zod schema
- **Fix**: Ensure you're sending `{ "name": "Acme" }` with correct JSON
**500 Server error**
- Check server logs for details
- Common causes:
- Module not registered in config
- Migration not run
- Workflow syntax error
### Testing Checklist
- [ ] Dev server running
- [ ] Authentication token obtained
- [ ] Brand created successfully via cURL
- [ ] Response contains brand with ID, name, timestamps
---
## Lesson 1 Complete! 🎉
### What You Built
Congratulations! You just built a complete custom feature in Medusa:
- ✅ **Brand Module**: Data model + auto-generated service
- ✅ **createBrandWorkflow**: Business logic with rollback
- ✅ **POST /admin/brands**: API endpoint with validation
- ✅ **Tested**: Created a brand via cURL
### What You Learned
**Architecture**:
- Module → Workflow → API Route pattern
- Why each layer exists and what it's responsible for
- How they connect together
**Modules**:
- Data models define database tables
- Services provide CRUD operations
- Modules are isolated and reusable
**Workflows**:
- Orchestrate multi-step operations
- Provide automatic rollback via compensation functions
- Ensure data consistency
**API Routes**:
- Expose features to clients
- Validate input via middlewares
- Execute workflows (keep routes thin!)
### Architecture Reinforcement
Before moving to Lesson 2, reflect on these questions:
**1. Why can't I call `brandModuleService` directly from the API route?**
Think about it, then expand:
Answer
While you *could* do:
```typescript
export const POST = async (req, res) => {
const brandService = req.scope.resolve("brand")
const brand = await brandService.createBrands(req.body)
res.json({ brand })
}
```
**Problems**:
- No rollback if subsequent operations fail
- Can't reuse logic elsewhere (scheduled jobs, other routes)
- Hard to test
- Violates separation of concerns
**Workflows solve this** by:
- Providing automatic rollback
- Being reusable from anywhere
- Having clear interfaces
- Being independently testable
**2. What happens if there's an error creating the brand?**
Answer
The workflow's compensation function (`createBrandStep`'s 3rd parameter) is called automatically, which deletes the brand. This ensures no orphaned data.
**3. Where would I add business validation (e.g., "brand name must be unique")?**
Answer
In a workflow step, NOT the API route!
```typescript
export const validateBrandNameStep = createStep(
"validate-brand-name",
async ({ name }, { container }) => {
const brandService = container.resolve("brand")
const existing = await brandService.listBrands({ name })
if (existing.length > 0) {
throw new Error("Brand name must be unique")
}
return new StepResponse({ validated: true })
}
)
// Then in workflow:
export const createBrandWorkflow = createWorkflow(
"create-brand",
function (input) {
validateBrandNameStep(input)
const brand = createBrandStep(input)
return new WorkflowResponse(brand)
}
)
```
### Commit Your Work
Save your progress:
```bash
git add .
git commit -m "Complete Lesson 1: Brand Module, Workflow, and API Route"
```
---
## Ready for Lesson 2?
In **Lesson 2: Extend Medusa**, you'll learn how to:
- **Link brands to products** using Module Links (maintain module isolation)
- **Extend core workflows** using Workflow Hooks (add custom logic to Medusa's workflows)
- **Query linked data** across modules using Query
You'll be able to:
- Create a product with a brand: `POST /admin/products` with `additional_data: { brand_id: "..." }`
- Retrieve a product's brand: `GET /admin/products/:id?fields=+brand.*`
- List all brands with their products: `GET /admin/brands` returning linked products
**Documentation**: [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links) | [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks) | [Query Guide](https://docs.medusajs.com/learn/fundamentals/query)
When you're ready, let me know and we'll start Lesson 2!