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:
- 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.
You can follow this guide whether you're new to Medusa or an advanced Medusa developer.
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#
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.
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.
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:
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 mappedBy: "vendor",11 }),12})13 14export default Vendor
You define the data model using DML's define method. It accepts two parameters:
- The first one is the name of the data model's table in the database.
- 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 theVendorAdmindata model which you'll create next.
Then, to create the VendorAdmin data model, 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
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 theVendordata 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.
In this section, you'll create the Marketplace Module's service. Create the file 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 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.
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:
You use the Module function from the Modules SDK to create the module's definition. It accepts two parameters:
- The module's name, which is
marketplace. - An object with a required property
serviceindicating 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:
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.
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:
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:
This will create the tables for the Marketplace Module's data models in the database.
Further Reads#
Step 3: Define Links to Product and Order Data Models#
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.
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.
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:
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.id,9 isList: true,10 }11)
You define a link using defineLink from the Modules SDK. It accepts two parameters:
- The first data model part of the link, which is the Marketplace Module's
vendordata model. A module has a speciallinkableproperty that contain link configurations for its data models. - The second data model part of the link, which is the Product Module's
productdata model. You also enableisList, 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:
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.id,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.
Sync Links to Database#
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:
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:
- Retrieve a registration JWT token. Medusa has a
/auth/{actor_type}/emailpass/registerroute to retrieve a registration JWT token for the specified actor type. - 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.
- Retrieve an authenticated JWT token using Medusa's
/auth/{actor_type}/emailpassroute, 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.
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:
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:
- The step's unique name, which is
create-vendor. - 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.
- 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:
- The data to return from the step, which is the created vendor in this case.
- 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:
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:
1import { 2 createWorkflow,3 WorkflowResponse,4 transform,5} from "@medusajs/framework/workflows-sdk"6import { 7 setAuthAppMetadataStep,8 useQueryGraphStep,9} from "@medusajs/medusa/core-flows"10import createVendorAdminStep from "./steps/create-vendor-admin"11import createVendorStep from "./steps/create-vendor"12 13export type CreateVendorWorkflowInput = {14 name: string15 handle?: string16 logo?: string17 admin: {18 email: string19 first_name?: string20 last_name?: string21 }22 authIdentityId: string23}24 25const createVendorWorkflow = createWorkflow(26 "create-vendor",27 function (input: CreateVendorWorkflowInput) {28 const vendor = createVendorStep({29 name: input.name,30 handle: input.handle,31 logo: input.logo,32 })33 34 const vendorAdminData = transform({35 input,36 vendor,37 }, (data) => {38 return {39 ...data.input.admin,40 vendor_id: data.vendor.id,41 }42 })43 44 const vendorAdmin = createVendorAdminStep(45 vendorAdminData46 )47 48 setAuthAppMetadataStep({49 authIdentityId: input.authIdentityId,50 actorType: "vendor",51 value: vendorAdmin.id,52 })53 // @ts-ignore54 const { data: vendorWithAdmin } = useQueryGraphStep({55 entity: "vendor",56 fields: ["id", "name", "handle", "logo", "admins.*"],57 filters: {58 id: vendor.id,59 },60 })61 62 return new WorkflowResponse({63 vendor: vendorWithAdmin[0],64 })65 }66)67 68export default createVendorWorkflow
You create a workflow with createWorkflow from the Workflows SDK. It accepts two parameters:
- The workflow's unique name, which is
create-vendor. - 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:
createVendorStepto create the vendor.createVendorAdminStepto create the vendor admin.- Notice that you use
transformfrom the Workflows SDK to prepare the data you pass into the step. Medusa doesn't allow direct manipulation of variables within the workflow's constructor function. Learn more in the Data Manipulation in Workflows documentation.
- Notice that you use
setAuthAppMetadataStepto associate the vendor admin with its auth identity of actor typevendor. This will allow the vendor admin to send authenticated requests afterwards.useQueryGraphStepto 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#
- How to Create a Workflow
- What is an Actor Type
- How to Create an Actor Type
- What is a Compensation Function
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.
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:
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:
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:
- A request object with details and context on the request, such as body parameter or authenticated user details.
- 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.
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:
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:
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:
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:
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:
View step details
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:
1import { CreateProductWorkflowInputDTO } from "@medusajs/framework/types"2import { 3 createWorkflow, 4 transform, 5 WorkflowResponse,6} from "@medusajs/framework/workflows-sdk"7import { 8 createProductsWorkflow, 9 CreateProductsWorkflowInput, 10 createRemoteLinkStep, 11 useQueryGraphStep,12} from "@medusajs/medusa/core-flows"13import { MARKETPLACE_MODULE } from "../../../modules/marketplace"14import { Modules } from "@medusajs/framework/utils"15 16type WorkflowInput = {17 vendor_admin_id: string18 product: CreateProductWorkflowInputDTO19}20 21const createVendorProductWorkflow = createWorkflow(22 "create-vendor-product",23 (input: WorkflowInput) => {24 // Retrieve default sales channel to make the product available in.25 // Alternatively, you can link sales channels to vendors and allow vendors26 // to manage sales channels27 const { data: stores } = useQueryGraphStep({28 entity: "store",29 fields: ["default_sales_channel_id"],30 })31 32 const productData = transform({33 input,34 stores,35 }, (data) => {36 return {37 products: [{38 ...data.input.product,39 sales_channels: [40 {41 id: data.stores[0].default_sales_channel_id,42 },43 ],44 }],45 }46 })47 48 const createdProducts = createProductsWorkflow.runAsStep({49 input: productData as CreateProductsWorkflowInput,50 })51 52 // TODO link vendor and products53 }54)55 56export 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:
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.
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:
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:
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:
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:
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:
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:
View step details
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:
1import { 2 createStep,3 StepResponse,4} from "@medusajs/framework/workflows-sdk"5import { CartLineItemDTO } from "@medusajs/framework/types"6import { ContainerRegistrationKeys, promiseAll } from "@medusajs/framework/utils"7 8export type GroupVendorItemsStepInput = {9 cart: {10 items?: CartLineItemDTO[]11 }12}13 14const groupVendorItemsStep = createStep(15 "group-vendor-items",16 async ({ cart }: GroupVendorItemsStepInput, { container }) => {17 const query = container.resolve(ContainerRegistrationKeys.QUERY)18 19 const vendorsItems: Record<string, CartLineItemDTO[]> = {}20 21 await promiseAll((cart.items || []).map(async (item) => {22 const { data: [product] } = await query.graph({23 entity: "product",24 fields: ["vendor.*"],25 filters: {26 id: item.product_id || "",27 },28 })29 30 const vendorId = product.vendor?.id31 32 if (!vendorId) {33 return34 }35 vendorsItems[vendorId] = [36 ...(vendorsItems[vendorId] || []),37 item,38 ]39 }))40 41 return new StepResponse({42 vendorsItems,43 })44 }45)46 47export 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:
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 (data, { 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 occurred 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#
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:
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 } as unknown as GroupVendorItemsStepInput)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 "shipping_methods.tax_lines.*",53 "shipping_methods.adjustments.*",54 ],55 },56 })57 58 const { 59 orders: vendorOrders, 60 linkDefs,61 } = createVendorOrdersStep({62 parentOrder: order,63 vendorsItems,64 })65 66 createRemoteLinkStep(linkDefs)67 68 return new WorkflowResponse({69 parent_order: order,70 vendor_orders: vendorOrders,71 })72 }73)74 75export default createVendorOrdersWorkflow
The workflow receives the cart's ID as an input. In the workflow, you run the following steps:
useQueryGraphStepto retrieve the cart's details.completeCartWorkflowto complete the cart and create a parent order.groupVendorItemsStepto group the order's items by their vendor.getOrderDetailWorkflowto retrieve the parent order's details.createVendorOrdersStepto create child orders for each vendor's items.createRemoteLinkStepto 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:
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:
Replace them with the following:
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.
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:
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 :
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.
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 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.