DEV Community

Cover image for Open source ecommerce platform for multi-vendor marketplaces
Shahed Nasser for Medusa

Posted on • Updated on • Originally published at medusajs.com

Open source ecommerce platform for multi-vendor marketplaces

🚨 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:

  1. A Medusa server instance was installed. You can follow along with our easy quickstart guide to learn how you can do that.
  2. PostgreSQL installed and your Medusa server connected to it.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

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",
  },
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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) {
}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

Then, add the StoreModule in the array passed as a parameter to Medusa.load:

await new Medusa(__dirname + '/../', expressInstance).load([
    StoreModule
]);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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[];
Enter fullscreen mode Exit fullscreen mode

Make sure to import the User entity at the beginning of the file:

import { User } from '../../user/entities/user.entity';
Enter fullscreen mode Exit fullscreen mode

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) {
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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 the user module or the auth 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

@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';
Enter fullscreen mode Exit fullscreen mode

Then, add the StoreService to the imports array passed to @Module :

imports: [Store, StoreRepository, StoreService],
Enter fullscreen mode Exit fullscreen mode

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,
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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 {
}
Enter fullscreen mode Exit fullscreen mode

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' }] })
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

If you didn’t create the UserRouter in the previous step then make sure to remove it from the imports 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';
Enter fullscreen mode Exit fullscreen mode

Then, add the UserModule in the array passed as a parameter to Medusa.load:

await new Medusa(__dirname + '/../', expressInstance).load([
        UserModule,
        StoreModule
]);
Enter fullscreen mode Exit fullscreen mode

Test it Out

You are now ready to test out this customization! In your terminal, run your Medusa server:

npm start
Enter fullscreen mode Exit fullscreen mode

Or using Medusa’s CLI:

medusa develop
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

The request will return a user object with the details of the new user:

Create User Result

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;
}
Enter fullscreen mode Exit fullscreen mode

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[];
Enter fullscreen mode Exit fullscreen mode

Make sure to import the Product entity at the beginning of the file:

import { Product } from '../../product/entities/product.entity';
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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) {
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

Finally, import the ProductModule at the beginning of src/main.ts:

import { ProductModule } from './modules/product/product.module';
Enter fullscreen mode Exit fullscreen mode

And add the ProductModule to the array passed to load along with UserModule:

await new Medusa(__dirname + '/../', expressInstance).load([
    UserModule,
    ProductModule,
    StoreModule
]);
Enter fullscreen mode Exit fullscreen mode

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.

Result of Get Products

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,
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

Then, add it in the imports array passed to @Module:

imports: [
  Product,
  ProductRepository,
  ProductService,
  addStoreIdToProduct1645034402086,
  AttachProductSubscribersMiddleware
]
Enter fullscreen mode Exit fullscreen mode

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": []
}
Enter fullscreen mode Exit fullscreen mode

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.

Add Product Request Result

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.

Retrieve Products

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.

Admin Dashboard

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)

Collapse
 
nitish1111 profile image
Nitish1111

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.

Collapse
 
shahednasser profile image
Shahed Nasser

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.

Collapse
 
nitish1111 profile image
Nitish1111

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

Thread Thread
 
shahednasser profile image
Shahed Nasser

Did you follow along with our MinIO documentation? If it helps, we can continue this conversation either over emails or discord.

Collapse
 
nitish1111 profile image
Nitish1111

🥲

Thread Thread
 
shahednasser profile image
Shahed Nasser

Collapse
 
keithhon profile image
Keith Hon • Edited

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

Collapse
 
danilovmy profile image
Maxim Danilov

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?

Collapse
 
sebastianmacias profile image
Sebastian Macias

Do you have a repo I can checkout? I'm interested in your project as I use the same technology stack.

Collapse
 
danilovmy profile image
Maxim Danilov

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.

Collapse
 
developmentvargamarcel profile image
development-vargamarcel • Edited

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?

Collapse
 
shahednasser profile image
Shahed Nasser

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

Collapse
 
willzxxz profile image
Guilherme de Deus

i wish there was an update on this plug in :(

Collapse
 
rhuanbarros profile image
Rhuan Barros

Hi, great tutorial! When does the next parts come out?