Medusa v2 Custom Modules
Before writing code
Fetch live docs:
- Fetch
https://docs.medusajs.com/learn/fundamentals/modulesfor module overview - Web-search
site:docs.medusajs.com DML data model referencefor model property types - Web-search
site:docs.medusajs.com MedusaService methodsfor base service API - Web-search
site:docs.medusajs.com module linksfor linking modules together - Web-search
site:docs.medusajs.com module container registrationfor dependency injection
Module Concept
A module is a self-contained package of functionality in Medusa v2:
- Encapsulates data models, services, and business logic
- Registered in
medusa-config.tsunder themodulesarray - Accessed from workflows, API routes, and subscribers via the dependency container
- Modules communicate through links, not direct imports
Module Directory Structure
| File | Purpose |
|---|---|
src/modules/my-module/models/my-entity.ts | DML data model |
src/modules/my-module/service.ts | Service extending MedusaService |
src/modules/my-module/loaders/seed.ts | Optional loader (runs on startup) |
src/modules/my-module/migrations/ | Auto-generated by CLI |
src/modules/my-module/index.ts | Module definition export |
DML Data Models
Property Types
| DML Type | Database Column | Notes |
|---|---|---|
model.id() | Primary key UUID | Auto-generated |
model.text() | text / varchar | String fields |
model.number() | integer | Numeric fields |
model.bigNumber() | numeric | Precise decimals (prices) |
model.boolean() | boolean | True/false |
model.dateTime() | timestamptz | Date/time values |
model.json() | jsonb | Arbitrary JSON |
model.enum() | enum | Constrained values |
model.array() | array | Array of primitives |
Relationship Types
| Relationship | DML Method | Description |
|---|---|---|
| Has One | model.hasOne() | One-to-one owned |
| Has Many | model.hasMany() | One-to-many owned |
| Belongs To | model.belongsTo() | Inverse of hasOne/hasMany |
| Many to Many | model.manyToMany() | Junction table auto-created |
Model Definition Skeleton
// src/modules/my-module/models/my-entity.ts
// Fetch live docs for current DML model API
import { model } from "@medusajs/framework/utils"
const MyEntity = model.define("my_entity", {
id: model.id(), name: model.text(),
// Fetch live docs for property options and relationships
})
Service Pattern
Every module exposes a service that extends MedusaService:
// src/modules/my-module/service.ts
// Fetch live docs for MedusaService generic signature
import { MedusaService } from "@medusajs/framework/utils"
import MyEntity from "./models/my-entity"
class MyModuleService extends MedusaService({ MyEntity }) {}
export default MyModuleService // Add custom methods as needed
Inherited MedusaService Methods
| Method Pattern | Purpose |
|---|---|
create<Entity>(data) | Create one or many records |
update<Entity>(data) | Update records by selector or ID |
delete<Entity>(ids) | Soft-delete records |
retrieve<Entity>(id, config) | Retrieve single record by ID |
list<Entity>(filters, config) | List with filters, pagination, relations |
softRestore<Entity>(ids) | Restore soft-deleted records |
The method names are auto-generated from the model name (e.g., createMyEntity, listMyEntities).
Module Definition Export
// src/modules/my-module/index.ts
// Fetch live docs for Module definition shape
import { Module } from "@medusajs/framework/utils"
import MyModuleService from "./service"
export const MY_MODULE = Module("my-module", {
service: MyModuleService,
})
Registration in medusa-config.ts
// Inside modules array
// Fetch live docs for module registration options
{ resolve: "./src/modules/my-module" }
Module Links
Links connect data models across different modules without coupling:
| Link Type | Purpose |
|---|---|
| One-to-one link | Connect a custom entity to a core entity (e.g., product) |
| One-to-many link | One core entity linked to many custom entities |
| List link | Bidirectional many-to-many between modules |
Link Definition Skeleton
// src/links/product-my-entity.ts
// Fetch live docs for defineLink API
import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"
import { MY_MODULE } from "../modules/my-module"
Links are queried using the query utility from the Medusa container, not through direct service calls.
Loaders
Loaders run when the module initializes (server startup):
- Seed default data
- Register event listeners
- Validate configuration
- Set up external connections
Container Resolution
Modules register their service in the dependency injection container:
- Resolved by module key:
container.resolve("my-module") - Available in workflows, API routes, subscribers, and other services
- Never import a module's service directly -- always resolve from the container
Best Practices
- One module per bounded context -- keep modules focused and cohesive
- Use DML models exclusively -- never write raw SQL or use Mikro-ORM directly
- Extend
MedusaServicefor CRUD -- only add custom methods for non-standard logic - Use module links to connect to core entities -- never modify core module tables
- Run
npx medusa db:generateafter every model change to create migrations - Export a constant for your module key (e.g.,
MY_MODULE) and reuse it everywhere
Common Patterns
| Pattern | When to Use |
|---|---|
| Custom module + link to Product | Adding metadata/features to products |
| Custom module + link to Order | Order-related extensions (loyalty, tracking) |
| Standalone module | Independent domain (CMS, analytics, reviews) |
| Module with loader | Pre-seeding data or external service setup |
Fetch the Medusa module documentation for exact DML property options, MedusaService generics, and link definition patterns before implementing.