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 marketplace functionalities natively, it provides features that you can extend and a framework to support all your customization needs to build a marketplace.

Summary#

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.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

NoteThis guide provides an example of an approach to implement marketplaces. You're free to choose a different approach using Medusa's framework.
Marketplace Example Repository
Find the full code for this recipe in this repository.
OpenApi Specs for Postman
Import 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#

To add custom tables to the database, which are called data models, you create a module. A module is a re-usable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In this step, you'll create a Marketplace Module that holds the data models for a vendor and an admin and allows you to manage them.

NoteLearn more about modules in this documentation.

Create Module Directory#

A module is created under the src/modules directory of your Medusa application. So, create the directory src/modules/marketplace.

Create Data Models#

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

NoteLearn more about data models in this documentation.

In the Marketplace Module, you'll create two data models:

  • Vendor: Represents a business that sells its products in the marketplace.
  • VendorAdmin: Represents an admin of a vendor.

You create a data model in a TypeScript or JavaScript file under the models directory of a module. So, to create the Vendor data model, 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().unique(),7  name: model.text(),8  logo: model.text().nullable(),9  admins: model.hasMany(() => VendorAdmin),10})11
12export default Vendor

You define the data model using DML's define method. It accepts two parameters:

  1. The first one is the name of the data model's table in the database.
  2. The second is an object, which is the data model's schema. The schema's properties are defined using DML methods.

You define the following properties for the Vendor data model:

  • id: A primary key ID for each record.
  • handle: A unique handle for the vendor. This can be used in URLs on the storefront, such as to show a vendor's details and products.
  • name: The name of the vendor.
  • logo: The logo image of a vendor.
  • admins: The admins of a vendor. It's a relation to the VendorAdmin data model which you'll create next.
NoteLearn more about data model properties and relations.

Then, to create the VendorAdmin data model, 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

The VendorAdmin data model has the following properties:

  • id: A primary key ID for each record.
  • first_name: The first name of the admin.
  • last_name: The last name of the admin.
  • email: The email of the admin.
  • vendor: The vendor the admin belongs to. It's a relation to the Vendor data model.

Create Service#

You define data-management methods of your data models in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can perform database operations.

NoteLearn more about services in this documentation.

In this section, you'll create the Marketplace Module's service. Create the file 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
10export default MarketplaceModuleService

The MarketplaceModuleService extends MedusaService from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

So, the MarketplaceModuleService class now has methods like createVendors and retrieveVendorAdmin.

NoteFind all methods generated by the MedusaService in this reference.

You'll use this service in later steps to store and manage vendors and vendor admins.

Export Module Definition#

The final piece to a module is its definition, which you export in an index.ts file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file 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 = "marketplace"5
6export default Module(MARKETPLACE_MODULE, {7  service: MarketplaceModuleService,8})

You use the Module function from the Modules SDK to create the module's definition. It accepts two parameters:

  1. The module's name, which is marketplace.
  2. An object with a required property service indicating the module's service.

Add Module to Medusa's Configurations#

Once you finish building the module, add it to Medusa's configurations to start using it.

In medusa-config.ts, add a modules property and pass an array with your custom module:

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

Each object in the modules array has a resolve property, whose value is either a path to the module's directory, or an npm package’s name.

Generate Migrations#

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.

NoteLearn more about migrations in this documentation.

Medusa's CLI tool generates the migrations for you. To generate a migration for the Marketplace Module, run the following command in your Medusa application's directory:

Terminal
npx medusa db:generate marketplace

The db:generate command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a migrations directory under src/modules/marketplace that holds the generated migration.

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

Terminal
npx medusa db:migrate

This will create the tables for the Marketplace Module's data models in the database.

Further Reads#


Modules are isolated to ensure they're re-usable and don't have side effects when integrated into the Medusa application. So, to build associations between modules, you define module links. A Module link associates two modules' data models while maintaining module isolation.

NoteLearn more about module links in this documentation.

Each vendor should have 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 from the Sales Channel Module, define those links in a similar manner.

To define a link between the Vendor and Product data models, 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)

You define a link using defineLink from the Modules SDK. It accepts two parameters:

  1. The first data model part of the link, which is the Marketplace Module's vendor data model. A module has a special linkable property that contain link configurations for its data models.
  2. The second data model part of the link, which is the Product Module's product data model. You also enable isList, indicating that a vendor can have many products.

Next, to define a link between the Vendor and Order data models, 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)

Similarly, you define an association between the Vendor and Order data models, where a vendor can have many orders.

In the next steps, you'll see how these link allows you to retrieve and manage a vendor's products and orders.

Medusa represents the links you define in link tables similar to pivot tables. So, to sync the defined links to the database, run the db:migrate command:

Terminal
npx medusa db:migrate

This command runs any pending migrations and syncs link definitions to the database, creating the necessary tables for your links.

Further Read#


Intermission: Understanding Authentication#

Before proceeding further, you need to understand some concepts related to authenticating users, especially those of custom actor types.

An actor type is a type of user that can send an authenticated requests. Medusa has two default actor types: customer for customers, and admin for admin users.

You can also create custom actor types, allowing you to authenticate your custom users to specific routes. In this recipe, your custom actor type would be the vendor's admin.

When you create a user of the actor type (for example, a vendor admin), you must:

  1. Retrieve a registration JWT token. Medusa has a /auth/{actor_type}/emailpass/register route to retrieve a registration JWT token for the specified actor type.
  2. Create the user. This requires creating the user in the database, and associate an auth identity with that user. An auth identity allows this user to later send authenticated requests.
  3. Retrieve an authenticated JWT token using Medusa's /auth/{actor_type}/emailpass route, which retrieves the token for the specified actor type if the credentials in the request body match a user in the database.

In the next steps, you'll implement the logic to create a vendor and its admin around the above authentication flow. You can also refer to the following documentation pages to learn more about authentication in Medusa:


Step 4: Create Vendor Workflow#

To implement and expose a feature that manipulates data, you create a workflow.

A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an endpoint.

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

NoteLearn more about workflows in this documentation

The workflow’s steps are:

Medusa provides the last two steps through its @medusajs/medusa/core-flows package. So, you only need to implement the first two steps.

createVendorStep#

The first step of the workflow creates the vendor in the database using the Marketplace Module's service.

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

src/workflows/marketplace/create-vendor/steps/create-vendor.ts
1import { 2  createStep,3  StepResponse,4} from "@medusajs/framework/workflows-sdk"5import { MARKETPLACE_MODULE } from "../../../../modules/marketplace"6import MarketplaceModuleService from "../../../../modules/marketplace/service"7
8type CreateVendorStepInput = {9  name: string10  handle?: string11  logo?: string12}13
14const createVendorStep = createStep(15  "create-vendor",16  async (vendorData: CreateVendorStepInput, { container }) => {17    const marketplaceModuleService: MarketplaceModuleService = 18      container.resolve(MARKETPLACE_MODULE)19
20    const vendor = await marketplaceModuleService.createVendors(vendorData)21
22    return new StepResponse(vendor, vendor.id)23  },24  async (vendorId, { container }) => {25    if (!vendorId) {26      return27    }28
29    const marketplaceModuleService: MarketplaceModuleService = 30      container.resolve(MARKETPLACE_MODULE)31
32      marketplaceModuleService.deleteVendors(vendorId)33  }34)35
36export default createVendorStep

You create a step with createStep from the Workflows SDK. It accepts three parameters:

  1. The step's unique name, which is create-vendor.
  2. An async function that receives two parameters:
    • An input object with the details of the vendor to create.
    • The Medusa container, which is a registry of framework and commerce tools that you can access in the step.
  3. An async compensation function. This function is only executed when an error occurs in the workflow. It undoes the changes made by the step.

In the step function, you resolve the Marketplace Module's service from the container. Then, you use the service's generated createVendors method to create the vendor.

A step must return an instance of StepResponse. It accepts two parameters:

  1. The data to return from the step, which is the created vendor in this case.
  2. The data to pass as an input to the compensation function.

You pass the vendor's ID to the compensation function. In the compensation function, you delete the vendor if an error occurs in the workflow.

createVendorAdminStep#

The second step of the workflow creates the vendor's admin. So, create the file src/workflows/marketplace/create-vendor/steps/create-vendor-admin.ts with the following content:

src/workflows/marketplace/create-vendor/steps/create-vendor-admin.ts
6import { MARKETPLACE_MODULE } from "../../../../modules/marketplace"7
8type CreateVendorAdminStepInput = {9  email: string10  first_name?: string11  last_name?: string12  vendor_id: string13}14
15const createVendorAdminStep = createStep(16  "create-vendor-admin-step",17  async (18    adminData: CreateVendorAdminStepInput, 19    { container }20  ) => {21    const marketplaceModuleService: MarketplaceModuleService = 22      container.resolve(MARKETPLACE_MODULE)23
24    const vendorAdmin = await marketplaceModuleService.createVendorAdmins(25      adminData26    )27
28    return new StepResponse(29      vendorAdmin,30      vendorAdmin.id31    )32  },33  async (vendorAdminId, { container }) => {34    if (!vendorAdminId) {35      return36    }37    38    const marketplaceModuleService: MarketplaceModuleService = 39      container.resolve(MARKETPLACE_MODULE)40
41    marketplaceModuleService.deleteVendorAdmins(vendorAdminId)42  }43)44
45export default createVendorAdminStep

Similar to the previous step, you create a step that accepts the vendor admin's details as an input, and creates the vendor admin using the Marketplace Module. In the compensation function, you delete the vendor admin if an error occurs.

Create Workflow#

You can now create the workflow that creates a vendor and its admin.

Create the file src/workflows/marketplace/create-vendor/index.ts with the following content:

src/workflows/marketplace/create-vendor/index.ts
1import { 2  createWorkflow,3  WorkflowResponse,4} from "@medusajs/framework/workflows-sdk"5import { 6  setAuthAppMetadataStep,7  useQueryGraphStep,8} from "@medusajs/medusa/core-flows"9import createVendorAdminStep from "./steps/create-vendor-admin"10import createVendorStep from "./steps/create-vendor"11
12export type CreateVendorWorkflowInput = {13  name: string14  handle?: string15  logo?: string16  admin: {17    email: string18    first_name?: string19    last_name?: string20  }21  authIdentityId: string22}23
24const createVendorWorkflow = createWorkflow(25  "create-vendor",26  function (input: CreateVendorWorkflowInput) {27    const vendor = createVendorStep({28      name: input.name,29      handle: input.handle,30      logo: input.logo,31    })32
33    const vendorAdminData = transform({34      input,35      vendor,36    }, (data) => {37      return {38        ...data.input.admin,39        vendor_id: data.vendor.id,40      }41    })42
43    const vendorAdmin = createVendorAdminStep(44      vendorAdminData45    )46
47    setAuthAppMetadataStep({48      authIdentityId: input.authIdentityId,49      actorType: "vendor",50      value: vendorAdmin.id,51    })52
53    const { data: vendorWithAdmin } = useQueryGraphStep({54      entity: "vendor",55      fields: ["id", "name", "handle", "logo", "admins.*"],56      filters: {57        id: vendor.id,58      },59    })60
61    return new WorkflowResponse({62      vendor: vendorWithAdmin[0],63    })64  }65)66
67export default createVendorAdminWorkflow

You create a workflow with createWorkflow from the Workflows SDK. It accepts two parameters:

  1. The workflow's unique name, which is create-vendor.
  2. A function that receives an input object with the details of the vendor and its admin.

In the workflow function, you run the following steps:

  1. createVendorStep to create the vendor.
  2. createVendorAdminStep to create the vendor admin.
    • Notice that you use transform from the Workflows SDK to prepare the data you pass into the step. Medusa doesn't allow direct manipulation of variables within the worflow's constructor function. Learn more in the Data Manipulation in Workflows documentation.
  3. setAuthAppMetadataStep to associate the vendor admin with its auth identity of actor type vendor. This will allow the vendor admin to send authenticated requests afterwards.
  4. useQueryGraphStep to retrieve the created vendor with its admins using Query. Query allows you to retrieve data across modules.

A workflow must return a WorkflowResponse instance. It accepts as a parameter the data to return, which is the vendor in this case.

In the next step, you'll learn how to execute the workflow in an API route.

Further Read#


Step 5: Create Vendor API Route#

Now that you've implemented the logic to create a vendor, you'll expose this functionality in an API route. An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts or custom dashboards.

NoteLearn more about API routes in this documentation.

Create API Route#

An API route is created in a route.ts file under a sub-directory of the src/api directory.

The path of the API route is the file's path relative to src/api. So, to create the /vendors API route, create 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 createVendorWorkflow, { 8  CreateVendorWorkflowInput,9} from "../../workflows/marketplace/create-vendor"10
11export const PostVendorCreateSchema = z.object({12  name: z.string(),13  handle: z.string().optional(),14  logo: z.string().optional(),15  admin: z.object({16    email: z.string(),17    first_name: z.string().optional(),18    last_name: z.string().optional(),19  }).strict(),20}).strict()21
22type RequestBody = z.infer<typeof PostVendorCreateSchema>

You start by defining the accepted fields in incoming request bodies using Zod. You'll later learn how to enforce the schema validation on all incoming requests.

Then, to create the API route, add the following content 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 vendorData = req.validatedBody15
16  // create vendor admin17  const { result } = await createVendorWorkflow(req.scope)18    .run({19      input: {20        ...vendorData,21        authIdentityId: req.auth_context.auth_identity_id,22      } as CreateVendorWorkflowInput,23    })24
25  res.json({26    vendor: result.vendor,27  })28}

Since you export a POST function in this file, you're exposing a POST API route at /vendors. The route handler function accepts two parameters:

  1. A request object with details and context on the request, such as body parameter or authenticated user details.
  2. A response object to manipulate and send the response.

In the function, you first check that the user accessing the request isn't already registered (as a vendor admin). Then, you execute the createVendorWorkflow from the previous step, passing it the request body.

You also pass the workflow the ID of the auth identity to associate the vendor admin with. This auth identity is set in the request's context because you'll later pass the registration JWT token in the request's header.

Finally, you return the created vendor in the response.

Apply Authentication and Validation Middlewares#

To ensure that incoming request bodies contain the required parameters, and that only vendor admins with a registration token can access this route, you'll add middlewares to the API route.

A middleware is a function executed before the API route when a request is sent to it. Middlewares are useful to restrict access to an API route based on validation or authentication requirements.

NoteLearn more about middlewares in this documentation.

You define middlewares in Medusa in the src/api/middlewares.ts special file. So, create the file src/api/middlewares.ts with the following content:

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

In this file, you export the middlewares definition using defineMiddlewares from the Medusa Framework. This function accepts an object having a routes property, which is an array of middleware configurations to apply on routes.

You pass in the routes array objects having the following properties:

  • matcher: The route to apply the middleware on.
  • method: Optional HTTP methods to apply the middleware on for the specified API route.
  • middlewares: An array of the middlewares to apply.

You first apply two middlewares to the POST /vendors API route you just created:

  • authenticate: Ensure that the user sending the request has a registration JWT token.
  • validateAndTransformBody: Validate that the incoming request body matches the Zod schema that you created in the API route's file.

You also apply the authenticate middleware on all routes starting with /vendors* to ensure they can only be accessed by authenticated vendor admin. Note that since you don't enable allowUnregistered, the vendor admin must be registered to access these routes.

Test it Out#

To test out the above API route, start the Medusa application:

Then, you must retrieve a registration JWT token to access the Create Vendor API route. To obtain it, send a POST request to 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": "vendor@exampl.com",5    "password": "supersecret"6}'

You can replace the email and password with other credentials.

Then, to create a vendor and its admin, 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": "vendor@example.com,9        "first_name": "Admin",10        "last_name": "Acme"11    }12}'

Make sure to replace {token} with the registration token you retrieved. If you changed the email previously, make sure to change it here as well.

This will return the created vendor and its admin.

You can now retrieve an authenticated token of the vendor admin. To do that, send a POST 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": "vendor@example.com,5    "password": "supersecret"6}'

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

Further Reads#


Step 6: Create Product API Route#

Now that you support creating vendors, you want to allow these vendors to manage their products.

In this step, you'll create a workflow that creates a product, then use that workflow in a new API route.

Create Product Workflow#

The workflow to create a product has the following steps:

The workflow's steps are all provided by Medusa's @medusajs/medusa/core-flows package. So, you can create the workflow right away.

Create the file src/workflows/marketplace/create-vendor-product/index.ts with the following content:

src/workflows/marketplace/create-vendor-product/index.ts
1import { CreateProductWorkflowInputDTO } from "@medusajs/framework/types"2import { 3  createWorkflow, 4  transform, 5  WorkflowResponse,6} from "@medusajs/framework/workflows-sdk"7import { 8  createProductsWorkflow, 9  createRemoteLinkStep, 10  useQueryGraphStep,11} from "@medusajs/medusa/core-flows"12import { MARKETPLACE_MODULE } from "../../../modules/marketplace"13import { Modules } from "@medusajs/framework/utils"14
15type WorkflowInput = {16  vendor_admin_id: string17  product: CreateProductWorkflowInputDTO18}19
20const createVendorProductWorkflow = createWorkflow(21  "create-vendor-product",22  (input: WorkflowInput) => {23    // Retrieve default sales channel to make the product available in.24    // Alternatively, you can link sales channels to vendors and allow vendors25    // to manage sales channels26    const { data: stores } = useQueryGraphStep({27      entity: "store",28      fields: ["default_sales_channel_id"],29    })30
31    const productData = transform({32      input,33      stores,34    }, (data) => {35      return {36        products: [{37          ...data.input.product,38          sales_channels: [39            {40              id: data.stores[0].default_sales_channel_id,41            },42          ],43        }],44      }45    })46
47    const createdProducts = createProductsWorkflow.runAsStep({48      input: productData,49    })50    51    // TODO link vendor and products52  }53)54
55export default createVendorProductWorkflow

The workflow accepts two parameters:

  • vendor_admin_id: The ID of the vendor admin creating the product.
  • product: The details of the product to create.

In the workflow, you first retrieve the default sales channel in the store. This is necessary, as the product can only be purchased in the sales channels it's available in.

Then, you prepare the product's data, combining what's passed in the input and the default sales channel's ID. Finally, you create the product.

Next, you want to create a link between the product and the vendor it's created for. So, replace the TODO with the following:

src/workflows/marketplace/create-vendor-product/index.ts
1const { data: vendorAdmins } = useQueryGraphStep({2  entity: "vendor_admin",3  fields: ["vendor.id"],4  filters: {5    id: input.vendor_admin_id,6  },7}).config({ name: "retrieve-vendor-admins" })8
9const linksToCreate = transform({10  input,11  createdProducts,12  vendorAdmins,13}, (data) => {14  return data.createdProducts.map((product) => {15    return {16      [MARKETPLACE_MODULE]: {17        vendor_id: data.vendorAdmins[0].vendor.id,18      },19      [Modules.PRODUCT]: {20        product_id: product.id,21      },22    }23  })24})25
26createRemoteLinkStep(linksToCreate)27
28const { data: products } = useQueryGraphStep({29  entity: "product",30  fields: ["*", "variants.*"],31  filters: {32    id: createdProducts[0].id,33  },34}).config({ name: "retrieve-products" })35
36return new WorkflowResponse({37  product: products[0],38})

You retrieve the ID of the admin's vendor. Then, you prepare the data to create a link.

Medusa provides a createRemoteLinkStep that allows you to create links between records of different modules. The step accepts as a parameter an array of link objects, where each object has the module name as the key and the ID of the record to link as the value. The modules must be passed in the same order they were passed in to defineLink.

NoteRefer to the Link documentation to learn more about creating links.

Finally, you retrieve the created product's details using Query and return the product.

Create API Route#

Next, you'll create the API route that uses the above workflow to create a product for a vendor.

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

src/api/vendors/products/route.ts
1import { 2  AuthenticatedMedusaRequest, 3  MedusaResponse,4} from "@medusajs/framework/http"5import { 6  HttpTypes,7} from "@medusajs/framework/types"8import createVendorProductWorkflow from "../../../workflows/marketplace/create-vendor-product"9
10export const POST = async (11  req: AuthenticatedMedusaRequest<HttpTypes.AdminCreateProduct>,12  res: MedusaResponse13) => {14  const { result } = await createVendorProductWorkflow(req.scope)15    .run({16      input: {17        vendor_admin_id: req.auth_context.actor_id,18        product: req.validatedBody,19      },20    })21
22  res.json({23    product: result.product,24  })25}

Since you export a POST function, you're exposing a POST API route at /vendors/products.

In the route handler, you execute the createVendorProductWorkflow workflow, passing it the authenticated vendor admin's ID and the request body, which holds the details of the product to create. Finally, you return the product.

Apply Validation Middleware#

Since the above API route requires passing the product's details in the request body, you need to apply a validation middleware on it.

In src/api/middlewares.ts, add a new middleware route object:

src/api/middlewares.ts
1// other imports...2import { AdminCreateProduct } from "@medusajs/medusa/api/admin/products/validators"3
4export default defineMiddlewares({5  routes: [6    // ...7    {8      matcher: "/vendors/products",9      method: ["POST"],10      middlewares: [11        validateAndTransformBody(AdminCreateProduct),12      ],13    },14  ],15})

Similar to before, you apply the validateAndTransformBody middleware on the POST /vendors/products API route. You pass to the middleware the AdminCreateProduct schema that Medusa uses to validate the request body of the Create Product Admin API Route.

Test it Out#

To test it out, start the Medusa application:

Then, send the following request to /vendors/products to create a product for the vendor:

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

Make sure to replace {token} with the authenticated token of the vendor admin you retrieved earlier.

This will return the created product. In the next step, you'll add API routes to retrieve the vendor's products.

Further Reads#


Step 7: Retrieve Products API Route#

In this step, you'll add the API route to retrieve a vendor's products.

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

src/api/vendors/products/route.ts
1// other imports...2import { 3  ContainerRegistrationKeys,4} from "@medusajs/framework/utils"5
6export const GET = async (7  req: AuthenticatedMedusaRequest,8  res: MedusaResponse9) => {10  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)11
12  const { data: [vendorAdmin] } = await query.graph({13    entity: "vendor_admin",14    fields: ["vendor.products.*"],15    filters: {16      id: [17        // ID of the authenticated vendor admin18        req.auth_context.actor_id,19      ],20    },21  })22
23  res.json({24    products: vendorAdmin.vendor.products,25  })26}

You add a GET API route at /vendors/products. In the route handler, you use Query to retrieve the list of products of the authenticated admin's vendor and returns them in the response. You can retrieve the linked records since Query retrieves data across modules.

Test it Out#

To test out the new API routes, start the Medusa application:

Then, 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}'

Make sure to replace {token} with the authenticated token of the vendor admin you retrieved earlier.

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:

You only need to implement the third and fourth steps, as Medusa provides the rest of the steps in its @medusajs/medusa/core-flows package.

groupVendorItemsStep#

The third step of the workflow returns an object of items grouped by their vendor.

To create the step, create the file src/workflows/marketplace/create-vendor-orders/steps/group-vendor-items.ts with the following content:

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, promiseAll } 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 promiseAll(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 receives the cart's details as an input. In the step, you group the items by the vendor associated with the product into an object and returns the object. You use Query to retrieve a product's vendor.

createVendorOrdersStep#

The fourth step of the workflow creates an order for each vendor. The order consists of the items in the parent order that belong to the vendor.

Create the file src/workflows/marketplace/create-vendor-orders/steps/create-vendor-orders.ts with the following content:

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: MarketplaceModuleService =47      container.resolve(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#

Now that you have all the necessary steps, you can create the workflow.

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

The workflow receives the cart's ID as an input. 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 the parent 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-vendor/route.ts with the following content:

src/api/store/carts/[id]/complete-vendor/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}

Since you expose a POST function, you're exposing a POST API route at /store/carts/:id/complete-vendor. In the route handler, you execute the createVendorOrdersWorkflow and return the created order.

Test it Out#

To test this out, it’s recommended to install the Next.js Starter storefront.

Then, you need to customize the storefront to use your complete cart API route rather than Medusa's. In src/lib/data/cart.ts, find the following lines in the src/lib/data/cart.ts:

Storefront
src/lib/data/cart.ts
1const cartRes = await sdk.store.cart2  .complete(id, {}, headers)3  .then(async (cartRes) => {4    const cartCacheTag = await getCacheTag("carts")5    revalidateTag(cartCacheTag)6    return cartRes7  })8  .catch(medusaError)

Replace them with the following:

Storefront
src/lib/data/cart.ts
1const cartRes = await sdk.client.fetch<HttpTypes.StoreCompleteCartResponse>(2  `/store/carts/${id}/complete-vendor`, {3    method: "POST",4    headers,5  })6  .then(async (cartRes) => {7    const cartCacheTag = await getCacheTag("carts")8    revalidateTag(cartCacheTag)9    return cartRes10  })11  .catch(medusaError)

Now, the checkout flow uses your custom API route to place the order instead of Medusa's.

NoteRefer to the JS SDK documentation to learn more about using it.

Try going through the checkout flow now, purchasing a product that you created for the vendor earlier. The order should be placed successfully.

In the next step, you'll create an API route to retrieve the vendor's orders, allowing you to confirm that the child order was created for the vendor.


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
1import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"2import { ContainerRegistrationKeys } from "@medusajs/framework/utils"3import { getOrdersListWorkflow } from "@medusajs/medusa/core-flows"4
5export const GET = async (6  req: AuthenticatedMedusaRequest,7  res: MedusaResponse8) => {9  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)10
11  const { data: [vendorAdmin] } = await query.graph({12    entity: "vendor_admin",13    fields: ["vendor.orders.*"],14    filters: {15      id: [req.auth_context.actor_id],16    },17  })18
19  const { result: orders } = await getOrdersListWorkflow(req.scope)20    .run({21      input: {22        fields: [23          "metadata",24          "total",25          "subtotal",26          "shipping_total",27          "tax_total",28          "items.*",29          "items.tax_lines",30          "items.adjustments",31          "items.variant",32          "items.variant.product",33          "items.detail",34          "shipping_methods",35          "payment_collections",36          "fulfillments",37        ],38        variables: {39          filters: {40            id: vendorAdmin.vendor.orders.map((order) => order.id),41          },42        },43      },44    })45
46  res.json({47    orders,48  })49}

You add a GET API route at /vendors/orders. In the route handler, you first use Query to retrieve the orders of the authenticated admin's vendor. Then, you use Medusa's getOrdersListWorkflow to retrieve the list of orders with the specified fields.

Test it Out#

To test it out, start the Medusa application:

Then, 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 authentication 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#

If you want vendors to perform actions that are available for admin users through Medusa's Admin API routes, such as managing their orders, you need to recreate them similar to the create product API route you created earlier.

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 or other settings to vendors.

Storefront Development#

Medusa provides a Next.js Starter storefront, which you can customize to fit your specific 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 custom widgets to existing pages or create entirely new pages. For example, you can add a new page showing the list of vendors. Learn more about it in this documentation.

Only super admins can access the Medusa Admin, not vendor admins. So, if you need a dashboard specific to each vendor admin, you will need to build a custom dashboard with the necessary features.

Was this page helpful?
Ask Anything
FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break