Medusa - Framework

Framework

Framework

Build any feature, integrate with any system

A built-in backend framework that lets you create customizations and integrations in hours, not weeks.

With the Medusa framework, the only limit is your imagination.

Orchestrate actions across systems with Workflows

Traditional commerce platforms force you to use separate servers, middleware, or applications for customizations. This is hard to manage, leading to long implementations and unreliable operations. In Medusa, you have a built-in foundation to build customizations natively.

Our tools enable you to avoid hacky workarounds and instead build bespoke, scalable commerce applications fast. Let Medusa provide the framework, so you can focus on building the features that differentiate your business.

Define a step

A workflow step performs a query or action in a system. Steps can receive inputs from previous steps and return data to be used in subsequent steps. You can resolve services in your steps, allowing you to build business logic involving Medusa’s core commerce modules, third-party systems, or custom systems.

Define a workflow

To connect multiple steps together you create a Workflow. Creating a Workflow is like writing any other function. Behind the scenes, Medusa generates a representation of your workflow that enables automatic retries of steps if they fail.

Rollback on permanent failures

If step retries don't succeed, Medusa will rollback previous steps. A rollback will call a step's compensation method, ensuring your data is kept consistent across systems.

Run your workflow

You can trigger your workflows anywhere in Medusa. Use your workflows in Subscribers to start them in response to an event. Start them in response to requests with API Routes. Or have them run on a defined schedule with Scheduled Jobs.

src > workflows > steps > create-order-in-erp.ts
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

export const createOrderInERP = createStep(
 "create-order-in-erp"
 async (order: StepInput, { container }) => {
  const erpService = container.resolve("erp")

const createdOrder = await erpService.createOrder(order)

return new StepResponse(createdOrder)
 }
)

Define a step

src > workflows > steps > create-order-in-erp.ts
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

export const createOrderInERP = createStep(
 "create-order-in-erp"
 async (order: StepInput, { container }) => {
  const erpService = container.resolve("erp")

const createdOrder = await erpService.createOrder(order)

return new StepResponse(createdOrder)
 }
)

Define a workflow

src > workflows > sync-order-to-erp.ts
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createOrderInERP, transformOrder } from "./steps"

export const syncOrderToERP = createWorkflow(
 "sync-order-to-erp",
  (input: WorkflowInput, { container }) => {

const medusaOrder = retrieveOrder(input.order_id)

const createdOrder = when({ medusaOrder }, ({ medusaOrder }) => {
    return medusaOrder.status === "completed"
  }).then(() => {
    const preparedOrder = transformOrder(medusaOrder)

return createOrderInERP(preparedOrder)
  })

return new WorkflowResponse(createdOrder)
 }
)

Rollback on permanent failures

src > workflows > steps > create-order-in-erp.ts
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

export const createOrderInERP = createStep(
 "create-order-in-erp"
 async (order: StepInput, { container }) => {
  const erpService = container.resolve("erp")

const createdOrder = await erpService.createOrder(order)

return new StepResponse(createdOrder, createdOrder)
 },
 // In the face of errors, we roll back and delete the order
 async (createdOrder, { container }) => {
   const erpService = container.resolve("erp")

await erpService.deleteOrder(createdOrder)
 }
)

Run your workflow

src > subscribers > sync-order-to-erp.ts
import {
  type SubscriberConfig,
} from "@medusajs/framework"
import syncOrderToErp from "../workflows/sync-order-to-erp"

export default async function handleOrderPlaced({
  event: { data },
  container,
}) {

const orderService = container.resolve("order")

const order = await orderService.retrieve(id)

await syncOrderToErp(container).run({
    input: {
      order
    },
  })
}

export const config: SubscriberConfig = {
  event: "order.placed",
}

Modules

Build custom modules to support any commerce logic

Unlike other platforms that require separate applications for customization, Medusa enables you to write custom modules that integrate seamlessly into your application.

You can define your own data models, endpoints, and logic for modules that work alongside Medusa's pre-built commerce modules.

Data Model

A data model represents a table in the database. You create data models using Medusa's Data Model API. It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

src > modules > company > models > company.ts
import { model } from "@medusajs/framework/utils"

export const Company = model.define("company", {
  id: model.id({ prefix: "comp" }).primaryKey(),
  name: model.text(),
  logo_url: model.text().nullable(),
  domain: model.text().nullable(),
  employees: model.hasMany(() => Employee),
  spending_limit_reset_frequency: model
    .enum(["never", "daily", "weekly", "monthly", "yearly"])
    .default("monthly"),
})

export const Employee = model.define("employee", {
  id: model.id({ prefix: "emp" }).primaryKey(),
  email: model.text(),
  spending_limit: model.bigNumber().default(0),
  company: model.belongsTo(() => Company, {
    mappedBy: "employees",
  }),
});

Service

You perform database operations on your data models in a service. Services provide methods, such as create, update, retrieve, list, and more, to manage your data models out of the box.

Additionally, services can hold custom methods for your business logic and/or integrate with third-party providers.

src > modules > company > service.ts
import { MedusaService } from "@medusajs/framework/utils"
import { Company, Employee } from "./models"

class CompanyModuleService extends MedusaService({
  Company,
  Employee
}) {

async validateEmployeeEmail(employee, company) {
    if (!employee.email.includes(company.domain)) {
      throw new Error("Employee email not associated with company")
    }
  }
}

export default CompanyModuleService

Container

Once services are defined, they can be resolved from Medusa's dependency container in API Routes, Subscribers, Scheduled Jobs, Workflows, and more.

src > workflows > steps > companies > create-company.ts
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

export const createCompanyStep = createStep(
  "create-company-step",
  async (input: CreateCompanyStepInput, { container }) => {
    const companyService = container.resolve("company")

const company = await companyService.createCompanies(input)

return new StepResponse(company)
  }
)

Query

With multiple modules defined and linked to each other, you can use Query to retrieve data across all of them, including third-party systems.

Similar to services, Query can be resolved from Medusa's dependency container across all the different application entry points.

src > api > admin > companies > route.ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve("query")

const { data } = await query({
    entity: "company",
    fields: ["id", "name", "employees"],
    filters: {
      take: 20,
      skip: 0
    }
  })

res.json({ companies: data })
}

Query data across multiple systems.

Query gives you a single entry point to data across all systems in your stack. Create data relationships between modules with the new Link Modules and write queries that fetch data from multiple sources, including 3rd-party systems.

Admin extensions

Powerful system for Admin extensions.

Extend existing Admin pages by adding widgets, which let you insert custom React components into predefined injection zones. Or go all the way and create new bespoke Admin pages using UI routes.

UI Widgets

Widgets allow you to add custom React components in injection zones spread across existing pages in the Admin Dashboard. For example, you can add a button on the order details page that links to the order in an integrated ERP system.

Widgets are easy to build, and Medusa automatically registers them when exporting a React component and its configuration from files placed in your Medusa project.

src > admin > widgets > erp-button.tsx
import type { WidgetConfig } from "@medusajs/admin"
import { Container } from "@medusajs/ui"

const OrderWidget = ({ order }) => {
  const link = `https://myerp.com/orders/${order.id}`

return (
    <Container className="divide-y p-0">
      <a href={link}>
        View in MyERP
      </a>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "order.details.before",
})

export default OrderWidget

UI Routes

UI Routes allow you to add new routes to your admin dashboard. Similar to Widgets, Admin UI Routes are simply exported React components created in files in your Medusa project.

src > admin > routes > erp-orders > page.tsx
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const ERPOrdersPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Nested Orders Page</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "ERP Orders",
  nested: "/orders",
})

export default ERPOrdersPage

Built with React

Ready to build your custom commerce setup?

Start now