Skip to main content
Custom modules allow you to extend Medusa with domain-specific business logic while maintaining the framework’s architectural patterns. Modules are self-contained units that can manage their own data models, services, and business logic.

What is a Module?

A module in Medusa is a package that:
  • Encapsulates specific domain logic (e.g., product management, inventory)
  • Manages its own data models and database schema
  • Exposes a service interface for other parts of the application
  • Can be easily replaced or extended

Creating a Basic Module

1

Define the Module Entry Point

Create an index.ts file that exports your module using the Module utility:
src/modules/brand/index.ts
import { Module, Modules } from "@medusajs/framework/utils"
import { BrandModuleService } from "./services"

export const BRAND_MODULE = "brandModuleService"

export default Module(BRAND_MODULE, {
  service: BrandModuleService,
})
2

Create Data Models

Define your entities using MikroORM decorators:
src/modules/brand/models/brand.ts
import { Entity, Property } from "@mikro-orm/core"
import { BaseEntity } from "@medusajs/framework/utils"

@Entity()
export class Brand extends BaseEntity {
  @Property()
  name: string

  @Property({ nullable: true })
  description?: string

  @Property({ nullable: true })
  logo_url?: string
}
Export your models:
src/modules/brand/models/index.ts
export { Brand } from "./brand"
3

Create the Module Service

Extend MedusaService to create your module’s main service:
src/modules/brand/services/brand-module-service.ts
import { Context, DAL, InternalModuleDeclaration } from "@medusajs/framework/types"
import {
  InjectManager,
  InjectTransactionManager,
  MedusaContext,
  MedusaService,
  ModulesSdkTypes,
} from "@medusajs/framework/utils"
import { Brand } from "../models"

type InjectedDependencies = {
  baseRepository: DAL.RepositoryService
  brandService: ModulesSdkTypes.IMedusaInternalService<any>
}

export default class BrandModuleService extends MedusaService<{
  Brand: {
    dto: BrandDTO
  }
}>({
  Brand,
}) {
  protected baseRepository_: DAL.RepositoryService
  protected readonly brandService_: ModulesSdkTypes.IMedusaInternalService<Brand>

  constructor(
    { baseRepository, brandService }: InjectedDependencies,
    protected readonly moduleDeclaration: InternalModuleDeclaration
  ) {
    super(...arguments)
    this.baseRepository_ = baseRepository
    this.brandService_ = brandService
  }

  // Custom methods
  @InjectManager()
  async findByName(
    name: string,
    @MedusaContext() sharedContext: Context = {}
  ): Promise<Brand | null> {
    const [brand] = await this.brandService_.list(
      { name },
      { take: 1 },
      sharedContext
    )
    return brand || null
  }
}
The MedusaService base class automatically provides:
  • createBrands() - Create one or more brands
  • updateBrands() - Update brands
  • listBrands() - List brands with filtering
  • listAndCountBrands() - List with pagination
  • retrieveBrand() - Get a single brand by ID
  • deleteBrands() - Delete brands
  • softDeleteBrands() - Soft delete brands
  • restoreBrands() - Restore soft-deleted brands
4

Define Type Definitions

Create TypeScript interfaces for your DTOs:
src/modules/brand/types/index.ts
export interface BrandDTO {
  id: string
  name: string
  description?: string
  logo_url?: string
  created_at: Date
  updated_at: Date
}

export interface CreateBrandDTO {
  name: string
  description?: string
  logo_url?: string
}

export interface UpdateBrandDTO {
  id: string
  name?: string
  description?: string
  logo_url?: string
}
5

Register the Module

Add your module to medusa-config.ts:
medusa-config.ts
import { defineConfig } from "@medusajs/framework/utils"
import { Modules } from "@medusajs/framework/utils"

export default defineConfig({
  // ... other config
  modules: [
    {
      resolve: "./src/modules/brand",
      options: {
        // Module-specific options
      },
    },
  ],
})

Using Service Decorators

Medusa provides decorators for common service patterns:

@InjectManager

Injects the database entity manager into the method context. Use this for public methods:
@InjectManager()
async createBrand(
  data: CreateBrandDTO,
  @MedusaContext() sharedContext: Context = {}
): Promise<BrandDTO> {
  return await this.createBrands([data], sharedContext).then((r) => r[0])
}

@InjectTransactionManager

Injects a transactional entity manager. Use this for protected methods that modify data:
@InjectTransactionManager()
protected async createBrand_(
  data: CreateBrandDTO,
  @MedusaContext() sharedContext: Context = {}
): Promise<Brand> {
  const brand = this.brandService_.create(data, sharedContext)
  return brand
}

@MedusaContext

Marks a parameter as the shared context, which contains the transaction manager and metadata:
async listBrands(
  filters: FilterableBrandProps = {},
  config: FindConfig<BrandDTO> = {},
  @MedusaContext() sharedContext: Context = {}
) {
  return await this.brandService_.list(filters, config, sharedContext)
}

Module Configuration

You can define configuration options for your module:
src/modules/brand/types/index.ts
export interface BrandModuleOptions {
  defaultLogoUrl?: string
  enableBrandValidation?: boolean
}
Access configuration in your service:
const options = this.moduleDeclaration.options as BrandModuleOptions
const logoUrl = data.logo_url || options.defaultLogoUrl

Best Practices

  • Keep modules focused on a single domain
  • Use the base MedusaService methods instead of reimplementing CRUD operations
  • Apply @InjectManager() to public methods and @InjectTransactionManager() to protected methods
  • Always include @MedusaContext() parameter for database operations
  • Define clear DTO interfaces for type safety
  • Use soft deletes for data that may need to be restored

Next Steps

Create Workflows

Compose multi-step business processes

Create Services

Build internal services for module logic

Build docs developers (and LLMs) love