Marketplace Recipe: Vendors Example

In this guide, you'll learn how to build a marketplace with Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with support for customizations. While Medusa doesn't provide marketeplace functionalities natively, it provides features that you can extend and a framework to support all your customization needs to build a marketplace.

In this guide, you'll customize Medusa to build a marketplace with the following features:

  1. Manage multiple vendors, each having vendor admins.
  2. Allow vendor admins to manage the vendor’s products and orders.
  3. Split orders placed by customers into multiple orders for each vendor.
NoteThis guide provides an example of an approach to implement digital products. You're free to choose a different approach using Medusa's framework.
Marketplace Example Repository
Find the full code for this recipe example in this repository.
OpenApi Specs for Postman
Imported this OpenApi Specs file into tools like Postman.

Step 1: Install a Medusa Application#

Start by installing the Medusa application on your machine with the following command:

Terminal
npx create-medusa-app@latest

You'll first be asked for the project's name. You can also optionally choose to install the Next.js starter storefront.

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the {project-name}-storefront name.

Why is the storefront installed separately?The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called API routes. Learn more about Medusa's architecture in this documentation.

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form. Afterwards, you can login with the new user and explore the dashboard.

Ran to Errors?Check out the troubleshooting guides for help.

Step 2: Create Marketplace Module#

Medusa creates commerce features in modules. For example, product features and data models are created in the Product Module.

You also create custom commerce data models and features in custom modules. They're integrated into the Medusa application similar to Medusa's modules without side effects.

So, you'll create a marketplace module that holds the data models for a vendor and an admin and allows you to manage them.

Create the directory src/modules/marketplace.

Create Data Models#

Create the file src/modules/marketplace/models/vendor.ts with the following content:

src/modules/marketplace/models/vendor.ts
1import { model } from "@medusajs/framework/utils"2import VendorAdmin from "./vendor-admin"3
4const Vendor = model.define("vendor", {5  id: model.id().primaryKey(),6  handle: model.text(),7  name: model.text(),8  logo: model.text().nullable(),9  admins: model.hasMany(() => VendorAdmin),10})11
12export default Vendor

This creates a Vendor data model, which represents a business that sells its products in the marketplace.

Notice that the Vendor has many admins whose data model you’ll create next.

Create the file src/modules/marketplace/models/vendor-admin.ts with the following content:

src/modules/marketplace/models/vendor-admin.ts
1import { model } from "@medusajs/framework/utils"2import Vendor from "./vendor"3
4const VendorAdmin = model.define("vendor_admin", {5  id: model.id().primaryKey(),6  first_name: model.text().nullable(),7  last_name: model.text().nullable(),8  email: model.text().unique(),9  vendor: model.belongsTo(() => Vendor, {10    mappedBy: "admins",11  }),12})13
14export default VendorAdmin

This creates a VendorAdmin data model, which represents an admin of a vendor.

Create Main Module Service#

Next, create the main service of the module at src/modules/marketplace/service.ts with the following content:

src/modules/marketplace/service.ts
1import { MedusaService } from "@medusajs/framework/utils"2import Vendor from "./models/vendor"3import VendorAdmin from "./models/vendor-admin"4
5class MarketplaceModuleService extends MedusaService({6  Vendor,7  VendorAdmin,8}) {9}10
11export default MarketplaceModuleService

The service extends the service factory, which provides basic data-management features.

Create Module Definition#

After that, create the module definition at src/modules/marketplace/index.ts with the following content:

src/modules/marketplace/index.ts
1import { Module } from "@medusajs/framework/utils"2import MarketplaceModuleService from "./service"3
4export const MARKETPLACE_MODULE = "marketplaceModuleService"5
6export default Module(MARKETPLACE_MODULE, {7  service: MarketplaceModuleService,8})

Add Module to Medusa Configuration#

Finally, add the module to the list of modules in medusa-config.ts:

medusa-config.ts
1module.exports = defineConfig({2  // ...3  modules: [4    {5      resolve: "./src/modules/marketplace",6    },7  ],8})

Further Reads#


Modules are isolated in Medusa, making them reusable, replaceable, and integrable in your application without side effects.

So, you can't have relations between data models in modules. Instead, you define a link between them.

Links are relations between data models of different modules that maintain the isolation between the modules.

Each vendor has products and orders. So, in this step, you’ll define links between the Vendor data model and the Product and Order data models from the Product and Order modules, respectively.

TipIf your use case requires linking the vendor to other data models, such as SalesChannel, define those links in a similar manner.

Create the file src/links/vendor-product.ts with the following content:

src/links/vendor-product.ts
1import { defineLink } from "@medusajs/framework/utils"2import MarketplaceModule from "../modules/marketplace"3import ProductModule from "@medusajs/medusa/product"4
5export default defineLink(6  MarketplaceModule.linkable.vendor,7  {8    linkable: ProductModule.linkable.product,9    isList: true,10  }11)

This adds a list link between the Vendor and Product data models, indicating that a vendor record can be linked to many product records.

Then, create the file src/links/vendor-order.ts with the following content:

src/links/vendor-order.ts
1import { defineLink } from "@medusajs/framework/utils"2import MarketplaceModule from "../modules/marketplace"3import OrderModule from "@medusajs/medusa/order"4
5export default defineLink(6  MarketplaceModule.linkable.vendor,7  {8    linkable: OrderModule.linkable.order,9    isList: true,10  }11)

This adds a list link between the Vendor and Order data models, indicating that a vendor record can be linked to many order records.

Further Read#


To create tables for the marketplace data models in the database, start by generating the migrations for the Marketplace Module with the following command:

Terminal
npx medusa db:generate marketplaceModuleService

This generates a migration in the src/modules/marketeplace/migrations directory.

Then, to reflect the migration and links in the database, run the following command:

Terminal
npx medusa db:migrate

Step 5: Create Vendor Admin Workflow#

To implement and expose a feature that manipulates data, you create a workflow that uses services to implement the functionality, then create an API route that executes that workflow.

In this step, you’ll create the workflow used to create a vendor admin. You'll use it in the next step in an API route.

The workflow’s steps are:

  1. Create the vendor admin using the Marketplace Module’s main service.
  2. Create a vendor actor type to authenticate the vendor admin using the Auth Module. Medusa provides a step to perform this.

First, create the file src/workflows/marketplace/create-vendor-admin/steps/create-vendor-admin.ts with the following content:

src/workflows/marketplace/create-vendor-admin/steps/create-vendor-admin.ts
7import { MARKETPLACE_MODULE } from "../../../../modules/marketplace"8
9const createVendorAdminStep = createStep(10  "create-vendor-admin-step",11  async ({ 12    admin: adminData,13  }: Pick<CreateVendorAdminWorkflowInput, "admin">, 14  { container }) => {15    const marketplaceModuleService: MarketplaceModuleService = 16      container.resolve(MARKETPLACE_MODULE)17
18    const vendorAdmin = await marketplaceModuleService.createVendorAdmins(19      adminData20    )21
22    return new StepResponse(23      vendorAdmin,24      vendorAdmin25    )26  },27  async (vendorAdmin, { container }) => {28    const marketplaceModuleService: MarketplaceModuleService = 29      container.resolve(MARKETPLACE_MODULE)30
31    marketplaceModuleService.deleteVendorAdmins(vendorAdmin.id)32  }33)34
35export default createVendorAdminStep

This is the first step that creates the vendor admin and returns it.

In the compensation function, which runs if an error occurs in the workflow, it removes the admin.

Then, create the workflow at src/workflows/marketplace/create-vendor-admin/index.ts with the following content:

src/workflows/marketplace/create-vendor-admin/index.ts
1import { 2  createWorkflow,3  WorkflowResponse,4} from "@medusajs/framework/workflows-sdk"5import { 6  setAuthAppMetadataStep,7} from "@medusajs/medusa/core-flows"8import createVendorAdminStep from "./steps/create-vendor-admin"9
10export type CreateVendorAdminWorkflowInput = {11  admin: {12    email: string13    first_name?: string14    last_name?: string15    vendor_id: string16  }17  authIdentityId: string18}19
20const createVendorAdminWorkflow = createWorkflow(21  "create-vendor-admin",22  function (input: CreateVendorAdminWorkflowInput) {23    const vendorAdmin = createVendorAdminStep({24      admin: input.admin,25    })26
27    setAuthAppMetadataStep({28      authIdentityId: input.authIdentityId,29      actorType: "vendor",30      value: vendorAdmin.id,31    })32
33    return new WorkflowResponse(vendorAdmin)34  }35)36
37export default createVendorAdminWorkflow

In this workflow, you run the following steps:

  1. createVendorAdminStep to create the vendor admin.
  2. setAuthAppMetadataStep to create the vendor actor type. This step is provided by Medusa in the @medusajs/medusa/core-flows package.

You return the created vendor admin.

Further Read#


Step 6: Create Vendor API Route#

To expose custom commerce features to frontend applications, such as the Medusa Admin dashboard or a storefront, you expose an endpoint by creating an API route.

In this step, you’ll create the API route that runs the workflow from the previous step.

Start by creating the file src/api/vendors/route.ts with the following content:

src/api/vendors/route.ts
1import { 2  AuthenticatedMedusaRequest, 3  MedusaResponse,4} from "@medusajs/framework/http"5import { MedusaError } from "@medusajs/framework/utils"6import { z } from "zod"7import MarketplaceModuleService from "../../modules/marketplace/service"8import createVendorAdminWorkflow from "../../workflows/marketplace/create-vendor-admin"9
10const schema = z.object({11  name: z.string(),12  handle: z.string().optional(),13  logo: z.string().optional(),14  admin: z.object({15    email: z.string(),16    first_name: z.string().optional(),17    last_name: z.string().optional(),18  }).strict(),19}).strict()20
21type RequestBody = {22  name: string,23  handle?: string,24  logo?: string,25  admin: {26    email: string,27    first_name?: string,28    last_name?: string29  }30}

This defines the schema to be accepted in the request body.

Then, add the route handler to the same file:

src/api/vendors/route.ts
1export const POST = async (2  req: AuthenticatedMedusaRequest<RequestBody>,3  res: MedusaResponse4) => {5  // If `actor_id` is present, the request carries 6  // authentication for an existing vendor admin7  if (req.auth_context?.actor_id) {8    throw new MedusaError(9      MedusaError.Types.INVALID_DATA,10      "Request already authenticated as a vendor."11    )12  }13
14  const { admin, ...vendorData } = schema.parse(req.body) as RequestBody15
16  const marketplaceModuleService: MarketplaceModuleService = req.scope17    .resolve("marketplaceModuleService")18
19  // create vendor20  let vendor = await marketplaceModuleService.createVendors([vendorData])21
22  // create vendor admin23  await createVendorAdminWorkflow(req.scope)24    .run({25      input: {26        admin: {27          ...admin,28          vendor_id: vendor[0].id,29        },30        authIdentityId: req.auth_context.auth_identity_id,31      },32    })33
34  // retrieve vendor again with admins35  vendor = await marketplaceModuleService.retrieveVendor(vendor[0].id, {36    relations: ["admins"],37  })38
39  res.json({40    vendor,41  })42}

This API route expects the request header to contain a new vendor admin’s authentication JWT token.

The route handler creates a vendor using the Marketplace Module’s main service and then uses the createVendorAdminWorkflow to create an admin for the vendor.

Next, create the file src/api/middlewares.ts with the following content:

src/api/middlewares.ts
1import { 2  defineMiddlewares,3  authenticate,4} from "@medusajs/framework/http"5
6export default defineMiddlewares({7  routes: [8    {9      matcher: "/vendors",10      method: "POST",11      middlewares: [12        authenticate("vendor", ["session", "bearer"], {13          allowUnregistered: true,14        }),15      ],16    },17    {18      matcher: "/vendors/*",19      middlewares: [20        authenticate("vendor", ["session", "bearer"]),21      ],22    },23  ],24})

This applies two middlewares:

  1. On the /vendors POST API route; it requires authentication but allows unregistered users.
  2. On the /vendors/* API routes, which you’ll implement in upcoming sections; it requires an authenticated vendor admin.

Test it Out#

To test out the above API route:

  1. Start the Medusa application:
  1. Retrieve a JWT token from the /auth/vendor/emailpass/register API route:
Code
1curl -X POST 'http://localhost:9000/auth/vendor/emailpass/register' \2-H 'Content-Type: application/json' \3--data-raw '{4    "email": "admin@medusa-test.com",5    "password": "supersecret"6}'
TipThis route is available because you created the vendor actor type previously.
  1. Send a request to the /vendors API route, passing the token retrieved from the previous response in the request header:
NoteDon't include a trailing slash at the end of the URL. Learn more here.
Code
1curl -X POST 'http://localhost:9000/vendors' \2-H 'Content-Type: application/json' \3-H 'Authorization: Bearer {token}' \4--data-raw '{5    "name": "Acme",6    "handle": "acme",7    "admin": {8        "email": "admin@medusa-test.com",9        "first_name": "Admin",10        "last_name": "Acme"11    }12}'

This returns the created vendor and admin.

  1. Retrieve an authenticated token of the vendor admin by sending another request to the /auth/vendor/emailpass API route:
Code
1curl -X POST 'http://localhost:9000/auth/vendor/emailpass' \2-H 'Content-Type: application/json' \3--data-raw '{4    "email": "admin@medusa-test.com",5    "password": "supersecret"6}'

Use this token in the header of later requests that require authentication.

Further Reads#


Step 7: Add Product API Routes#

In this section, you’ll add two API routes: one to retrieve the vendor’s products and one to create a product.

To create the API route that retrieves the vendor’s products, create the file src/api/vendors/products/route.ts with the following content:

src/api/vendors/products/route.ts
1import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"2import { 3  ContainerRegistrationKeys,4} from "@medusajs/framework/utils"5import MarketplaceModuleService from "../../../modules/marketplace/service"6import { MARKETPLACE_MODULE } from "../../../modules/marketplace"7
8export const GET = async (9  req: AuthenticatedMedusaRequest,10  res: MedusaResponse11) => {12  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)13  const marketplaceModuleService: MarketplaceModuleService = 14    req.scope.resolve(MARKETPLACE_MODULE)15
16  const vendorAdmin = await marketplaceModuleService.retrieveVendorAdmin(17    req.auth_context.actor_id,18    {19      relations: ["vendor"],20    }21  )22
23  const { data: [vendor] } = await query.graph({24    entity: "vendor",25    fields: ["products.*"],26    filters: {27      id: [vendorAdmin.vendor.id],28    },29  })30
31  res.json({32    products: vendor.products,33  })34}

This adds a GET API route at /vendors/products that, using Query, retrieves the list of products of the vendor and returns them in the response.

To add the create product API route, add to the same file the following:

src/api/vendors/products/route.ts
1// other imports...2import { createProductsWorkflow } from "@medusajs/medusa/core-flows"3import { 4  CreateProductWorkflowInputDTO,5  IProductModuleService,6  ISalesChannelModuleService,7} from "@medusajs/framework/types"8import { 9  Modules, 10  Modules,11} from "@medusajs/framework/utils"12
13// GET method...14
15type RequestType = CreateProductWorkflowInputDTO16
17export const POST = async (18  req: AuthenticatedMedusaRequest<RequestType>,19  res: MedusaResponse20) => {21  const remoteLink = req.scope.resolve("remoteLink")22  const marketplaceModuleService: MarketplaceModuleService = 23    req.scope.resolve(MARKETPLACE_MODULE)24  const productModuleService: IProductModuleService = req.scope25    .resolve(Modules.PRODUCT)26  const salesChannelModuleService: ISalesChannelModuleService = req.scope27    .resolve(Modules.SALES_CHANNEL)28  // Retrieve default sales channel to make the product available in.29  // Alternatively, you can link sales channels to vendors and allow vendors30  // to manage sales channels31  const salesChannels = await salesChannelModuleService.listSalesChannels()32  const vendorAdmin = await marketplaceModuleService.retrieveVendorAdmin(33    req.auth_context.actor_id,34    {35      relations: ["vendor"],36    }37  )38  39  // TODO create and link product40}

This adds a POST API route at /vendors/products. It resolves the necessary modules' main services, and retrieves the sales channels and vendor admin.

In the place of the TODO, add the following:

src/api/vendors/products/route.ts
1const { result } = await createProductsWorkflow(req.scope)2  .run({3    input: {4      products: [{5        ...req.body,6        sales_channels: salesChannels,7      }],8    },9  })10
11// link product to vendor12await remoteLink.create({13  [MARKETPLACE_MODULE]: {14    vendor_id: vendorAdmin.vendor.id,15  },16  [Modules.PRODUCT]: {17    product_id: result[0].id,18  },19})20
21// retrieve product again22const product = await productModuleService.retrieveProduct(23  result[0].id24)25
26res.json({27  product,28})

This creates a product, links it to the vendor, and returns the product in the response.

NoteIn the route handler, you add the product to the default sales channel. You can, instead, link sales channels with vendors similar to the steps explained in step 2.

Finally, in src/api/middlewares.ts, apply a middleware on the create products route to validate the request body before executing the route handler:

src/api/middlewares.ts
1import { 2  defineMiddlewares,3  authenticate,4  validateAndTransformBody,5} from "@medusajs/framework/http"6import { 7  AdminCreateProduct,8} from "@medusajs/medusa/api/admin/products/validators"9
10export default defineMiddlewares({11  routes: [12    // ...13    {14      matcher: "/vendors/products",15      method: "POST",16      middlewares: [17        authenticate("vendor", ["session", "bearer"]),18        validateAndTransformBody(AdminCreateProduct),19      ],20    },21  ],22})

Test it Out#

To test out the new API routes:

  1. Send a POST request to /vendors/products to create a product:
Code
1curl -X POST 'http://localhost:9000/vendors/products' \2-H 'Content-Type: application/json' \3-H 'Authorization: Bearer {token}' \4--data '{5    "title": "T-Shirt",6    "status": "published",7    "options": [8        {9            "title": "Color",10            "values": ["Blue"]11        }12    ],13    "variants": [14        {15            "title": "T-Shirt",16            "prices": [17                {18                    "currency_code": "eur",19                    "amount": 1020                }21            ],22            "manage_inventory": false,23            "options": {24                "Color": "Blue"25            }26        }27    ]28}'
  1. Send a GET request to /vendors/products to retrieve the vendor’s products:
Code
1curl 'http://localhost:9000/vendors/products' \2-H 'Authorization: Bearer {token}'

Further Reads#


Step 8: Create Vendor Order Workflow#

In this step, you’ll create a workflow that’s executed when the customer places an order. It has the following steps:

  1. Retrieve the cart using its ID. Medusa provides a useQueryGraphStep in the @medusajs/medusa/core-flows package that you can use.
  2. Create a parent order for the cart and its items. Medusa also has a completeCartWorkflow in the @medusajs/medusa/core-flows package that you can use as a step.
  3. Group the cart items by their product’s associated vendor.
  4. Retrieve the order's details using Medusa's getOrderDetailWorkflow exported by the @medusajs/medusa/core-flows package.
  5. For each vendor, create a child order with the cart items of their products, and return the orders with the links to be created.
  6. Create the links created by the previous step. Medusa provides a createRemoteLinkStep in the @medusajs/medusa/core-flows package that you can use.

You'll implement the third and fourth steps.

groupVendorItemsStep#

Create the third step in the file src/workflows/marketplace/create-vendor-orders/steps/group-vendor-items.ts:

src/workflows/marketplace/create-vendor-orders/steps/group-vendor-items.ts
1import { 2  createStep,3  StepResponse,4} from "@medusajs/framework/workflows-sdk"5import { CartDTO, CartLineItemDTO } from "@medusajs/framework/types"6import { ContainerRegistrationKeys } from "@medusajs/framework/utils"7
8type StepInput = {9  cart: CartDTO10}11
12const groupVendorItemsStep = createStep(13  "group-vendor-items",14  async ({ cart }: StepInput, { container }) => {15    const query = container.resolve(ContainerRegistrationKeys.QUERY)16
17    const vendorsItems: Record<string, CartLineItemDTO[]> = {}18
19    await Promise.all(cart.items?.map(async (item) => {20      const { data: [product] } = await query.graph({21        entity: "product",22        fields: ["vendor.*"],23        filters: {24          id: [item.product_id],25        },26      })27
28      const vendorId = product.vendor?.id29
30      if (!vendorId) {31        return32      }33      vendorsItems[vendorId] = [34        ...(vendorsItems[vendorId] || []),35        item,36      ]37    }))38
39    return new StepResponse({40      vendorsItems,41    })42  }43)44
45export default groupVendorItemsStep

This step groups the items by the vendor associated with the product into an object and returns the object.

createVendorOrdersStep#

Next, create the fourth step in the file src/workflows/marketplace/create-vendor-orders/steps/create-vendor-orders.ts:

src/workflows/marketplace/create-vendor-orders/steps/create-vendor-orders.ts
18import Vendor from "../../../../modules/marketplace/models/vendor"19
20export type VendorOrder = (OrderDTO & {21  vendor: InferTypeOf<typeof Vendor>22})23
24type StepInput = {25  parentOrder: OrderDTO26  vendorsItems: Record<string, CartLineItemDTO[]>27}28
29function prepareOrderData(30  items: CartLineItemDTO[], 31  parentOrder: OrderDTO32) {33  // TODO format order data34}35
36const createVendorOrdersStep = createStep(37  "create-vendor-orders",38  async (39    { vendorsItems, parentOrder }: StepInput, 40    { container, context }41  ) => {42    const linkDefs: LinkDefinition[] = []43    const createdOrders: VendorOrder[] = []44    const vendorIds = Object.keys(vendorsItems)45    46    const marketplaceModuleService =47      container.resolve<MarketplaceModuleService>(MARKETPLACE_MODULE)48
49    const vendors = await marketplaceModuleService.listVendors({50      id: vendorIds,51    })52
53    // TODO create child orders54
55    return new StepResponse({ 56      orders: createdOrders, 57      linkDefs,58    }, {59      created_orders: createdOrders,60    })61  },62  async ({ created_orders }, { container, context }) => {  63    // TODO add compensation function64  }65)66
67export default createVendorOrdersStep

This creates a step that receives the grouped vendor items and the parent order. For now, it initializes variables and retrieves vendors by their IDs.

The step returns the created orders and the links to be created. It also passes the created orders to the compensation function

Replace the TODO in the step with the following:

src/workflows/marketplace/create-vendor-orders/steps/create-vendor-orders.ts
1if (vendorIds.length === 1) {2  linkDefs.push({3    [MARKETPLACE_MODULE]: {4      vendor_id: vendors[0].id,5    },6    [Modules.ORDER]: {7      order_id: parentOrder.id,8    },9  })10
11  createdOrders.push({12    ...parentOrder,13    vendor: vendors[0],14  })15  16  return new StepResponse({17    orders:  createdOrders,18    linkDefs,19  }, {20    created_orders: [],21  })22}23
24// TODO create multiple child orders

In the above snippet, if there's only one vendor in the group, the parent order is added to the linkDefs array and it's returned in the response.

TipSince the parent order isn't a child order, it's not passed to the compensation function as it should only handle child orders.

Next, replace the new TODO with the following snippet:

src/workflows/marketplace/create-vendor-orders/steps/create-vendor-orders.ts
1try {2  await promiseAll(3    vendorIds.map(async (vendorId) => {4      const items = vendorsItems[vendorId]5      const vendor = vendors.find((v) => v.id === vendorId)!6
7      const { result: childOrder } = await createOrderWorkflow(8        container9      )10      .run({11        input: prepareOrderData(items, parentOrder),12        context,13      }) as unknown as { result: VendorOrder }14
15      childOrder.vendor = vendor16      createdOrders.push(childOrder)17      18      linkDefs.push({19        [MARKETPLACE_MODULE]: {20          vendor_id: vendor.id,21        },22        [Modules.ORDER]: {23          order_id: childOrder.id,24        },25      })26    })27  )28} catch (e) {29  return StepResponse.permanentFailure(30    `An error occured while creating vendor orders: ${e}`,31    {32      created_orders: createdOrders,33    }34  )35}

In this snippet, you create multiple child orders for each vendor and link the orders to the vendors.

You use promiseAll from the Workflows SDK that loops over an array of promises and ensures that all transactions within these promises are rolled back in case an error occurs. You also wrap promiseAll in a try-catch block, and in the catch block you invoke and return StepResponse.permanentFailure which indicates that the step has failed but still invokes the compensation function that you'll implement in a bit. The first parameter of permanentFailure is the error message, and the second is the data to pass to the compensation function.

If an error occurs, the created orders in the createdOrders array are canceled using Medusa's cancelOrderWorkflow from the @medusajs/medusa/core-flows package.

The order's data is formatted using the prepareOrderData function. Replace its definition with the following:

src/workflows/marketplace/create-vendor-orders/steps/create-vendor-orders.ts
1function prepareOrderData(2  items: CartLineItemDTO[], 3  parentOrder: OrderDTO4) {5  return  {6    items,7    metadata: {8      parent_order_id: parentOrder.id,9    },10    // use info from parent11    region_id: parentOrder.region_id,12    customer_id: parentOrder.customer_id,13    sales_channel_id: parentOrder.sales_channel_id,14    email: parentOrder.email,15    currency_code: parentOrder.currency_code,16    shipping_address_id: parentOrder.shipping_address?.id,17    billing_address_id: parentOrder.billing_address?.id,18    // A better solution would be to have shipping methods for each19    // item/vendor. This requires changes in the storefront to commodate that20    // and passing the item/vendor ID in the `data` property, for example.21    // For simplicity here we just use the same shipping method.22    shipping_methods: parentOrder.shipping_methods.map((shippingMethod) => ({23      name: shippingMethod.name,24      amount: shippingMethod.amount,25      shipping_option_id: shippingMethod.shipping_option_id,26      data: shippingMethod.data,27      tax_lines: shippingMethod.tax_lines.map((taxLine) => ({28        code: taxLine.code,29        rate: taxLine.rate,30        provider_id: taxLine.provider_id,31        tax_rate_id: taxLine.tax_rate_id,32        description: taxLine.description,33      })),34      adjustments: shippingMethod.adjustments.map((adjustment) => ({35        code: adjustment.code,36        amount: adjustment.amount,37        description: adjustment.description,38        promotion_id: adjustment.promotion_id,39        provider_id: adjustment.provider_id,40      })),41    })),42  }43}

This formats the order's data using the items and parent order's details.

NoteWhen creating the child orders, the shipping method of the parent is used as-is for simplicity. A better practice would be to allow the customer to choose different shipping methods for each vendor’s items and then store those details in the data property of the shipping method.

Finally, replace the TODO in the compensation function with the following:

src/workflows/marketplace/create-vendor-orders/steps/create-vendor-orders.ts
1await Promise.all(created_orders.map((createdOrder) => {2  return cancelOrderWorkflow(container).run({3    input: {4      order_id: createdOrder.id,5    },6    context,7    container,8  })9}))

The compensation function cancels all child orders received from the step. It uses the cancelOrderWorkflow that Medusa provides in the @medusajs/medusa/core-flows package.

Create Workflow#

Finally, create the workflow at the file src/workflows/marketplace/create-vendor-orders/index.ts:

src/workflows/marketplace/create-vendor-orders/index.ts
12import createVendorOrdersStep from "./steps/create-vendor-orders"13
14type WorkflowInput = {15  cart_id: string16}17
18const createVendorOrdersWorkflow = createWorkflow(19  "create-vendor-order",20  (input: WorkflowInput) => {21    const { data: carts } = useQueryGraphStep({22      entity: "cart",23      fields: ["id", "items.*"],24      filters: { id: input.cart_id },25      options: {26        throwIfKeyNotFound: true,27      },28    })29
30    const { id: orderId } = completeCartWorkflow.runAsStep({31      input: {32        id: carts[0].id,33      },34    })35
36    const { vendorsItems } = groupVendorItemsStep({37      cart: carts[0],38    })39    40    const order = getOrderDetailWorkflow.runAsStep({41      input: {42        order_id: orderId,43        fields: [44          "region_id",45          "customer_id",46          "sales_channel_id",47          "email",48          "currency_code",49          "shipping_address.*",50          "billing_address.*",51          "shipping_methods.*",52        ],53      },54    })55
56    const { 57      orders: vendorOrders, 58      linkDefs,59    } = createVendorOrdersStep({60      parentOrder: order,61      vendorsItems,62    })63
64    createRemoteLinkStep(linkDefs)65
66    return new WorkflowResponse({67      parent_order: order,68      vendor_orders: vendorOrders,69    })70  }71)72
73export default createVendorOrdersWorkflow

In the workflow, you run the following steps:

  1. useQueryGraphStep to retrieve the cart's details.
  2. completeCartWorkflow to complete the cart and create a parent order.
  3. groupVendorItemsStep to group the order's items by their vendor.
  4. getOrderDetailWorkflow to retrieve an order's details.
  5. createVendorOrdersStep to create child orders for each vendor's items.
  6. createRemoteLinkStep to create the links returned by the previous step.

You return the parent and vendor orders.

Create API Route Executing the Workflow#

You’ll now create the API route that executes the workflow.

Create the file src/api/store/carts/[id]/complete/route.ts with the following content:

src/api/store/carts/[id]/complete/route.ts
1import { 2  AuthenticatedMedusaRequest, 3  MedusaResponse,4} from "@medusajs/framework/http"5import createVendorOrdersWorkflow from "../../../../../workflows/marketplace/create-vendor-orders"6
7export const POST = async (8  req: AuthenticatedMedusaRequest,9  res: MedusaResponse10) => {11  const cartId = req.params.id12
13  const { result } = await createVendorOrdersWorkflow(req.scope)14    .run({15      input: {16        cart_id: cartId,17      },18    })19
20  res.json({21    type: "order",22    order: result.parent_order,23  })24}

This API route replaces the existing API route in the Medusa application used to complete the cart and place an order. It executes the workflow and returns the parent order in the response.

Test it Out#

To test this out, it’s recommended to install the Next.js Starter storefront. Then, add products to the cart and place an order. You can also try placing an order with products from different vendors.


Step 9: Retrieve Vendor Orders API Route#

In this step, you’ll create an API route that retrieves a vendor’s orders.

Create the file src/api/vendors/orders/route.ts with the following content:

src/api/vendors/orders/route.ts
5import { MARKETPLACE_MODULE } from "../../../modules/marketplace"6
7export const GET = async (8  req: AuthenticatedMedusaRequest,9  res: MedusaResponse10) => {11  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)12  const marketplaceModuleService: MarketplaceModuleService = 13    req.scope.resolve(MARKETPLACE_MODULE)14
15  const vendorAdmin = await marketplaceModuleService.retrieveVendorAdmin(16    req.auth_context.actor_id,17    {18      relations: ["vendor"],19    }20  )21
22  const { data: [vendor] } = await query.graph({23    entity: "vendor",24    fields: ["orders.*"],25    filters: {26      id: [vendorAdmin.vendor.id],27    },28  })29
30  const { result: orders } = await getOrdersListWorkflow(req.scope)31    .run({32      input: {33        fields: [34          "metadata",35          "total",36          "subtotal",37          "shipping_total",38          "tax_total",39          "items.*",40          "items.tax_lines",41          "items.adjustments",42          "items.variant",43          "items.variant.product",44          "items.detail",45          "shipping_methods",46          "payment_collections",47          "fulfillments",48        ],49        variables: {50          filters: {51            id: vendor.orders.map((order) => order.id),52          },53        },54      },55    })56
57  res.json({58    orders,59  })60}

This adds a GET API route at /vendors/orders that returns a vendor’s list of orders.

Test it Out#

To test it out, send a GET request to /vendors/orders :

Code
1curl 'http://localhost:9000/vendors/orders' \2-H 'Authorization: Bearer {token}'

Make sure to replace the {token} with the vendor admin’s token.

You’ll receive in the response the orders of the vendor created in the previous step.


Next Steps#

The next steps of this example depend on your use case. This section provides some insight into implementing them.

Use Existing Features#

You can use Medusa’s admin API routes for orders to allow vendors to manage their orders. This requires you to add the following middleware in src/api/middlewares.ts:

src/api/middlewares.ts
1export default defineMiddlewares({2  routes: [3	  // ...4    {5      matcher: "/admin/orders/*",6      method: "POST",7      middlewares: [8        authenticate("vendor", ["session", "bearer"]),9      ],10    },11  ],12})

You can also re-create or override any of the existing API routes, similar to what you did with the complete cart API route.

Similar to linking an order and a product to a vendor, you can link other data models to vendors as well.

For example, you can link sales channels to vendors or other settings.

Storefront Development#

Medusa provides a Next.js Starter storefront that you can customize to your use case.

You can also create a custom storefront. Check out the Storefront Development section to learn how to create a storefront.

Admin Development#

The Medusa Admin is extendable, allowing you to add widgets to existing pages or create new pages. Learn more about it in this documentation.

If your use case requires bigger customizations to the admin, such as showing different products and orders based on the logged-in vendor, use the admin API routes to build a custom admin.

Was this page helpful?
Edit this page