- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
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:
- Manage multiple vendors, each having vendor admins.
- Allow vendor admins to manage the vendor’s products and orders.
- Split orders placed by customers into multiple orders for each vendor.
Step 1: Install a Medusa Application#
Start by installing the Medusa application on your machine with the following command:
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.
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.
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:
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:
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:
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:
Add Module to Medusa Configuration#
Finally, add the module to the list of modules in medusa-config.ts
:
Further Reads#
Step 3: Define Links to Product and Order Data Models#
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.
SalesChannel
, define those links in a similar manner.Create the file src/links/vendor-product.ts
with the following content:
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:
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#
Step 4: Run Migrations and Sync Links#
To create tables for the marketplace data models in the database, start by generating the migrations for the Marketplace Module with the following command:
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:
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:
- Create the vendor admin using the Marketplace Module’s main service.
- 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:
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:
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:
createVendorAdminStep
to create the vendor admin.setAuthAppMetadataStep
to create thevendor
actor type. This step is provided by Medusa in the@medusajs/medusa/core-flows
package.
You return the created vendor admin.
Further Read#
- How to Create a Workflow
- What is an Actor Type
- How to Create an Actor Type
- What is a Compensation Function
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:
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:
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:
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:
- On the
/vendors
POST API route; it requires authentication but allows unregistered users. - 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:
- Start the Medusa application:
- Retrieve a JWT token from the
/auth/vendor/emailpass/register
API route:
vendor
actor type previously.- Send a request to the
/vendors
API route, passing the token retrieved from the previous response in the request header:
This returns the created vendor and admin.
- Retrieve an authenticated token of the vendor admin by sending another request to the
/auth/vendor/emailpass
API route:
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:
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:
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:
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.
Finally, in src/api/middlewares.ts
, apply a middleware on the create products route to validate the request body before executing the route handler:
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:
- Send a
POST
request to/vendors/products
to create a product:
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}'
- Send a
GET
request to/vendors/products
to retrieve the vendor’s products:
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:
- Retrieve the cart using its ID. Medusa provides a
useQueryGraphStep
in the@medusajs/medusa/core-flows
package that you can use. - 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. - Group the cart items by their product’s associated vendor.
- Retrieve the order's details using Medusa's
getOrderDetailWorkflow
exported by the@medusajs/medusa/core-flows
package. - For each vendor, create a child order with the cart items of their products, and return the orders with the links to be created.
- 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
:
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
:
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:
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.
Next, replace the new TODO
with the following snippet:
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:
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.
data
property of the shipping method.Finally, replace the TODO
in the compensation function with the following:
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
:
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:
useQueryGraphStep
to retrieve the cart's details.completeCartWorkflow
to complete the cart and create a parent order.groupVendorItemsStep
to group the order's items by their vendor.getOrderDetailWorkflow
to retrieve an order's details.createVendorOrdersStep
to create child orders for each vendor's items.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:
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:
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
:
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
:
You can also re-create or override any of the existing API routes, similar to what you did with the complete cart API route.
Link Other Data Models to Vendors#
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.