www/apps/book/app/learn/fundamentals/framework/page.mdx
import { CardList, SplitSections, SplitSection, CodeTabs, CodeTab, SplitList } from "docs-ui"
export const metadata = {
title: ${pageNumber} Framework Overview,
}
In this chapter, you'll learn about the Medusa Framework and how it facilitates building customizations in your Medusa application.
All commerce application require some degree of customization. So, it's important to choose a platform that facilitates building those customizations.
When you build customizations with other ecommerce platforms, they require you to pull data through HTTP APIs, run custom logic that span across systems in a separate application, and manually ensure data consistency across systems. This adds significant overhead and slows down development as you spend time managing complex distributed systems.
The Medusa Framework eliminates this overhead by providing powerful low-level APIs and tools that let you build any type of customization directly within your Medusa project. You can build custom features, orchestrate operations and query data seamlessy across systems, extend core functionality, and automate tasks in your Medusa application.
With the Medusa Framework, you can focus your efforts on building meaningful business customizations and continuously delivering new features.
Using the Medusa Framework, you can build customizations like:
<SplitList items={[ { title: "Medusa Container", link: "/learn/fundamentals/medusa-container", }, { title: "Modules", link: "/learn/fundamentals/modules", }, { title: "Module Links", link: "/learn/fundamentals/module-links", }, { title: "Query", link: "/learn/fundamentals/module-links/query", }, { title: "Data Models", link: "/learn/fundamentals/data-models", }, { title: "Workflows", link: "/learn/fundamentals/workflows", }, { title: "API Routes", link: "/learn/fundamentals/api-routes", }, { title: "Events and Subscribers", link: "/learn/fundamentals/events-and-subscribers", }, { title: "Scheduled Jobs", link: "/learn/fundamentals/scheduled-jobs", }, { title: "Plugins", link: "/learn/fundamentals/plugins", } ]} listsNum={2} />
The Medusa Framework allows you to build custom features tailored to your business needs.
To create a custom feature, you can create a module that contains your feature's data models and the logic to manage them. A module is integrated into your Medusa application without side effects.
<CodeTabs group="module-customizations"> <CodeTab label="Data Model" value="data-model">export const modelHighlights = [ ["3", "Post", "Create a data model with Medusa's Data Model Language (DML)."] ]
import { model } from "@medusajs/framework/utils"
export const Post = model.define("post", {
id: model.id().primaryKey(),
title: model.text(),
})
export const serviceHighlights = [
["4", "MedusaService", "Save time by extending the MedusaService for basic CRUD operations."]
]
import { MedusaService } from "@medusajs/framework/utils"
import { Post } from "./post"
export class BlogModuleService extends MedusaService({
Post,
}){
// CRUD methods generated by MedusaService
}
import { Module } from "@medusajs/framework/utils"
import { BlogModuleService } from "./service"
export const BLOG_MODULE = "blog"
export default Module(BLOG_MODULE, {
service: BlogModuleService,
})
Then, you can build commerce features and flows in workflows that use your module. By using workflows, you benefit from features like rollback mechanism and retry configuration.
<CodeTabs group="workflow-customizations"> <CodeTab label="Step" value="step">export const stepHighlights = [ ["19", "", "Define the rollback logic for any step."] ]
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { BlogModuleService, BLOG_MODULE } from "../../modules/blog"
type Input = {
title: string
}
const createPostStep = createStep(
"create-post",
async (input: Input, { container }) => {
const blogModuleService: BlogModuleService = container.resolve(
BLOG_MODULE
)
const post = await blogModuleService.createPosts(input.title)
return new StepResponse(post, post.id)
},
async (postId, { container }) => {
if (!postId) {
return
}
const blogModuleService: BlogModuleService = container.resolve(
BLOG_MODULE
)
await blogModuleService.deletePosts(postId)
}
)
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createPostStep } from "./steps"
type Input = {
title: string
}
export const createPostWorkflow = createWorkflow(
"create-post",
(input: Input) => {
const post = createPostStep(input)
return new WorkflowResponse(post)
}
)
Finally, you can expose your custom feature with API routes that are built on top of your module and workflows.
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { createPostWorkflow } from "../../../workflows/create-post"
type PostRequestBody = {
title: string
}
export const POST = async (
req: MedusaRequest<PostRequestBody>,
res: MedusaResponse
) => {
const { result } = await createPostWorkflow(req.scope)
.run({
input: result.validatedBody,
})
return res.json(result)
}
The following tutorials are step-by-step guides that show you how to build custom features using the Medusa Framework.
<CardList items={[ { title: "Product Reviews", text: "Build a product reviews feature in your Medusa application.", href: "!resources!/how-to-tutorials/tutorials/product-reviews", }, { title: "Wishlist", text: "Build a wishlist feature in your Medusa application.", href: "!resources!/plugins/guides/wishlist", } ]} />
To learn more about the different concepts useful for building custom features, check out the following chapters:
<CardList items={[ { title: "Modules", href: "/learn/fundamentals/modules", }, { title: "Workflows", href: "/learn/fundamentals/workflows", }, { title: "API Routes", href: "/learn/fundamentals/api-routes", } ]} />
The Medusa Framework is flexible and extensible, allowing you to extend and build on top of existing models and features.
To associate new properties and relations with an existing model, you can create a module with data models that define these additions. Then, you can define a module link that associates two data models from separate modules.
<CodeTabs group="module-extensions"> <CodeTab label="Module Link" value="module-link">export const defineLinkHighlights = [ ["5", "defineLink", "Define a link between products and brands."] ]
import BrandModule from "../modules/brand"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"
export default defineLink(
{
linkable: ProductModule.linkable.product,
isList: true,
},
BrandModule.linkable.brand
)
import { model } from "@medusajs/framework/utils"
export const Brand = model.define("brand", {
id: model.id().primaryKey(),
name: model.text(),
})
import { MedusaService } from "@medusajs/framework/utils"
import { Brand } from "./models/brand"
class BrandModuleService extends MedusaService({
Brand,
}) {
}
export default BrandModuleService
import { Module } from "@medusajs/framework/utils"
import BrandModuleService from "./service"
export const BRAND_MODULE = "brand"
export default Module(BRAND_MODULE, {
service: BrandModuleService,
})
Then, you can hook into existing workflows to perform custom actions as part of existing features and flows. For example, you can create a brand when a product is created.
export const hookHighlights = [ ["8", "productsCreated", "Perform a custom action within the product creation flow."], ["9", "additional_data", "Custom data passed in the HTTP request body."] ]
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
import { StepResponse } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
import { LinkDefinition } from "@medusajs/framework/types"
import { BRAND_MODULE } from "../../modules/brand"
import BrandModuleService from "../../modules/brand/service"
createProductsWorkflow.hooks.productsCreated(
(async ({ products, additional_data }, { container }) => {
if (!additional_data?.brand_id) {
return new StepResponse([], [])
}
const brandModuleService: BrandModuleService = container.resolve(
BRAND_MODULE
)
const brand = await brandModuleService.createBrands({
name: additional_data.brand_name,
})
})
)
You can also build custom workflows using your custom module and Medusa's modules, and use existing workflows and steps within your custom workflows.
The following tutorials are step-by-step guides that show you how to extend existing features using the Medusa Framework.
<CardList items={[ { title: "Custom Item Pricing", text: "Add products with custom items to the cart.", href: "!resources!/examples/guides/custom-item-price", }, { title: "Loyalty Points System", text: "Reward and allow customers to redeem loyalty points.", href: "!resources!/how-to-tutorials/tutorials/loyalty-points", } ]} />
To learn more about the different concepts useful for extending features, check out the following chapters:
<CardList items={[ { title: "Modules", href: "/learn/fundamentals/modules", }, { title: "Module Links", href: "/learn/fundamentals/module-links", }, { title: "Workflow Hooks", href: "/learn/fundamentals/workflows/workflow-hooks", } ]} />
The Medusa Framework provides the tools and infrastructure to build a middleware solution for your commerce ecosystem. You can integrate third-party services, perform operations across systems, and query data from multiple sources.
The Medusa Framework solves one of the biggest hurdles for ecommerce platforms: orchestrating operations across systems. Medusa has a built-in durable execution engine to help complete tasks that span multiple systems.
You can integrate a third-party service in a module. This module provides an interface to perform operations with the third-party service.
<CodeTabs group="third-party-integration"> <CodeTab label="Service" value="service">export const erpServiceHighlights = [ ["5", "ErpModuleService", "Create a service that connects to the ERP system."], ]
type Options = {
apiKey: string
}
export default class ErpModuleService {
private options: Options
private client
constructor({}, options: Options) {
this.options = options
// TODO initialize client that connects to ERP
}
async getProducts() {
// assuming client has a method to fetch products
return this.client.getProducts()
}
// TODO add more methods
}
import { Module } from "@medusajs/framework/utils"
import ErpModuleService from "./service"
export const ERP_MODULE = "erp"
export default Module(ERP_MODULE, {
service: ErpModuleService,
})
Then, you can build workflows that perform operations across systems. In the workflow, you can use your module to interact with the integrated third-party service.
For example, you can create a workflow that syncs products from your ERP system to your Medusa application.
<CodeTabs group="erp-sync-workflow"> <CodeTab label="Workflow" value="workflow">export const erpWorkflowHighlights = [ ["11", "getProductsFromErpStep", "Get products from the ERP system."], ["13", "transform", "Prepare the products to be created in Medusa."], ["32", "createProductsWorkflow", "Create the ERP products in Medusa."], ]
import {
createWorkflow,
WorkflowResponse,
transform,
} from "@medusajs/framework/workflows-sdk"
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
export const syncFromErpWorkflow = createWorkflow(
"sync-from-erp",
() => {
const erpProducts = getProductsFromErpStep()
const productsToCreate = transform({
erpProducts,
}, (data) => {
// TODO prepare ERP products to be created in Medusa
return data.erpProducts.map((erpProduct) => {
return {
title: erpProduct.title,
external_id: erpProduct.id,
variants: erpProduct.variants.map((variant) => ({
title: variant.title,
metadata: {
external_id: variant.id,
},
})),
// other data...
}
})
})
createProductsWorkflow.runAsStep({
input: {
products: productsToCreate,
},
})
return new WorkflowResponse({
erpProducts,
})
}
)
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { ERP_MODULE } from "../../modules/erp"
import { ErpModuleService } from "../../modules/erp/service"
const getProductsFromErpStep = createStep(
"get-products-from-erp",
async (_, { container }) => {
const erpModuleService: ErpModuleService = container.resolve(
ERP_MODULE
)
const products = await erpModuleService.getProducts()
return new StepResponse(products)
}
)
By using a workflow to manage operations across systems, you benefit from features like rollback mechanism, background long-running execution, retry configuration, and more. This is essential for building a middleware solution that performs operations across systems, as you don't have to worry about data inconsistencies or failures.
You can then execute this workflow at a specific interval using scheduled jobs or when an event occurs using events and subscribers. You can also expose its features to client applications using an API route.
<CodeTabs group="erp-sync-api"> <CodeTab label="Scheduled Job" value="scheduled-job">export const syncProductsJobHighlights = [ ["6", "syncProductsJob", "Sync products once a day."], ]
import {
MedusaContainer,
} from "@medusajs/framework/types"
import { syncFromErpWorkflow } from "../workflows/sync-from-erp"
export default async function syncProductsJob(container: MedusaContainer) {
await syncFromErpWorkflow(container).run({})
}
export const config = {
name: "daily-product-sync",
schedule: "0 0 * * *", // Every day at midnight
}
export const productsCreatedHandlerHighlights = [ ["4", "productsCreatedHandler", "Sync products when they are created."], ]
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation"
export default async function productsCreatedHandler({
event: { data },
container,
}: SubscriberArgs<{ id: string }[]>) {
await syncFromErpWorkflow(container).run({})
}
export const config: SubscriberConfig = {
event: `product.created`,
}
export const apiRouteHighlights = [ ["7", "POST", "Expose a REST API route to sync products."], ]
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { syncFromErpWorkflow } from "../../../workflows/sync-from-erp"
export const POST = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const { result } = await syncFromErpWorkflow(req.scope).run({})
return res.status(200).json(result)
}
The following tutorials are step-by-step guides that show you how to orchestrate operations across third-party services using the Medusa Framework.
<CardList items={[ { title: "Migrate Data from Magento", text: "Migrate data from Magento to your Medusa application.", href: "!resources!/integrations/guides/magento", }, { title: "Integrate Third-Party Services", text: "Integrate CMS, Fulfillment, Payment, and other third-party services.", href: "!resources!/integrations", } ]} className="mb-2" />
To learn more about the different concepts useful for integrating third-party services, check out the following chapters:
<CardList items={[ { title: "Modules", href: "/learn/fundamentals/modules", }, { title: "Workflows", href: "/learn/fundamentals/workflows", }, { title: "API Routes", href: "/learn/fundamentals/api-routes", }, { title: "Events and Subscribers", href: "/learn/fundamentals/events-and-subscribers", }, { title: "Scheduled Jobs", href: "/learn/fundamentals/scheduled-jobs", } ]} className="mb-2" />
Another essential feature for integrating third-party services is querying data across those systems efficiently.
The Framework allows you to build links not only between Medusa data models, but also virtual data models using read-only module links. You can build a module that provides the logic to query data from a third-party service, then create a read-only link between an existing data model and a virtual one from the third-party service.
<CodeTabs group="erp-query"> <CodeTab label="Read-Only Link" value="read-only-link">export const readOnlyLinkHighlights = [ ["5", "defineLink", "Define a read-only link between products and brands from a third-party service."], ]
import BrandModule from "../modules/brand"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"
export default defineLink(
{
linkable: ProductModule.linkable.product,
field: "id",
},
{
...BrandModule.linkable.brand.id,
primaryKey: "product_id",
},
{
readOnly: true,
}
)
export const brandModuleService = [ ["12", "list", "Provide a method in the service to retrieve\nthe data from the third-party service."], ]
type BrandModuleOptions = {
apiKey: string
}
export default class BrandModuleService {
private client
constructor({}, options: BrandModuleOptions) {
this.client = new Client(options)
}
async list(
filter: {
id: string | string[]
}
) {
return this.client.getBrands(filter)
/**
* Example of returned data:
*
* [
* {
* "id": "brand_123",
* "name": "Brand 123",
* "product_id": "prod_321"
* },
* {
* "id": "post_456",
* "name": "Brand 456",
* "product_id": "prod_654"
* }
* ]
*/
}
}
import { Module } from "@medusajs/framework/utils"
import BrandModuleService from "./service"
export const BRAND_MODULE = "brand"
export default Module(BRAND_MODULE, {
service: BrandModuleService,
})
Then, you can use Query to retrieve a product and its brand from the third-party service in a single query.
export const queryHighlights = [ ["1", "graph", "Query a product and its brand from the\nthird-party service in a single query."], ]
const { result } = await query.graph({
entity: "product",
fields: ["id", "brand.*"],
filters: {
id: "prod_123",
},
})
// result = [{
// id: "prod_123",
// brand: {
// id: "brand_123",
// name: "Brand 123",
// product_id: "prod_123"
// }
// ...
// }]
Query simplifies the process of retrieving data across systems, as you can retrieve data from multiple sources in a single query.
The following tutorials are step-by-step guides that show you how to query data across systems using the Medusa Framework.
<CardList items={[ { title: "Integrate Sanity CMS", text: "Query data from third-party services using read-only links.", href: "!resources!/integrations/guides/sanity", } ]} className="mb-2" />
To learn more about the different concepts useful for querying data across systems, check out the following chapters:
<CardList items={[ { title: "Modules", href: "/learn/fundamentals/modules", }, { title: "Read-Only Links", href: "/learn/fundamentals/module-links/read-only", }, { title: "Query", href: "/learn/fundamentals/module-links/query", } ]} className="mb-2" />
The Medusa Framework provides the tools to automate tasks in your Medusa application. Automation is useful when you want to perform a task periodically, such as syncing data, or when an event occurs, such as sending a confirmation email when an order is placed.
To build the task to be automated, you first create a workflow that contains the task's logic, such as syncing data or sending an email.
<CodeTabs group="automate-tasks"> <CodeTab label="Step" value="step">import { Modules } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { CreateNotificationDTO } from "@medusajs/framework/types"
export const sendNotificationStep = createStep(
"send-notification",
async (data: CreateNotificationDTO[], { container }) => {
const notificationModuleService = container.resolve(
Modules.NOTIFICATION
)
const notification = await notificationModuleService.createNotifications(
data
)
return new StepResponse(notification)
}
)
import {
createWorkflow,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { sendNotificationStep } from "./steps/send-notification"
type WorkflowInput = {
id: string
}
export const sendOrderConfirmationWorkflow = createWorkflow(
"send-order-confirmation",
({ id }: WorkflowInput) => {
const { data: orders } = useQueryGraphStep({
entity: "order",
fields: [
"id",
"email",
"currency_code",
"total",
"items.*",
],
filters: {
id,
},
})
const notification = sendNotificationStep([{
to: orders[0].email,
channel: "email",
template: "order-placed",
data: {
order: orders[0],
},
}])
return new WorkflowResponse(notification)
}
)
Then, you can execute this workflow when an event occurs using a subscriber, or at a specific interval using a scheduled job.
<CodeTabs group="automate-tasks-examples"> <CodeTab label="Event Subscriber" value="event-subscriber">export const orderPlacedHandlerHighlights = [ ["7", "orderPlacedHandler", "Send an order confirmation email when an order is placed."], ]
import type {
SubscriberArgs,
SubscriberConfig,
} from "@medusajs/framework"
import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation"
export default async function orderPlacedHandler({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
await sendOrderConfirmationWorkflow(container)
.run({
input: {
id: data.id,
},
})
}
export const config: SubscriberConfig = {
event: "order.placed",
}
export const orderConfirmationJobHighlights = [ ["6", "orderConfirmationJob", "Send an order confirmation email once a day."], ]
import type {
MedusaContainer,
} from "@medusajs/framework/types"
import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation"
export default async function orderConfirmationJob(
container: MedusaContainer
) {
await sendOrderConfirmationWorkflow(container).run({
input: {
id: "order_123",
},
})
}
export const config = {
name: "order-confirmation-job",
schedule: "0 0 * * *", // Every day at midnight
}
The following guides are step-by-step guides that show you how to automate tasks using the Medusa Framework.
<CardList items={[ { title: "Restock Notifications", text: "Send restock notifications to customers when a product is back in stock.", href: "!resources!/recipes/commerce-automation/restock-notification", }, { title: "Sync Data from and to ERP", text: "Sync data between your Medusa application and an ERP system.", href: "!resources!/recipes/erp#sync-products-from-erp", } ]} />
To learn more about the different concepts useful for automating tasks, check out the following chapters:
<CardList items={[ { title: "Workflows", href: "/learn/fundamentals/workflows", }, { title: "Events and Subscribers", href: "/learn/fundamentals/events-and-subscribers", }, { title: "Scheduled Jobs", href: "/learn/fundamentals/scheduled-jobs", } ]} />
If you have custom features that you want to re-use across multiple Medusa applications, or you want to publish your customizations for the community to use, you can build a plugin.
A plugin encapsulates your customizations in a single package. The customizations include modules, workflows, API routes, and more.
You can then publish that plugin to NPM and install it in any Medusa application. This allows you to re-use your customizations efficiently across multiple projects, or share them with the community.
The following tutorials are step-by-step guides that show you how to build plugins using the Medusa Framework.
<CardList items={[ { title: "Wishlist Plugin", text: "Build a wishlist plugin for your Medusa application.", href: "!resources!/plugins/guides/wishlist", }, { title: "Migrate Data from Magento Plugin", text: "Build a plugin that migrates data from Magento to your Medusa application.", href: "!resources!/integrations/guides/magento", } ]} />
To learn more about the different concepts useful for building plugins, check out the following chapters:
<CardList items={[ { title: "Plugins", href: "/learn/fundamentals/plugins", }, ]} itemsPerRow={1} />