# Medusa Commerce Platform Medusa is an open-source commerce platform with a built-in framework for customization that allows you to build custom commerce applications without reinventing core commerce logic. It's a TypeScript monorepo containing 30+ modular commerce packages including products, orders, carts, payments, fulfillment, inventory, pricing, and more. The framework supports advanced B2B, DTC stores, marketplaces, PoS systems, and similar solutions requiring foundational commerce primitives. The platform follows a modular architecture where commerce features are implemented as isolated modules. API routes use workflows from `@medusajs/core-flows` to orchestrate business logic across modules. The codebase is organized with packages for the main Medusa server, core framework utilities, commerce modules, admin dashboard, CLI tools, and design system components. All commerce modules are open-source and available on npm, designed to be used together or independently based on your requirements. --- ## Admin API - List Orders The Admin Orders API allows authenticated admin users to retrieve and manage orders. Orders are fetched using the `getOrdersListWorkflow` which handles pagination, filtering, and field selection. The endpoint supports query parameters for filtering by status, customer, dates, and other order properties. ```typescript // packages/medusa/src/api/admin/orders/route.ts import { getOrdersListWorkflow } from "@medusajs/core-flows" import { HttpTypes, OrderDTO } from "@medusajs/framework/types" import { AuthenticatedMedusaRequest, MedusaResponse, } from "@medusajs/framework/http" export const GET = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { const variables = { filters: { ...req.filterableFields, is_draft_order: false, }, ...req.queryConfig.pagination, } const workflow = getOrdersListWorkflow(req.scope) const { result } = await workflow.run({ input: { fields: req.queryConfig.fields, variables, }, }) const { rows, metadata } = result as { rows: OrderDTO[] metadata: any } res.json({ orders: rows as unknown as HttpTypes.AdminOrder[], count: metadata.count, offset: metadata.skip, limit: metadata.take, }) } // curl example: // curl -X GET "http://localhost:9000/admin/orders?limit=10&offset=0" \ // -H "Authorization: Bearer " \ // -H "Content-Type: application/json" ``` --- ## Admin API - Create Product The Create Product endpoint uses the `createProductsWorkflow` to create products with options, variants, and pricing. Products are created with validation for required options, associated with sales channels, and linked to shipping profiles. The workflow returns the created product with all variants populated. ```typescript // packages/medusa/src/api/admin/products/route.ts import { createProductsWorkflow } from "@medusajs/core-flows" import { AuthenticatedMedusaRequest, MedusaResponse, refetchEntity, } from "@medusajs/framework/http" import { AdditionalData, HttpTypes } from "@medusajs/framework/types" export const POST = async ( req: AuthenticatedMedusaRequest< HttpTypes.AdminCreateProduct & AdditionalData, HttpTypes.SelectParams >, res: MedusaResponse ) => { const { additional_data, ...products } = req.validatedBody const { result } = await createProductsWorkflow(req.scope).run({ input: { products: [products], additional_data }, }) const product = await refetchEntity({ entity: "product", idOrFilter: result[0].id, scope: req.scope, fields: req.queryConfig.fields ?? [], }) res.status(200).json({ product }) } // curl example: // curl -X POST "http://localhost:9000/admin/products" \ // -H "Authorization: Bearer " \ // -H "Content-Type: application/json" \ // -d '{ // "title": "Medusa T-Shirt", // "status": "published", // "options": [{"title": "Size", "values": ["S", "M", "L"]}], // "variants": [{ // "title": "Small Shirt", // "sku": "SHIRT-S", // "options": {"Size": "S"}, // "prices": [{"amount": 2000, "currency_code": "usd"}], // "manage_inventory": true // }], // "shipping_profile_id": "sp_123" // }' ``` --- ## Admin API - Get/Update/Delete Product The product detail endpoint supports retrieving a single product by ID, updating product properties, and soft-deleting products. Updates use the `updateProductsWorkflow` while deletions use `deleteProductsWorkflow`. All operations return standardized response formats. ```typescript // packages/medusa/src/api/admin/products/[id]/route.ts import { deleteProductsWorkflow, updateProductsWorkflow, } from "@medusajs/core-flows" import { AuthenticatedMedusaRequest, MedusaResponse, refetchEntity, } from "@medusajs/framework/http" import { MedusaError } from "@medusajs/framework/utils" import { AdditionalData, HttpTypes } from "@medusajs/framework/types" export const GET = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { const product = await refetchEntity({ entity: "product", idOrFilter: req.params.id, scope: req.scope, fields: req.queryConfig.fields ?? [], }) if (!product) { throw new MedusaError(MedusaError.Types.NOT_FOUND, "Product not found") } res.status(200).json({ product }) } export const POST = async ( req: AuthenticatedMedusaRequest< HttpTypes.AdminUpdateProduct & AdditionalData, HttpTypes.SelectParams >, res: MedusaResponse ) => { const { additional_data, ...update } = req.validatedBody const { result } = await updateProductsWorkflow(req.scope).run({ input: { selector: { id: req.params.id }, update, additional_data, }, }) const product = await refetchEntity({ entity: "product", idOrFilter: result[0].id, scope: req.scope, fields: req.queryConfig.fields ?? [], }) res.status(200).json({ product }) } export const DELETE = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { const id = req.params.id await deleteProductsWorkflow(req.scope).run({ input: { ids: [id] }, }) res.status(200).json({ id, object: "product", deleted: true, }) } // curl examples: // GET: curl -X GET "http://localhost:9000/admin/products/prod_123" \ // -H "Authorization: Bearer " // // UPDATE: curl -X POST "http://localhost:9000/admin/products/prod_123" \ // -H "Authorization: Bearer " \ // -H "Content-Type: application/json" \ // -d '{"title": "Updated T-Shirt"}' // // DELETE: curl -X DELETE "http://localhost:9000/admin/products/prod_123" \ // -H "Authorization: Bearer " ``` --- ## Store API - List Products The Store Products API allows public access to retrieve products with calculated prices for the storefront. It supports the index engine for fast queries, pricing context for region/currency-specific prices, and inventory quantity calculation. Products are filtered by sales channels and returned with tax-inclusive pricing when applicable. ```typescript // packages/medusa/src/api/store/products/route.ts import { MedusaResponse } from "@medusajs/framework/http" import { HttpTypes, QueryContextType } from "@medusajs/framework/types" import { ContainerRegistrationKeys, isPresent, QueryContext, } from "@medusajs/framework/utils" export const GET = async ( req: RequestWithContext, res: MedusaResponse ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) const context: QueryContextType = {} if (isPresent(req.pricingContext)) { context["variants"] ??= {} context["variants"]["calculated_price"] ??= QueryContext( req.pricingContext! ) } const { data: products = [], metadata } = await query.graph( { entity: "product", fields: req.queryConfig.fields, filters: req.filterableFields, pagination: req.queryConfig.pagination, context, }, { cache: { enable: true }, locale: req.locale, } ) res.json({ products, count: metadata!.count, offset: metadata!.skip, limit: metadata!.take, }) } // curl example: // curl -X GET "http://localhost:9000/store/products?limit=12&fields=*variants.calculated_price" \ // -H "Content-Type: application/json" \ // -H "x-publishable-api-key: " ``` --- ## Store API - Create Cart The Store Cart API enables creating shopping carts for customers. Carts are created via the `createCartWorkflow` which handles customer association, region selection, and initial setup. The workflow supports additional data for custom extensions and returns the fully populated cart. ```typescript // packages/medusa/src/api/store/carts/route.ts import { createCartWorkflow } from "@medusajs/core-flows" import { AdditionalData, CreateCartWorkflowInputDTO, HttpTypes, } from "@medusajs/framework/types" import { AuthenticatedMedusaRequest, MedusaResponse, } from "@medusajs/framework/http" export const POST = async ( req: AuthenticatedMedusaRequest< HttpTypes.StoreCreateCart & AdditionalData, HttpTypes.SelectParams >, res: MedusaResponse ) => { const workflowInput = { ...req.validatedBody, customer_id: req.auth_context?.actor_id, } const { result } = await createCartWorkflow(req.scope).run({ input: workflowInput as CreateCartWorkflowInputDTO, }) // refetchCart gets the full cart with all relations const cart = await refetchCart(result.id, req.scope, req.queryConfig.fields) res.status(200).json({ cart }) } // curl example: // curl -X POST "http://localhost:9000/store/carts" \ // -H "Content-Type: application/json" \ // -H "x-publishable-api-key: " \ // -d '{ // "region_id": "reg_123", // "sales_channel_id": "sc_123", // "items": [{ // "variant_id": "variant_123", // "quantity": 1 // }] // }' ``` --- ## Store API - Customer Profile The authenticated customer endpoint allows logged-in customers to retrieve and update their profile. It uses the `auth_context.actor_id` from the JWT token to identify the customer and the `updateCustomersWorkflow` for profile updates. ```typescript // packages/medusa/src/api/store/customers/me/route.ts import { AuthenticatedMedusaRequest, MedusaResponse, } from "@medusajs/framework/http" import { MedusaError } from "@medusajs/framework/utils" import { updateCustomersWorkflow } from "@medusajs/core-flows" import { HttpTypes } from "@medusajs/framework/types" export const GET = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { const id = req.auth_context.actor_id const customer = await refetchCustomer(id, req.scope, req.queryConfig.fields) if (!customer) { throw new MedusaError( MedusaError.Types.NOT_FOUND, `Customer with id: ${id} was not found` ) } res.json({ customer }) } export const POST = async ( req: AuthenticatedMedusaRequest< HttpTypes.StoreUpdateCustomer, HttpTypes.SelectParams >, res: MedusaResponse ) => { const customerId = req.auth_context.actor_id await updateCustomersWorkflow(req.scope).run({ input: { selector: { id: customerId }, update: req.validatedBody, }, }) const customer = await refetchCustomer( customerId, req.scope, req.queryConfig.fields ) res.status(200).json({ customer }) } // curl examples: // GET: curl -X GET "http://localhost:9000/store/customers/me" \ // -H "Authorization: Bearer " // // UPDATE: curl -X POST "http://localhost:9000/store/customers/me" \ // -H "Authorization: Bearer " \ // -H "Content-Type: application/json" \ // -d '{"first_name": "John", "last_name": "Doe"}' ``` --- ## Authentication API The authentication API handles user login for both admin users and customers. It supports multiple authentication providers (emailpass, Google, etc.) and returns JWT tokens on successful authentication. The API is parameterized by actor type (user/customer) and auth provider. ```typescript // packages/medusa/src/api/auth/[actor_type]/[auth_provider]/route.ts import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" import { AuthenticationInput, ConfigModule, IAuthModuleService, } from "@medusajs/framework/types" import { ContainerRegistrationKeys, MedusaError, Modules, } from "@medusajs/framework/utils" export const POST = async (req: MedusaRequest, res: MedusaResponse) => { const { actor_type, auth_provider } = req.params const config: ConfigModule = req.scope.resolve( ContainerRegistrationKeys.CONFIG_MODULE ) const service: IAuthModuleService = req.scope.resolve(Modules.AUTH) const authData = { url: req.url, headers: req.headers, query: req.query, body: req.body, protocol: req.protocol, } as AuthenticationInput const { success, error, authIdentity, location } = await service.authenticate( auth_provider, authData ) if (location) { return res.status(200).json({ location }) } if (success && authIdentity) { const { http } = config.projectConfig const token = await generateJwtTokenForAuthIdentity( { authIdentity, actorType: actor_type, authProvider: auth_provider, container: req.scope, }, { secret: http.jwtSecret!, expiresIn: http.jwtExpiresIn, options: http.jwtOptions, } ) return res.status(200).json({ token }) } throw new MedusaError( MedusaError.Types.UNAUTHORIZED, error || "Authentication failed" ) } // curl examples: // Admin login: // curl -X POST "http://localhost:9000/auth/user/emailpass" \ // -H "Content-Type: application/json" \ // -d '{"email": "admin@example.com", "password": "supersecret"}' // // Customer login: // curl -X POST "http://localhost:9000/auth/customer/emailpass" \ // -H "Content-Type: application/json" \ // -d '{"email": "customer@example.com", "password": "password123"}' ``` --- ## Workflow - Create Products The `createProductsWorkflow` orchestrates product creation across multiple modules. It validates product input, creates products with options, associates sales channels, links shipping profiles, creates variants with prices, and emits events. The workflow supports hooks for custom post-creation logic. ```typescript // packages/core/core-flows/src/product/workflows/create-products.ts import { WorkflowData, WorkflowResponse, createHook, createWorkflow, transform, } from "@medusajs/framework/workflows-sdk" import { ProductWorkflowEvents, Modules } from "@medusajs/framework/utils" import { createRemoteLinkStep, emitEventStep } from "../../common" import { associateProductsWithSalesChannelsStep } from "../../sales-channel" import { createProductsStep } from "../steps/create-products" import { createProductVariantsWorkflow } from "./create-product-variants" export type CreateProductsWorkflowInput = { products: CreateProductWorkflowInputDTO[] } & AdditionalData export const createProductsWorkflow = createWorkflow( "create-products", (input: WorkflowData) => { // Remove external relations for initial product creation const { products: productWithoutExternalRelations } = transform( { input }, (data) => ({ products: data.input.products.map((p) => ({ ...p, sales_channels: undefined, shipping_profile_id: undefined, variants: undefined, })), }) ) validateProductInputStep({ products: input.products }) const createdProducts = createProductsStep(productWithoutExternalRelations) // Create sales channel links const salesChannelLinks = transform({ input, createdProducts }, (data) => { return data.createdProducts .map((product, i) => data.input.products[i].sales_channels?.map((sc) => ({ sales_channel_id: sc.id, product_id: product.id, })) ?? [] ) .flat() }) associateProductsWithSalesChannelsStep({ links: salesChannelLinks }) // Create shipping profile links const shippingProfileLinks = transform({ input, createdProducts }, (data) => { return data.createdProducts .map((product, i) => ({ [Modules.PRODUCT]: { product_id: product.id }, [Modules.FULFILLMENT]: { shipping_profile_id: data.input.products[i].shipping_profile_id, }, })) .filter((link) => !!link[Modules.FULFILLMENT].shipping_profile_id) }) createRemoteLinkStep(shippingProfileLinks) // Create variants with prices const createdVariants = createProductVariantsWorkflow.runAsStep({ input: { product_variants: /* prepared variants */ }, }) emitEventStep({ eventName: ProductWorkflowEvents.CREATED, data: createdProducts.map((p) => ({ id: p.id })), }) const productsCreated = createHook("productsCreated", { products: createdProducts, additional_data: input.additional_data, }) return new WorkflowResponse(createdProducts, { hooks: [productsCreated], }) } ) // Usage example: const { result } = await createProductsWorkflow(container).run({ input: { products: [{ title: "Shirt", options: [{ title: "Size", values: ["S", "M", "L"] }], variants: [{ title: "Small Shirt", sku: "SMALLSHIRT", options: { Size: "S" }, prices: [{ amount: 10, currency_code: "usd" }], manage_inventory: true, }], shipping_profile_id: "sp_123", }], additional_data: { erp_id: "123" }, }, }) ``` --- ## Workflow - Add to Cart The `addToCartWorkflow` manages adding product variants to a cart as line items. It handles inventory confirmation, price calculation with custom pricing context, line item creation/updates, and cart refresh. The workflow uses locking to prevent race conditions and supports hooks for validation and pricing customization. ```typescript // packages/core/core-flows/src/cart/workflows/add-to-cart.ts import { createHook, createWorkflow, parallelize, transform, when, WorkflowResponse, } from "@medusajs/framework/workflows-sdk" import { CartWorkflowEvents } from "@medusajs/framework/utils" import { emitEventStep } from "../../common/steps/emit-event" import { acquireLockStep, releaseLockStep } from "../../locking" import { createLineItemsStep, updateLineItemsStep } from "../steps" import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory" import { refreshCartItemsWorkflow } from "./refresh-cart-items" export const addToCartWorkflow = createWorkflow( { name: "add-to-cart", idempotent: false }, (input: AddToCartWorkflowInputDTO & AdditionalData) => { // Acquire lock to prevent race conditions acquireLockStep({ key: input.cart_id, timeout: 2, ttl: 10 }) // Fetch cart with pricing context fields const { data: cart } = useQueryGraphStep({ entity: "cart", filters: { id: input.cart_id }, fields: cartFields, options: { throwIfKeyNotFound: true, isList: false }, }).config({ name: "get-cart" }) validateCartStep({ cart }) // Hook for custom validation const validate = createHook("validate", { input, cart }) // Hook for custom pricing context const setPricingContext = createHook("setPricingContext", { cart, variantIds, items: input.items, additional_data: input.additional_data, }) // Get variants with calculated prices const { variants, lineItems } = getVariantsAndItemsWithPrices.runAsStep({ input: { cart, items: input.items, setPricingContextResult: setPricingContext.getResult(), }, }) validateLineItemPricesStep({ items: lineItems }) // Confirm inventory availability confirmVariantInventoryWorkflow.runAsStep({ input: { sales_channel_id: cart.sales_channel_id, variants, items: input.items, }, }) // Create or update line items const [createdLineItems, updatedLineItems] = parallelize( createLineItemsStep({ id: cart.id, items: itemsToCreate }), updateLineItemsStep({ id: cart.id, items: itemsToUpdate }) ) // Refresh cart totals refreshCartItemsWorkflow.runAsStep({ input: { cart_id: cart.id, items: allItems }, }) parallelize( emitEventStep({ eventName: CartWorkflowEvents.UPDATED, data: { id: cart.id } }), releaseLockStep({ key: cart.id }) ) return new WorkflowResponse(void 0, { hooks: [validate, setPricingContext], }) } ) // Usage example: await addToCartWorkflow(container).run({ input: { cart_id: "cart_123", items: [ { variant_id: "variant_123", quantity: 1 }, { variant_id: "variant_456", quantity: 2, unit_price: 1500 }, ], }, }) ``` --- ## Workflow - Create Order The `createOrderWorkflow` creates orders with line items, shipping, and pricing. It validates inventory, creates the order with customer and region data, applies promotions, and calculates taxes. This workflow powers draft order creation and can be used for importing orders from external systems. ```typescript // packages/core/core-flows/src/order/workflows/create-order.ts import { WorkflowData, WorkflowResponse, createHook, createWorkflow, parallelize, transform, } from "@medusajs/framework/workflows-sdk" import { PromotionActions } from "@medusajs/framework/utils" import { findOneOrAnyRegionStep } from "../../cart/steps/find-one-or-any-region" import { findOrCreateCustomerStep } from "../../cart/steps/find-or-create-customer" import { findSalesChannelStep } from "../../cart/steps/find-sales-channel" import { confirmVariantInventoryWorkflow } from "../../cart/workflows/confirm-variant-inventory" import { createOrdersStep } from "../steps" import { updateOrderTaxLinesWorkflow } from "./update-tax-lines" import { refreshDraftOrderAdjustmentsWorkflow } from "../../draft-order/workflows" export type CreateOrderWorkflowInput = CreateOrderDTO & AdditionalData export const createOrderWorkflow = createWorkflow( "create-orders", (input: WorkflowData) => { // Fetch region, sales channel, and customer in parallel const [salesChannel, region, customerData] = parallelize( findSalesChannelStep({ salesChannelId: input.sales_channel_id }), findOneOrAnyRegionStep({ regionId: input.region_id }), findOrCreateCustomerStep({ customerId: input.customer_id, email: input.email }) ) // Hook for custom pricing context const setPricingContext = createHook("setPricingContext", { variantIds, region, customerData, additional_data: input.additional_data, }) // Fetch variants and calculate prices const variants = getVariantsAndItemsWithPrices.runAsStep({ input: { cart: { currency_code: input.currency_code, region, region_id: region.id }, items: input.items, setPricingContextResult: setPricingContext.getResult(), }, }) // Confirm inventory confirmVariantInventoryWorkflow.runAsStep({ input: { sales_channel_id: salesChannel.id, variants, items: input.items }, }) // Prepare and create order const orderInput = transform({ input, region, customerData, salesChannel }, (data) => ({ ...data.input, currency_code: data.input.currency_code ?? data.region.currency_code, region_id: data.region.id, customer_id: data.customerData.customer?.id, sales_channel_id: data.salesChannel?.id, })) const orders = createOrdersStep([orderInput]) const order = transform({ orders }, (data) => data.orders?.[0]) // Apply taxes and promotions parallelize( updateOrderTaxLinesWorkflow.runAsStep({ input: { order_id: order.id } }), refreshDraftOrderAdjustmentsWorkflow.runAsStep({ input: { order, promo_codes: input.promo_codes ?? [], action: PromotionActions.REPLACE }, }) ) const orderCreated = createHook("orderCreated", { order, additional_data: input.additional_data, }) return new WorkflowResponse(order, { hooks: [orderCreated, setPricingContext], }) } ) // Usage example: const { result } = await createOrderWorkflow(container).run({ input: { region_id: "reg_123", sales_channel_id: "sc_123", status: "pending", items: [{ variant_id: "variant_123", quantity: 1, title: "Shirt", unit_price: 2000, }], shipping_address: { first_name: "John", last_name: "Doe", address_1: "123 Main St", city: "Los Angeles", country_code: "us", postal_code: "90001", }, }, }) ``` --- ## Module Service Pattern Module services in Medusa extend `MedusaService` with typed model definitions and use decorators for cross-cutting concerns. Services inject dependencies via constructor, use `@InjectManager()` on public methods, `@InjectTransactionManager()` on protected methods, and `@MedusaContext()` for shared context. The `@EmitEvents()` decorator handles event emission after operations. ```typescript // packages/modules/product/src/services/product-module-service.ts import { Context, DAL, ProductTypes, IProductModuleService, } from "@medusajs/framework/types" import { EmitEvents, InjectManager, InjectTransactionManager, MedusaContext, MedusaError, MedusaService, } from "@medusajs/framework/utils" import { Product, ProductVariant, ProductCategory } from "@models" type InjectedDependencies = { baseRepository: DAL.RepositoryService productService: ModulesSdkTypes.IMedusaInternalService productVariantService: ModulesSdkTypes.IMedusaInternalService } export default class ProductModuleService extends MedusaService<{ Product: { dto: ProductTypes.ProductDTO } ProductVariant: { dto: ProductTypes.ProductVariantDTO } ProductCategory: { dto: ProductTypes.ProductCategoryDTO } }>({ Product, ProductVariant, ProductCategory, }) implements IProductModuleService { protected baseRepository_: DAL.RepositoryService protected readonly productService_: ModulesSdkTypes.IMedusaInternalService protected readonly productVariantService_: ModulesSdkTypes.IMedusaInternalService constructor(deps: InjectedDependencies, moduleOptions: Record) { super(...arguments) this.baseRepository_ = deps.baseRepository this.productService_ = deps.productService this.productVariantService_ = deps.productVariantService } // Public method with manager injection and event emission @InjectManager() @EmitEvents() async createProducts( data: ProductTypes.CreateProductDTO[], @MedusaContext() sharedContext: Context = {} ): Promise { return await this.createProducts_(data, sharedContext) } // Protected method with transaction manager for actual implementation @InjectTransactionManager() protected async createProducts_( data: ProductTypes.CreateProductDTO[], @MedusaContext() sharedContext: Context = {} ): Promise { const products = await this.productService_.create(data, sharedContext) return products } // Example of a retrieval method with error handling @InjectManager() async retrieveProduct( id: string, config?: FindConfig, @MedusaContext() sharedContext: Context = {} ): Promise { const product = await this.productService_.retrieve(id, config, sharedContext) if (!product) { throw new MedusaError( MedusaError.Types.NOT_FOUND, `Product with id: ${id} was not found` ) } return product } } ``` --- ## Workflow Step Pattern Workflow steps are created using `createStep()` with a main action and optional compensation function for rollback. Steps return `StepResponse` with the result and optional compensation data. The compensation function is called automatically if subsequent steps fail, ensuring data consistency. ```typescript // packages/core/core-flows/src/promotion/steps/delete-promotions.ts import { IPromotionModuleService } from "@medusajs/framework/types" import { Modules } from "@medusajs/framework/utils" import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" export const deletePromotionsStepId = "delete-promotions" export const deletePromotionsStep = createStep( deletePromotionsStepId, async (ids: string[], { container }) => { const promotionModule = container.resolve( Modules.PROMOTION ) await promotionModule.softDeletePromotions(ids) // Return compensation data (the IDs to restore if rollback needed) return new StepResponse(void 0, ids) }, // Compensation function - called on workflow failure async (idsToRestore, { container }) => { if (!idsToRestore?.length) { return } const promotionModule = container.resolve( Modules.PROMOTION ) await promotionModule.restorePromotions(idsToRestore) } ) // Using the step in a workflow import { createWorkflow, WorkflowData, WorkflowResponse, createHook, } from "@medusajs/framework/workflows-sdk" export const deletePromotionsWorkflow = createWorkflow( "delete-promotions", (input: WorkflowData<{ ids: string[] }>) => { const deletedPromotions = deletePromotionsStep(input.ids) // Create hook for post-deletion custom actions const promotionsDeleted = createHook("promotionsDeleted", { ids: input.ids, }) return new WorkflowResponse(deletedPromotions, { hooks: [promotionsDeleted], }) } ) // Usage: await deletePromotionsWorkflow(container).run({ input: { ids: ["promo_123", "promo_456"] }, }) ``` --- ## Error Handling Pattern Medusa uses `MedusaError` for all error throwing with specific error types for different scenarios. Error types include `NOT_FOUND`, `INVALID_DATA`, `NOT_ALLOWED`, `UNAUTHORIZED`, and `CONFLICT`. Errors should provide contextual, user-friendly messages and be thrown early in services and workflow steps. ```typescript // Error handling pattern example import { MedusaError, validateEmail } from "@medusajs/framework/utils" // NOT_FOUND - Resource doesn't exist async function retrieveOrder(id: string) { const order = await orderService.retrieve(id) if (!order) { throw new MedusaError( MedusaError.Types.NOT_FOUND, `Order with id: ${id} was not found` ) } return order } // INVALID_DATA - Invalid input or validation failure async function createCustomer(data: CreateCustomerDTO) { if (data.email) { // Built-in validation utility validateEmail(data.email) } if (!data.first_name || !data.last_name) { throw new MedusaError( MedusaError.Types.INVALID_DATA, "First name and last name are required" ) } return customerService.create(data) } // NOT_ALLOWED - Operation not permitted in current state async function updateOrder(id: string, update: UpdateOrderDTO) { const order = await orderService.retrieve(id) if (order.status === "cancelled") { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "Cannot update a cancelled order" ) } if (order.status === "completed" && update.items) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "Cannot modify items of a completed order" ) } return orderService.update(id, update) } // UNAUTHORIZED - Authentication/authorization failure async function getAdminResource(req: AuthenticatedMedusaRequest) { if (!req.auth_context?.actor_id) { throw new MedusaError( MedusaError.Types.UNAUTHORIZED, "You must be logged in to access this resource" ) } // Check permissions const user = await userService.retrieve(req.auth_context.actor_id) if (!user.is_admin) { throw new MedusaError( MedusaError.Types.UNAUTHORIZED, "Admin access required" ) } } // CONFLICT - Duplicate or conflicting state async function createProduct(data: CreateProductDTO) { const existing = await productService.list({ handle: data.handle }) if (existing.length > 0) { throw new MedusaError( MedusaError.Types.CONFLICT, `Product with handle "${data.handle}" already exists` ) } return productService.create(data) } ``` --- ## Import Patterns The Medusa framework organizes imports across several packages. Framework utilities, decorators, and types are imported from `@medusajs/framework/*`. Workflows and steps are imported from `@medusajs/framework/workflows-sdk`. Core flows (pre-built workflows) come from `@medusajs/core-flows`. HTTP types and utilities are from `@medusajs/framework/http`. ```typescript // Framework utilities and decorators import { InjectManager, InjectTransactionManager, MedusaContext, MedusaError, MedusaService, EmitEvents, Modules, ContainerRegistrationKeys, validateEmail, isPresent, isDefined, } from "@medusajs/framework/utils" // Framework types import type { Context, DAL, IOrderModuleService, IProductModuleService, HttpTypes, AdditionalData, CreateOrderDTO, FindConfig, } from "@medusajs/framework/types" // Workflow SDK import { WorkflowData, WorkflowResponse, createStep, createWorkflow, createHook, transform, when, parallelize, StepResponse, } from "@medusajs/framework/workflows-sdk" // Core flows (pre-built workflows) import { createProductsWorkflow, deleteProductsWorkflow, updateProductsWorkflow, createCartWorkflow, addToCartWorkflow, createOrderWorkflow, updateCustomersWorkflow, getOrdersListWorkflow, } from "@medusajs/core-flows" // HTTP utilities import { AuthenticatedMedusaRequest, MedusaRequest, MedusaResponse, refetchEntity, refetchEntities, } from "@medusajs/framework/http" // Module-specific models (using path aliases) import { Product, ProductVariant, ProductCategory } from "@models" import { ProductService, ProductCategoryService } from "@services" // Example API route with all imports import { deleteOrderWorkflow } from "@medusajs/core-flows" import { HttpTypes } from "@medusajs/framework/types" import { AuthenticatedMedusaRequest, MedusaResponse, } from "@medusajs/framework/http" export const DELETE = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { const { id } = req.params await deleteOrderWorkflow(req.scope).run({ input: { id }, }) res.status(200).json({ id, object: "order", deleted: true, }) } ``` --- Medusa is designed for building custom commerce applications with complex requirements like B2B, marketplaces, and multi-tenant platforms. The workflow-based architecture ensures data consistency across distributed modules through automatic compensation and rollback. Common integration patterns include: consuming workflow hooks for post-operation custom logic, extending products/orders with custom data via module links, implementing custom pricing through the pricing context hooks, and creating custom modules for third-party integrations. The API follows REST conventions with admin endpoints requiring authentication via Bearer token or API key, and store endpoints using publishable API keys. All write operations are orchestrated through workflows from `@medusajs/core-flows`, which handle validation, business logic, event emission, and error recovery. For custom functionality, developers create new modules with services extending `MedusaService`, define workflows using steps with compensation functions, and expose them through custom API routes following the established patterns.