đ¨ The content of this tutorial may be outdated. You can instead check out the full code for the series in this GitHub Repository.đ¨
Medusa is an open source headless commerce platform that allows you to create your own store in a matter of minutes. Part of what makes Medusa a good choice for your ecommerce store is its extensibility. Now, it is also possible to create multi-vendor marketplaces using Medusa.
To make things easier for our open source community, Adrien de Peretti, one of our amazing contributors, created a Medusa module that allows you to extend anything and everything you want.
"I've been looking for an e-commerce solution that could provide me with some core features while being fully customisable... After some research, where I found that none of the present solutions could provide what I needed, I chose Medusa as it provided me with many of the needed features while being easy to extend. I ended up loving the community atmosphere, especially the proximity with the team, and have been helping those in the community looking for a similar fully-customisable solution by sharing a part of my private project. This is how the medusa-extender was born." â Adrien de Peretti
In this tutorial, youâll learn how to install and set up the Medusa Extender module on your Medusa server. Youâll then learn how to use its customization abilities to create a marketplace in your store! The marketplace will have multiple stores or vendors, and each of these stores will be able to add its own products. This tutorial will be the first part of a series that will explore all aspects of creating a marketplace.
What is Medusa Extender
Medusa Extender is an NPM package that you can add to your Medusa store to extend or customize its functionalities. The scope of its customization entails Entities, Repositories, Services, and more.
The Medusa Extender has many use cases aside the marketplace functionality. It can be used in many other use cases, such as adding custom fields, listening to events to perform certain actions like sending emails, customizing Medusaâs validation of request parameters, and more.
What Youâll Be Creating
In this article and the following parts of this series, youâll learn how to create a marketplace using Medusa and Medusa Extender. A marketplace is an online store that allows multiple vendors to add their products and sell them.
A marketplace has a lot of features, including managing a vendor's own orders and settings. This part of the tutorial will only showcase how to create stores for each user and attach the products they create to that store.
Code for This Tutorial
If you want to follow along you can find the code for this tutorial in this repository.
Alternatively, if you want to install the marketplace into your existing Medusa store, you can install the Medusa Marketplace plugin. This plugin is created with the code from this tutorial and will be updated with every new part of this series released.
Prerequisites
Before you follow along with this tutorial, make sure you have:
- A Medusa server instance was installed. You can follow along with our easy quickstart guide to learn how you can do that.
- PostgreSQL installed and your Medusa server connected to it.
- Redis installed and your Medusa server connected to it.
Building the Marketplace
Project Setup
In the directory that holds your Medusa server, start by installing Medusa Extender using NPM:
npm i medusa-extender
Itâs recommended that you use TypeScript in your project to get the full benefits of Medusa-Extender. To do that, create the file tsconfig.json
in the root of the Medusa project with the following content:
{
"compilerOptions": {
"module": "CommonJS",
"declaration": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"target": "es2017",
"sourceMap": true,
"skipLibCheck": true,
"allowJs": true,
"outDir": "dist",
"rootDir": ".",
"esModuleInterop": true
},
"include": ["src", "medusa-config.js"],
"exclude": ["dist", "node_modules", "**/*.spec.ts"]
}
Next, update the scripts
key in package.json
with the following content:
"scripts": {
"seed": "medusa seed -f ./data/seed.json",
"build": "rm -rf dist && tsc",
"start": "npm run build && node dist/src/main.js",
},
These scripts will ensure that your TypeScript files will be transpiled before Medusa is run.
Then, create the file main.ts
in the directory src
with the following content:
import { Medusa } from 'medusa-extender';
import express = require('express');
async function bootstrap() {
const expressInstance = express();
await new Medusa(__dirname + '/../', expressInstance).load([]);
expressInstance.listen(9000, () => {
console.info('Server successfully started on port 9000');
});
}
bootstrap();
This file will make sure to load all the customizations youâll add next when you run your Medusa server.
Now, Medusa Extender is fully integrated into your Medusa instance and you can start building the Marketplace.
Customize the Store Entity
Youâll start by customizing the Store entity. Youâll need to use it later on to add relations between the store entity and the users and products entities.
By convention, customizations using Medusa Extender are organized in a module-like structure. However, this is completely optional.
In the src
directory, create the directory modules
in which youâll store all the customizations in.
Then, create the directory store
inside the modules
directory. The store
directory will hold all customizations related to the Store.
Create a Store Entity
Create the file src/modules/store/entities/store.entity.ts
with the following content:
import { Store as MedusaStore } from '@medusajs/medusa/dist';
import { Entity, JoinColumn, OneToMany } from 'typeorm';
import { Entity as MedusaEntity } from 'medusa-extender';
@MedusaEntity({ override: MedusaStore })
@Entity()
export class Store extends MedusaStore {
//TODO add relations
}
This uses the decorator @Entity
from medusa-extender
to customize Medusaâs Store
entity. You create a Store
class that extends Medusaâs Store entity (imported as MedusaStore
).
Youâll, later on, edit this entity to add the relations between the store and users and products.
Create a Store Repository
Next, you need to override Medusaâs StoreRepository
. This repository will return Medusaâs Store
entity. So, you need to override it to make sure it returns your Store
entity that you just created.
Create the file src/modules/store/repositories/store.repository.ts
with the following content:
import { EntityRepository } from 'typeorm';
import { StoreRepository as MedusaStoreRepository } from '@medusajs/medusa/dist/repositories/store';
import { Repository as MedusaRepository, Utils } from 'medusa-extender';
import { Store } from '../entities/store.entity';
@MedusaRepository({ override: MedusaStoreRepository })
@EntityRepository(Store)
export default class StoreRepository extends Utils.repositoryMixin<Store, MedusaStoreRepository>(MedusaStoreRepository) {
}
Create the Store Module
For now, these are the only files youâll add for the store. You can create the Store module using these files.
Create the file src/modules/store/store.module.ts
with the following content:
import { Module } from 'medusa-extender';
import { Store } from './entities/store.entity';
import StoreRepository from './repositories/store.repository';
@Module({
imports: [Store, StoreRepository],
})
export class StoreModule {}
This uses the @Module
decorator from medusa-extender
and imports the 2 classes you created.
The last thing left is to import this module and use it with Medusa. In src/main.ts
import StoreModule
at the beginning of the file:
import { StoreModule } from './modules/store/store.module';
Then, add the StoreModule
in the array passed as a parameter to Medusa.load
:
await new Medusa(__dirname + '/../', expressInstance).load([
StoreModule
]);
This is all that youâll do for now in the Store module. In the next sections, youâll be adding more classes to it as necessary.
Customize the User Entity
In this section, youâll customize the user entity mainly to link the user to a store.
Create the User Entity
Create the directory user
inside the modules
directory and create the file src/modules/user/entities/user.entity.ts
with the following content:
import { User as MedusaUser } from '@medusajs/medusa/dist';
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import { Entity as MedusaEntity } from 'medusa-extender';
import { Store } from '../../store/entities/store.entity';
@MedusaEntity({ override: MedusaUser })
@Entity()
export class User extends MedusaUser {
@Index()
@Column({ nullable: false })
store_id: string;
@ManyToOne(() => Store, (store) => store.members)
@JoinColumn({ name: 'store_id' })
store: Store;
}
This class will add an additional column store_id
of type string and will add a relation to the Store
entity.
To add the new column to the user
table in the database, you need to create a Migration file. Create the file src/modules/user/user.migration.ts
with the following content:
import { Migration } from 'medusa-extender';
import { MigrationInterface, QueryRunner } from 'typeorm';
@Migration()
export default class addStoreIdToUser1644946220401 implements MigrationInterface {
name = 'addStoreIdToUser1644946220401';
public async up(queryRunner: QueryRunner): Promise<void> {
const query = `ALTER TABLE public."user" ADD COLUMN IF NOT EXISTS "store_id" text;`;
await queryRunner.query(query);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const query = `ALTER TABLE public."user" DROP COLUMN "store_id";`;
await queryRunner.query(query);
}
}
The migration is created using the @Migration
decorator from medusa-extender
. Notice that the migration name should end with a JavaScript timestamp based on typeorm
's conventions.
The up
method is run if the migration hasnât been run before. It will add the column store_id
to the table user
if it doesnât exist.
Youâll also need to add the relation between the Store and the User entities in src/modules/store/entities/store.entity.ts
. Replace the //TODO
with the following:
@OneToMany(() => User, (user) => user.store)
@JoinColumn({ name: 'id', referencedColumnName: 'store_id' })
members: User[];
Make sure to import the User
entity at the beginning of the file:
import { User } from '../../user/entities/user.entity';
Create the User Repository
Next, you need to override Medusaâs UserRepository
. Create the file src/modules/user/repositories/user.repository.ts
with the following content:
import { UserRepository as MedusaUserRepository } from "@medusajs/medusa/dist/repositories/user";
import { Repository as MedusaRepository, Utils } from "medusa-extender";
import { EntityRepository } from "typeorm";
import { User } from "../entities/user.entity";
@MedusaRepository({ override: MedusaUserRepository })
@EntityRepository(User)
export default class UserRepository extends Utils.repositoryMixin<User, MedusaUserRepository>(MedusaUserRepository) {
}
Create the User Service
Next, you need to override Medusaâs UserService
class. Create the file src/modules/user/services/user.service.ts
with the following content:
import { Service } from 'medusa-extender';
import { EntityManager } from 'typeorm';
import EventBusService from '@medusajs/medusa/dist/services/event-bus';
import { FindConfig } from '@medusajs/medusa/dist/types/common';
import { UserService as MedusaUserService } from '@medusajs/medusa/dist/services';
import { User } from '../entities/user.entity';
import UserRepository from '../repositories/user.repository';
import { MedusaError } from 'medusa-core-utils';
type ConstructorParams = {
manager: EntityManager;
userRepository: typeof UserRepository;
eventBusService: EventBusService;
};
@Service({ override: MedusaUserService })
export default class UserService extends MedusaUserService {
private readonly manager: EntityManager;
private readonly userRepository: typeof UserRepository;
private readonly eventBus: EventBusService;
constructor(private readonly container: ConstructorParams) {
super(container);
this.manager = container.manager;
this.userRepository = container.userRepository;
this.eventBus = container.eventBusService;
}
public async retrieve(userId: string, config?: FindConfig<User>): Promise<User> {
const userRepo = this.manager.getCustomRepository(this.userRepository);
const validatedId = this.validateId_(userId);
const query = this.buildQuery_({ id: validatedId }, config);
const user = await userRepo.findOne(query);
if (!user) {
throw new MedusaError(MedusaError.Types.NOT_FOUND, `User with id: ${userId} was not found`);
}
return user as User;
}
}
This uses the @Service
decorator from medusa-extender
to override Medusaâs UserService
. The class you create to override it will extend UserService
.
This new class overrides the retrieve
method to ensure that the user returned is the new User entity class you created earlier.
Create a User Middleware
The loggedInUser
is not available natively in Medusa. Youâll need to create a Middleware that, when a request is authenticated, registers the logged-in User within the scope.
Create the file src/modules/user/middlewares/loggedInUser.middleware.ts
with the following content:
import { MedusaAuthenticatedRequest, MedusaMiddleware, Middleware } from 'medusa-extender';
import { NextFunction, Response } from 'express';
import UserService from '../../user/services/user.service';
@Middleware({ requireAuth: true, routes: [{ method: "all", path: '*' }] })
export class LoggedInUserMiddleware implements MedusaMiddleware {
public async consume(req: MedusaAuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
if (req.user && req.user.userId) {
const userService = req.scope.resolve('userService') as UserService;
const loggedInUser = await userService.retrieve(req.user.userId, {
select: ['id', 'store_id'],
});
req.scope.register({
loggedInUser: {
resolve: () => loggedInUser,
},
});
}
next();
}
}
You can use the @Middleware
decorator from medusa-extender
to create a Middleware that runs on specific requests. This Middleware is run when the request is received from an authenticated user, and it runs for all paths (notice the use of path: '*'
) and for all types of requests (notice the use of method: "all"
).
Inside the middleware, you retrieve the current user ID from the request, then retrieve the user model and register it in the scope so that it can be accessed from services.
This approach is simplified for the purpose of this tutorial. However, it makes more sense to include this middleware in a separate
auth
module. Whether you include this middleware in theuser
module or theauth
middleware will not affect its functionality.
Create a Store Service to Handle User Insert Events
You need to ensure that when a user is created, a store is associated with it. You can do that by listening to the User-created event and creating a new store for that user. Youâll add this event handler in a StoreService
.
Create the file src/modules/store/services/store.service.ts
with the following content:
import { StoreService as MedusaStoreService } from '@medusajs/medusa/dist/services';
import { EntityManager } from 'typeorm';
import { CurrencyRepository } from '@medusajs/medusa/dist/repositories/currency';
import { Store } from '../entities/store.entity';
import { EntityEventType, Service, MedusaEventHandlerParams, OnMedusaEntityEvent } from 'medusa-extender';
import { User } from '../../user/entities/user.entity';
import EventBusService from '@medusajs/medusa/dist/services/event-bus';
import StoreRepository from '../repositories/store.repository';
interface ConstructorParams {
loggedInUser: User;
manager: EntityManager;
storeRepository: typeof StoreRepository;
currencyRepository: typeof CurrencyRepository;
eventBusService: EventBusService;
}
@Service({ override: MedusaStoreService, scope: 'SCOPED' })
export default class StoreService extends MedusaStoreService {
private readonly manager: EntityManager;
private readonly storeRepository: typeof StoreRepository;
constructor(private readonly container: ConstructorParams) {
super(container);
this.manager = container.manager;
this.storeRepository = container.storeRepository;
}
withTransaction(transactionManager: EntityManager): StoreService {
if (!transactionManager) {
return this;
}
const cloned = new StoreService({
...this.container,
manager: transactionManager,
});
cloned.transactionManager_ = transactionManager;
return cloned;
}
@OnMedusaEntityEvent.Before.Insert(User, { async: true })
public async createStoreForNewUser(
params: MedusaEventHandlerParams<User, 'Insert'>
): Promise<EntityEventType<User, 'Insert'>> {
const { event } = params;
const createdStore = await this.withTransaction(event.manager).createForUser(event.entity);
if (!!createdStore) {
event.entity.store_id = createdStore.id;
}
return event;
}
public async createForUser(user: User): Promise<Store | void> {
if (user.store_id) {
return;
}
const storeRepo = this.manager.getCustomRepository(this.storeRepository);
const store = storeRepo.create() as Store;
return storeRepo.save(store);
}
public async retrieve(relations: string[] = []) {
if (!this.container.loggedInUser) {
return super.retrieve(relations);
}
const storeRepo = this.manager.getCustomRepository(this.storeRepository);
const store = await storeRepo.findOne({
relations,
join: { alias: 'store', innerJoin: { members: 'store.members' } },
where: (qb) => {
qb.where('members.id = :memberId', { memberId: this.container.loggedInUser.id });
},
});
if (!store) {
throw new Error('Unable to find the user store');
}
return store;
}
}
@OnMedusaEntityEvent.Before.Insert
is used to add a listener to an insert event on an entity, which in this case is the User
entity. Inside the listener, you create the user using the createForUser
method. This method just uses the StoreRepository
to create a store.
You also add a helper event retrieve
to retrieve the store that belongs to the currently logged-in user.
Notice the use of scope: 'SCOPED'
in the @Service
decorator. This will allow you to access the logged in user you registered earlier in the scope.
Youâll need to import this new class into the StoreModule
. In src/modules/store/store.module.ts
add the following import at the beginning:
import StoreService from './services/store.service';
Then, add the StoreService
to the imports
array passed to @Module
:
imports: [Store, StoreRepository, StoreService],
Create a User Subscriber
For the event listener to work, you need to first emit this event in a subscriber. The event will be emitted before a User
is inserted. Create the file src/modules/user/subscribers/user.subscriber.ts
with the following content:
import { Connection, EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm';
import { eventEmitter, Utils as MedusaUtils, OnMedusaEntityEvent } from 'medusa-extender';
import { User } from '../entities/user.entity';
@EventSubscriber()
export default class UserSubscriber implements EntitySubscriberInterface<User> {
static attachTo(connection: Connection): void {
MedusaUtils.attachOrReplaceEntitySubscriber(connection, UserSubscriber);
}
public listenTo(): typeof User {
return User;
}
public async beforeInsert(event: InsertEvent<User>): Promise<void> {
return await eventEmitter.emitAsync(OnMedusaEntityEvent.Before.InsertEvent(User), {
event,
transactionalEntityManager: event.manager,
});
}
}
This will create a subscriber using the EventSubscriber
decorator from typeorm
. Then, before a user is inserted the OnMedusaEntityEvent.Before.InsertEvent
event from medusa-extender
is emitted, which will trigger creating the store.
To register the subscriber, you need to create a middleware that registers it. Create the file src/modules/user/middlewares/userSubscriber.middleware.ts
with the following content:
import {
MEDUSA_RESOLVER_KEYS,
MedusaAuthenticatedRequest,
MedusaMiddleware,
Utils as MedusaUtils,
Middleware
} from 'medusa-extender';
import { NextFunction, Response } from 'express';
import { Connection } from 'typeorm';
import UserSubscriber from '../subscribers/user.subscriber';
@Middleware({ requireAuth: false, routes: [{ method: "post", path: '/admin/users' }] })
export class AttachUserSubscriberMiddleware implements MedusaMiddleware {
public async consume(req: MedusaAuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
const { connection } = req.scope.resolve(MEDUSA_RESOLVER_KEYS.manager) as { connection: Connection };
MedusaUtils.attachOrReplaceEntitySubscriber(connection, UserSubscriber);
return next();
}
}
This will register the subscriber when a POST
request is sent to /admin/users
, which creates a new user.
Create a User Router
The last customization left is an optional one. By default, Medusaâs create user endpoint requires you to be authenticated as an admin. In a marketplace use case, you might want users to register on their own and create their own stores. If this is not the case for you, you can skip creating the following class.
Medusa Extender allows you to also override routes in Medusa. In this case, youâll be adding the /admin/create-user
route to accept non-authenticated requests.
Create the file src/modules/user/routers/user.router.ts
and add the following content:
import { Router } from 'medusa-extender';
import createUserHandler from '@medusajs/medusa/dist/api/routes/admin/users/create-user';
import wrapHandler from '@medusajs/medusa/dist/api/middlewares/await-middleware';
@Router({
routes: [
{
requiredAuth: false,
path: '/admin/create-user',
method: 'post',
handlers: [wrapHandler(createUserHandler)],
},
],
})
export class UserRouter {
}
You use the @Router
decorator from medusa-extender
to create a router. This router will accept a routes
array which will either be added or override existing routes in your Medusa server. In this case, you override the /admin/create-user
route and set requiredAuth
to false.
To make sure that the AttachUserSubscriberMiddleware
also runs for this new route (so that the before insert user event handlers run for this new route), make sure to add a new entry to the routes
array:
@Middleware({ requireAuth: false, routes: [{ method: "post", path: '/admin/users' }, { method: "post", path: '/admin/create-user' }] })
Create a User Module
Youâve added all the customizations necessary to associate a user with their own store. Now, you can create the User module using these files.
Create the file src/modules/user/user.module.ts
with the following content:
import { AttachUserSubscriberMiddleware } from './middlewares/userSubscriber.middleware';
import { LoggedInUserMiddleware } from "./middlewares/loggedInUser.middleware";
import { Module } from 'medusa-extender';
import { User } from './entities/user.entity';
import UserRepository from './repositories/user.repository';
import { UserRouter } from "./routers/user.router";
import UserService from './services/user.service';
import addStoreIdToUser1644946220401 from './user.migration';
@Module({
imports: [
User,
UserService,
UserRepository,
addStoreIdToUser1644946220401,
UserRouter,
LoggedInUserMiddleware,
AttachUserSubscriberMiddleware
]
})
export class UserModule {}
If you didnât create the
UserRouter
in the previous step then make sure to remove it from theimports
array.
The last thing left is to import this Module. In src/main.ts
import UserModule
at the beginning of the file:
import { UserModule } from './modules/user/user.module';
Then, add the UserModule
in the array passed as a parameter to Medusa.load
:
await new Medusa(__dirname + '/../', expressInstance).load([
UserModule,
StoreModule
]);
Test it Out
You are now ready to test out this customization! In your terminal, run your Medusa server:
npm start
Or using Medusaâs CLI:
medusa develop
After your run your server, you need to use a tool like Postman to easily send requests to your server.
If you didnât add the UserRouter
, you first need to log in as an admin to be able to add users. You can do that by sending a POST
request to localhost:9000/admin/auth
. In the body, you should include the email and password. If youâre using a fresh Medusa install you can use the following credentials:
{
"email": "admin@medusa-test.com",
"password": "supersecret"
}
Following this request, you can send authenticated requests to the Admin.
Send a POST
request to [localhost:9000/admin/users](http://localhost:9000/admin/users)
to create a new user. In the body, you need to pass the email and password of the new user:
{
"email": "example@gmail.com",
"password": "supersecret"
}
The request will return a user object with the details of the new user:
Notice how thereâs a store_id
field now. If you try to create a couple of users, youâll see that the store_id
will be different each time.
Customize the Products Entity
Similar to how you just customized the User
entity, you need to customize the Product
entity to also hold the store_id
with the relationship as well. Youâll then customize the ProductService
as well as other classes to make sure that, when a product is created, the store ID of the user creating it is attached to it. Youâll also make sure that when the list of products is fetched, only the products that belong to the current userâs store are returned.
Create a Product Entity
Create the file src/modules/product/entities/product.entity.ts
with the following content:
import { Product as MedusaProduct } from '@medusajs/medusa/dist';
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import { Entity as MedusaEntity } from 'medusa-extender';
import { Store } from '../../store/entities/store.entity';
@MedusaEntity({ override: MedusaProduct })
@Entity()
export class Product extends MedusaProduct {
@Index()
@Column({ nullable: false })
store_id: string;
@ManyToOne(() => Store, (store) => store.members)
@JoinColumn({ name: 'store_id', referencedColumnName: 'id' })
store: Store;
}
This will override Medusaâs Product
entity to add the store_id
field and relation to the Store
entity.
You need to also reflect this relation in the Store
entity, so, in src/modules/store/entities/store.entity.ts
add the following code below the relation with the User
entity you previously added:
@OneToMany(() => Product, (product) => product.store)
@JoinColumn({ name: 'id', referencedColumnName: 'store_id' })
products: Product[];
Make sure to import the Product
entity at the beginning of the file:
import { Product } from '../../product/entities/product.entity';
Create a Product Migration
Next, create the file src/modules/product/product.migration.ts
with the following content:
import { MigrationInterface, QueryRunner } from 'typeorm';
import { Migration } from 'medusa-extender';
@Migration()
export default class addStoreIdToProduct1645034402086 implements MigrationInterface {
name = 'addStoreIdToProduct1645034402086';
public async up(queryRunner: QueryRunner): Promise<void> {
const query = `ALTER TABLE public."product" ADD COLUMN IF NOT EXISTS "store_id" text;`;
await queryRunner.query(query);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const query = `ALTER TABLE public."product" DROP COLUMN "store_id";`;
await queryRunner.query(query);
}
}
This will add a migration that will add the store_id
column to the product
table.
Create a Product Repository
Next, create the file src/modules/repositories/product.repository.ts
with the following content:
import { Repository as MedusaRepository, Utils } from "medusa-extender";
import { EntityRepository } from "typeorm";
import { ProductRepository as MedusaProductRepository } from "@medusajs/medusa/dist/repositories/product";
import { Product } from '../entities/product.entity';
@MedusaRepository({ override: MedusaProductRepository })
@EntityRepository(Product)
export default class ProductRepository extends Utils.repositoryMixin<Product, MedusaProductRepository>(MedusaProductRepository) {
}
This will override Medusaâs ProductRepository
to return your new Product
entity.
Create a Product Service
Now, youâll add the customization to ensure that only the products that belong to the currently logged-in user are returned when a request is sent.
Since you created the LoggedInUserMiddleware
earlier, you can have access to the logged-in user from any service through the container
object passed to the constructor of the service.
Create the file src/modules/product/services/product.service.ts
with the following content:
import { EntityEventType, MedusaEventHandlerParams, OnMedusaEntityEvent, Service } from 'medusa-extender';
import { EntityManager } from "typeorm";
import { ProductService as MedusaProductService } from '@medusajs/medusa/dist/services';
import { Product } from '../entities/product.entity';
import { User } from '../../user/entities/user.entity';
import UserService from '../../user/services/user.service';
type ConstructorParams = {
manager: any;
loggedInUser: User;
productRepository: any;
productVariantRepository: any;
productOptionRepository: any;
eventBusService: any;
productVariantService: any;
productCollectionService: any;
productTypeRepository: any;
productTagRepository: any;
imageRepository: any;
searchService: any;
userService: UserService;
}
@Service({ scope: 'SCOPED', override: MedusaProductService })
export class ProductService extends MedusaProductService {
readonly #manager: EntityManager;
constructor(private readonly container: ConstructorParams) {
super(container);
this.#manager = container.manager;
}
prepareListQuery_(selector: object, config: object): object {
const loggedInUser = this.container.loggedInUser
if (loggedInUser) {
selector['store_id'] = loggedInUser.store_id
}
return super.prepareListQuery_(selector, config);
}
}
This will override the prepareListQuery
method in Medusaâs ProductService
, which this new class extends, to get the logged-in user. Then, if the user is retrieved successfully the key store_id
is added to the selector
object to filter the products by the userâs store_id
.
Create a Product Module
Thatâs all the customization youâll do for now. You just need to import all these files into a Product module.
Create src/modules/product/product.module.ts
with the following content:
import { Module } from 'medusa-extender';
import { Product } from './entities/product.entity';
import ProductRepository from './repositories/product.repository';
import { ProductService } from './services/product.service';
import addStoreIdToProduct1645034402086 from './product.migration';
@Module({
imports: [
Product,
ProductRepository,
ProductService,
addStoreIdToProduct1645034402086,
]
})
export class ProductModule {}
Finally, import the ProductModule
at the beginning of src/main.ts
:
import { ProductModule } from './modules/product/product.module';
And add the ProductModule
to the array passed to load
along with UserModule
:
await new Medusa(__dirname + '/../', expressInstance).load([
UserModule,
ProductModule,
StoreModule
]);
Test it Out
You can go ahead and test it out now. Run the server if it isnât running already and log in with the user you created earlier by sending the credentials to localhost:9000/admin/auth
.
After that, send a GET
request to localhost:9000/admin/products
. Youâll receive an empty array of products as the current user does not have any products yet.
Create a Product Subscriber
Youâll now add the necessary customization to attach a store ID to a newly created product.
To listen to the product created event, create the file src/modules/product/subscribers/product.subscriber.ts
with the following content:
import { Connection, EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm';
import { OnMedusaEntityEvent, Utils, eventEmitter } from 'medusa-extender';
import { Product } from '../entities/product.entity';
@EventSubscriber()
export default class ProductSubscriber implements EntitySubscriberInterface<Product> {
static attachTo(connection: Connection): void {
Utils.attachOrReplaceEntitySubscriber(connection, ProductSubscriber);
}
public listenTo(): typeof Product {
return Product;
}
public async beforeInsert(event: InsertEvent<Product>): Promise<void> {
return await eventEmitter.emitAsync(OnMedusaEntityEvent.Before.InsertEvent(Product), {
event,
transactionalEntityManager: event.manager,
});
}
}
Then, you need to register this Subscriber using Middleware. Create the file src/modules/product/middlewares/product.middleware.ts
with the following content:
import {
MEDUSA_RESOLVER_KEYS,
MedusaAuthenticatedRequest,
MedusaMiddleware,
Utils as MedusaUtils,
Middleware
} from 'medusa-extender';
import { NextFunction, Request, Response } from 'express';
import { Connection } from 'typeorm';
import ProductSubscriber from '../subscribers/product.subscriber';
@Middleware({ requireAuth: true, routes: [{ method: 'post', path: '/admin/products' }] })
export default class AttachProductSubscribersMiddleware implements MedusaMiddleware {
public consume(req: MedusaAuthenticatedRequest | Request, res: Response, next: NextFunction): void | Promise<void> {
const { connection } = req.scope.resolve(MEDUSA_RESOLVER_KEYS.manager) as { connection: Connection };
MedusaUtils.attachOrReplaceEntitySubscriber(connection, ProductSubscriber);
return next();
}
}
This will register the subscriber when a POST
request is sent to /admin/products
, which creates a new product.
Add Event Listener in Product Service
Next, in src/modules/product/services/product.service.ts
add the following inside the class:
@OnMedusaEntityEvent.Before.Insert(Product, { async: true })
public async attachStoreToProduct(
params: MedusaEventHandlerParams<Product, 'Insert'>
): Promise<EntityEventType<Product, 'Insert'>> {
const { event } = params;
const loggedInUser = this.container.loggedInUser;
event.entity.store_id = loggedInUser.store_id;
return event;
}
This will listen to the Insert event using the @OnMedusaEntityEvent
decorator from medusa-extender
. It will then use the logged-in user and attach the userâs store_id
to the newly created product.
Add Middleware to Product Module
Finally, make sure to import the new middleware at the beginning of src/modules/product/product.module.ts
:
import AttachProductSubscribersMiddleware from './middlewares/product.middleware';
Then, add it in the imports
array passed to @Module
:
imports: [
Product,
ProductRepository,
ProductService,
addStoreIdToProduct1645034402086,
AttachProductSubscribersMiddleware
]
Youâre ready to add products into a store now! Run the server if itâs not running and make sure youâre logged in with the user you created earlier. Then, send a POST
request to [localhost:9000/admin/products](http://localhost:9000/admin/products)
with the following body:
{
"title": "my product",
"options": []
}
This is the minimum structure of a product. You can rename the title to anything you want.
After you send the request, you should receive a Product object where you can see the store_id
is set to the same store_id
of the user youâre logged in with.
Now, try sending a GET
request to [localhost:9000/admin/products](http://localhost:9000/admin/products)
as you did earlier. Instead of an empty array, youâll see the product you just added.
Testing it Out Using Medusaâs Admin
If you also have a Medusa Admin instance installed, you can also test this out. Log in with the user you created earlier and youâll see that you can only see the product they added.
Conclusion
In this tutorial, you learned the first steps of creating a Marketplace using Medusa and Medusa Extender! In later points, youâll learn about how you can add settings, manage orders, and more!
Be sure to support Medusa Extender and check the repository out for more details!
Should you have any issues or questions related to Medusa, then feel free to reach out to the Medusa team via Discord. You can also contact Adrien @adrien2p
for more details or help regarding Medusa Extender.
Top comments (14)
Hi, I cloned your repo Medusa Marketplace Tutorial, but facing issues in admin panel, like while adding products it throws "Price should be an array". While fetching team, no users are shown.
Please help me out.
Hello, unfortunately this tutorial and its code base is out of date with the current Medusa version. For now, it serves for the purpose of saying that it can be done using Medusa and Medusa Extender, but you'll need to make adjustment to the code of the tutorial to make it work.
One more thing, I got it working, finally. But I have minio configured but it still throws AWS InvalidAccessKey error, on local, can you tell me where its configured. I checked ".env", "medusa.config.js" and dockerfile
Did you follow along with our MinIO documentation? If it helps, we can continue this conversation either over emails or discord.
đĽ˛
I have put together a nx workspace for running the marketplace. Might be helpful for anyone who followed the tutorial.
github.com/Keith-Hon/medusa-market...
If you just want to run the original medusa under 1 minute, you could use the repo below instead.
github.com/Keith-Hon/medusa-nx
After choosing between shuup, saleor, oscar I wrote my own opensource miltivendor marketplace in python (wpshop dot at).
The frontend is in vuejs/nuxt.
Is there any advantage here in medusa over python/vuejs?
Do you have a repo I can checkout? I'm interested in your project as I use the same technology stack.
we have the TOF2 open repository, which is the library we use for translations in e-commerce. github.com/wP-soft-GmbH/django-tof
The wpshop repository is not open yet. Write me in private, maybe we can find other ways to show it to you.
I looked at the Api but there is no endpoint for user registration.
How should I approach this?
Add a row in the customers table?
Weâre working on making sure the endpoints in the API reference are updated but in the mean time please refer to the endpoint mentioned in the tutorial
i wish there was an update on this plug in :(
Hi, great tutorial! When does the next parts come out?