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})11 12export 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 theVendorAdmin
data 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 theVendor
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.
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
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:
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,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
vendor
data model. A module has a speciallinkable
property that contain link configurations for its data models. - The second data model part of the link, which is the Product Module's
product
data 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,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/register
route 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}/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.
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} 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:
- 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:
createVendorStep
to create the vendor.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.
- Notice that you use
setAuthAppMetadataStep
to associate the vendor admin with its auth identity of actor typevendor
. This will allow the vendor admin to send authenticated requests afterwards.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#
- 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 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:
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 { 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:
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:
1if (vendorIds.length === 1) {2 linkDefs.push({3 [MARKETPLACE_MODULE]: {4 vendor_id: vendors[0].id,5 },6 [Modules.ORDER]: {7 order_id: parentOrder.id,8 },9 })10 11 createdOrders.push({12 ...parentOrder,13 vendor: vendors[0],14 })15 16 return new StepResponse({17 orders: createdOrders,18 linkDefs,19 }, {20 created_orders: [],21 })22}23 24// TODO create multiple child orders
In the above snippet, if there's only one vendor in the group, the parent order is added to the linkDefs
array and it's returned in the response.
Next, replace the new TODO
with the following snippet:
1try {2 await promiseAll(3 vendorIds.map(async (vendorId) => {4 const items = vendorsItems[vendorId]5 const vendor = vendors.find((v) => v.id === vendorId)!6 7 const { result: childOrder } = await createOrderWorkflow(8 container9 )10 .run({11 input: prepareOrderData(items, parentOrder),12 context,13 }) as unknown as { result: VendorOrder }14 15 childOrder.vendor = vendor16 createdOrders.push(childOrder)17 18 linkDefs.push({19 [MARKETPLACE_MODULE]: {20 vendor_id: vendor.id,21 },22 [Modules.ORDER]: {23 order_id: childOrder.id,24 },25 })26 })27 )28} catch (e) {29 return StepResponse.permanentFailure(30 `An error occured while creating vendor orders: ${e}`,31 {32 created_orders: createdOrders,33 }34 )35}
In this snippet, you create multiple child orders for each vendor and link the orders to the vendors.
You use promiseAll
from the Workflows SDK that loops over an array of promises and ensures that all transactions within these promises are rolled back in case an error occurs. You also wrap promiseAll
in a try-catch block, and in the catch block you invoke and return StepResponse.permanentFailure
which indicates that the step has failed but still invokes the compensation function that you'll implement in a bit. The first parameter of permanentFailure
is the error message, and the second is the data to pass to the compensation function.
If an error occurs, the created orders in the createdOrders
array are canceled using Medusa's cancelOrderWorkflow
from the @medusajs/medusa/core-flows
package.
The order's data is formatted using the prepareOrderData
function. Replace its definition with the following:
1function prepareOrderData(2 items: CartLineItemDTO[], 3 parentOrder: OrderDTO4) {5 return {6 items,7 metadata: {8 parent_order_id: parentOrder.id,9 },10 // use info from parent11 region_id: parentOrder.region_id,12 customer_id: parentOrder.customer_id,13 sales_channel_id: parentOrder.sales_channel_id,14 email: parentOrder.email,15 currency_code: parentOrder.currency_code,16 shipping_address_id: parentOrder.shipping_address?.id,17 billing_address_id: parentOrder.billing_address?.id,18 // A better solution would be to have shipping methods for each19 // item/vendor. This requires changes in the storefront to commodate that20 // and passing the item/vendor ID in the `data` property, for example.21 // For simplicity here we just use the same shipping method.22 shipping_methods: parentOrder.shipping_methods.map((shippingMethod) => ({23 name: shippingMethod.name,24 amount: shippingMethod.amount,25 shipping_option_id: shippingMethod.shipping_option_id,26 data: shippingMethod.data,27 tax_lines: shippingMethod.tax_lines.map((taxLine) => ({28 code: taxLine.code,29 rate: taxLine.rate,30 provider_id: taxLine.provider_id,31 tax_rate_id: taxLine.tax_rate_id,32 description: taxLine.description,33 })),34 adjustments: shippingMethod.adjustments.map((adjustment) => ({35 code: adjustment.code,36 amount: adjustment.amount,37 description: adjustment.description,38 promotion_id: adjustment.promotion_id,39 provider_id: adjustment.provider_id,40 })),41 })),42 }43}
This formats the order's data using the items and parent order's details.
data
property of the shipping method.Finally, replace the TODO
in the compensation function with the following:
The compensation function cancels all child orders received from the step. It uses the cancelOrderWorkflow
that Medusa provides in the @medusajs/medusa/core-flows
package.
Create Workflow#
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 })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:
useQueryGraphStep
to retrieve the cart's details.completeCartWorkflow
to complete the cart and create a parent order.groupVendorItemsStep
to group the order's items by their vendor.getOrderDetailWorkflow
to retrieve the parent order's details.createVendorOrdersStep
to create child orders for each vendor's items.createRemoteLinkStep
to create the links returned by the previous step.
You return the parent and vendor orders.
Create API Route Executing the Workflow#
You’ll now create the API route that executes the workflow.
Create the file src/api/store/carts/[id]/complete-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.