Back to Medusa

{metadata.title}

www/apps/book/app/learn/fundamentals/framework/page.mdx

2.14.229.1 KB
Original Source

import { CardList, SplitSections, SplitSection, CodeTabs, CodeTab, SplitList } from "docs-ui"

export const metadata = { title: ${pageNumber} Framework Overview, }

{metadata.title}

In this chapter, you'll learn about the Medusa Framework and how it facilitates building customizations in your Medusa application.

What is the Medusa Framework?

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:

Framework Concepts and Tools

<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} />


Build Custom Features

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)."] ]

ts
import { model } from "@medusajs/framework/utils"

export const Post = model.define("post", {
  id: model.id().primaryKey(),
  title: model.text(),
})
</CodeTab> <CodeTab label="Service" value="service">

export const serviceHighlights = [ ["4", "MedusaService", "Save time by extending the MedusaService for basic CRUD operations."] ]

ts
import { MedusaService } from "@medusajs/framework/utils"
import { Post } from "./post"

export class BlogModuleService extends MedusaService({
  Post,
}){
  // CRUD methods generated by MedusaService
}
</CodeTab> <CodeTab label="Module Definition" value="module">
ts
import { Module } from "@medusajs/framework/utils"
import { BlogModuleService } from "./service"

export const BLOG_MODULE = "blog"

export default Module(BLOG_MODULE, {
  service: BlogModuleService,
})
</CodeTab> </CodeTabs>

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."] ]

ts
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)
  }
)
</CodeTab> <CodeTab label="Workflow" value="workflow">
ts
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)
  }
)
</CodeTab> </CodeTabs>

Finally, you can expose your custom feature with API routes that are built on top of your module and workflows.

ts
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)
}

Examples

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", } ]} />

Start Learning

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", } ]} />


Extend Existing Features

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."] ]

ts
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
)
</CodeTab> <CodeTab label="Data Model" value="data-model">
ts
import { model } from "@medusajs/framework/utils"

export const Brand = model.define("brand", {
  id: model.id().primaryKey(),
  name: model.text(),
})
</CodeTab> <CodeTab label="Service" value="service">
ts
import { MedusaService } from "@medusajs/framework/utils"
import { Brand } from "./models/brand"

class BrandModuleService extends MedusaService({
  Brand,
}) {

}

export default BrandModuleService
</CodeTab> <CodeTab label="Module Definition" value="module">
ts
import { Module } from "@medusajs/framework/utils"
import BrandModuleService from "./service"

export const BRAND_MODULE = "brand"

export default Module(BRAND_MODULE, {
  service: BrandModuleService,
})
</CodeTab> </CodeTabs>

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."] ]

ts
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.

Examples

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", } ]} />

Start Learning

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", } ]} />


Integrate Third-Party Services

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.

Orchestrate Operations Across Systems

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."], ]

ts
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
}
</CodeTab> <CodeTab label="Module Definition" value="module">
ts
import { Module } from "@medusajs/framework/utils"
import ErpModuleService from "./service"

export const ERP_MODULE = "erp"

export default Module(ERP_MODULE, {
  service: ErpModuleService,
})
</CodeTab> </CodeTabs>

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."], ]

ts
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,
    })
  }
)
</CodeTab> <CodeTab label="Step" value="step">
ts
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)
  }
)
</CodeTab> </CodeTabs>

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."], ]

ts
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
}
</CodeTab> <CodeTab label="Event Subscriber" value="event-subscriber">

export const productsCreatedHandlerHighlights = [ ["4", "productsCreatedHandler", "Sync products when they are created."], ]

ts
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`,
}
</CodeTab> <CodeTab label="API Route" value="api-route">

export const apiRouteHighlights = [ ["7", "POST", "Expose a REST API route to sync products."], ]

ts
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)
}
</CodeTab> </CodeTabs>

Examples

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" />

Start Learning

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" />

Query Data Across Systems

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."], ]

ts
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,
  }
)
</CodeTab> <CodeTab label="Module Service" value="module-service">

export const brandModuleService = [ ["12", "list", "Provide a method in the service to retrieve\nthe data from the third-party service."], ]

ts
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"
     *   }
     * ]
    */
  }
}
</CodeTab> <CodeTab label="Module Definition" value="module">
ts
import { Module } from "@medusajs/framework/utils"
import BrandModuleService from "./service"

export const BRAND_MODULE = "brand"

export default Module(BRAND_MODULE, {
  service: BrandModuleService,
})
</CodeTab> </CodeTabs>

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."], ]

ts
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.

Examples

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" />

Start Learning

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" />


Automate Tasks

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">
ts
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)
  }
)
</CodeTab> <CodeTab label="Workflow" value="workflow">
ts
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)
  }
)
</CodeTab> </CodeTabs>

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."], ]

ts
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",
}
</CodeTab> <CodeTab label="Scheduled Job" value="scheduled-job">

export const orderConfirmationJobHighlights = [ ["6", "orderConfirmationJob", "Send an order confirmation email once a day."], ]

ts
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
}
</CodeTab> </CodeTabs>

Examples

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", } ]} />

Start Learning

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", } ]} />


Re-Use Customizations Across Applications

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.

Examples

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", } ]} />

Start Learning

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} />