Documentation Index Fetch the complete documentation index at: https://mintlify.com/medusajs/medusa/llms.txt
Use this file to discover all available pages before exploring further.
Services in Medusa encapsulate business logic and data access for specific domains. They extend the MedusaService base class and use decorators for transaction management and event handling.
What is a Service?
A Medusa service:
Extends MedusaService to get automatic CRUD methods
Manages data access for one or more entities
Uses decorators for cross-cutting concerns (transactions, events)
Can be injected into workflows, API routes, and other services
Provides type-safe DTOs for inputs and outputs
Creating a Service
Define Your Entity
First, create your data model: src/modules/brand/models/brand.ts
import { Entity , Property , OneToMany } 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
@ Property ({ default: true })
is_active : boolean = true
}
Create the Service Class
Extend MedusaService with your entity configuration: 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"
import { BrandDTO , CreateBrandDTO } from "../types"
type InjectedDependencies = {
baseRepository : DAL . RepositoryService
brandService : ModulesSdkTypes . IMedusaInternalService < Brand >
}
export default class BrandModuleService extends MedusaService <{
Brand : {
dto : BrandDTO
}
}>({
Brand ,
}) {
protected baseRepository_ : DAL . RepositoryService
protected brandService_ : ModulesSdkTypes . IMedusaInternalService < Brand >
constructor (
{ baseRepository , brandService } : InjectedDependencies ,
protected readonly moduleDeclaration : InternalModuleDeclaration
) {
super ( ... arguments )
this . baseRepository_ = baseRepository
this . brandService_ = brandService
}
}
The service automatically gets these methods:
createBrands(data: CreateBrandDTO[]): Promise<BrandDTO[]>
updateBrands(data: UpdateBrandDTO[]): Promise<BrandDTO[]>
listBrands(filters?, config?): Promise<BrandDTO[]>
listAndCountBrands(filters?, config?): Promise<[BrandDTO[], number]>
retrieveBrand(id, config?): Promise<BrandDTO>
deleteBrands(ids: string[]): Promise<void>
softDeleteBrands(ids: string[]): Promise<void>
restoreBrands(ids: string[]): Promise<void>
Add Custom Methods
Implement custom business logic: export default class BrandModuleService extends MedusaService <{
Brand : { dto : BrandDTO }
}>({
Brand ,
}) {
// ... constructor ...
@ InjectManager ()
async findByName (
name : string ,
@ MedusaContext () sharedContext : Context = {}
) : Promise < BrandDTO | null > {
const [ brand ] = await this . brandService_ . list (
{ name },
{ take: 1 },
sharedContext
)
return brand ? await this . baseRepository_ . serialize ( brand ) : null
}
@ InjectManager ()
async listActiveBrands (
@ MedusaContext () sharedContext : Context = {}
) : Promise < BrandDTO []> {
const brands = await this . brandService_ . list (
{ is_active: true },
{},
sharedContext
)
return await this . baseRepository_ . serialize ( brands )
}
}
Service Decorators
Decorators provide cross-cutting functionality for your service methods.
@InjectManager
Injects the entity manager for database operations. Use on public methods :
@ InjectManager ()
async createBrand (
data : CreateBrandDTO ,
@ MedusaContext () sharedContext : Context = {}
): Promise < BrandDTO > {
const [brand] = await this.createBrands([data], sharedContext)
return brand
}
@InjectTransactionManager
Injects a transactional entity manager. Use on protected methods that modify data:
@ InjectTransactionManager ()
protected async updateBrandStatus_ (
id : string ,
isActive : boolean ,
@ MedusaContext () sharedContext : Context = {}
): Promise < Brand > {
const brand = await this . brandService_ . retrieve ( id , {}, sharedContext )
brand. is_active = isActive
await this.brandService_.update( [brand], sharedContext)
return brand
}
@MedusaContext
Marks the context parameter for transaction and manager injection:
async listBrands (
filters : FilterableBrandProps = {},
config : FindConfig < BrandDTO > = {},
@ MedusaContext () sharedContext : Context = {}
): Promise < BrandDTO [] > {
return await this.brandService_.list( filters , config , sharedContext)
}
@EmitEvents
Automatically emits events after method execution:
import { EmitEvents } from "@medusajs/framework/utils"
@ InjectManager ()
@ EmitEvents ()
async activateBrand (
id : string ,
@ MedusaContext () sharedContext : Context = {}
): Promise < BrandDTO > {
const brand = await this . updateBrandStatus_ ( id , true , sharedContext )
// Event automatically emitted
this.aggregatedEvents({
action : "activated" ,
object : "brand" ,
data : { id },
context : sharedContext ,
})
return await this.baseRepository_.serialize(brand)
}
Complete Service Example
Here’s a full service implementation with custom logic:
src/modules/brand/services/brand-module-service.ts
import {
Context ,
DAL ,
FilterableProductProps ,
FindConfig ,
InternalModuleDeclaration ,
} from "@medusajs/framework/types"
import {
EmitEvents ,
InjectManager ,
InjectTransactionManager ,
MedusaContext ,
MedusaError ,
MedusaService ,
ModulesSdkTypes ,
} from "@medusajs/framework/utils"
import { Brand } from "../models"
import { BrandDTO , CreateBrandDTO , UpdateBrandDTO } from "../types"
type InjectedDependencies = {
baseRepository : DAL . RepositoryService
brandService : ModulesSdkTypes . IMedusaInternalService < Brand >
}
export default class BrandModuleService extends MedusaService <{
Brand : {
dto : BrandDTO
}
}>({
Brand ,
}) {
protected baseRepository_ : DAL . RepositoryService
protected brandService_ : ModulesSdkTypes . IMedusaInternalService < Brand >
constructor (
{ baseRepository , brandService } : InjectedDependencies ,
protected readonly moduleDeclaration : InternalModuleDeclaration
) {
super ( ... arguments )
this . baseRepository_ = baseRepository
this . brandService_ = brandService
}
// Custom retrieval methods
@ InjectManager ()
async findByName (
name : string ,
@ MedusaContext () sharedContext : Context = {}
) : Promise < BrandDTO | null > {
const [ brand ] = await this . brandService_ . list (
{ name },
{ take: 1 },
sharedContext
)
return brand ? await this . baseRepository_ . serialize ( brand ) : null
}
@ InjectManager ()
async listActiveBrands (
filters : FilterableProductProps = {},
config : FindConfig < BrandDTO > = {},
@ MedusaContext () sharedContext : Context = {}
) : Promise < BrandDTO []> {
const brands = await this . brandService_ . list (
{ ... filters , is_active: true },
config ,
sharedContext
)
return await this . baseRepository_ . serialize ( brands )
}
// Custom business logic
@ InjectManager ()
@ EmitEvents ()
async activateBrand (
id : string ,
@ MedusaContext () sharedContext : Context = {}
) : Promise < BrandDTO > {
const brand = await this . updateBrandStatus_ ( id , true , sharedContext )
this . aggregatedEvents ({
action: "activated" ,
object: "brand" ,
data: { id },
context: sharedContext ,
})
return await this . baseRepository_ . serialize ( brand )
}
@ InjectManager ()
@ EmitEvents ()
async deactivateBrand (
id : string ,
@ MedusaContext () sharedContext : Context = {}
) : Promise < BrandDTO > {
const brand = await this . updateBrandStatus_ ( id , false , sharedContext )
this . aggregatedEvents ({
action: "deactivated" ,
object: "brand" ,
data: { id },
context: sharedContext ,
})
return await this . baseRepository_ . serialize ( brand )
}
// Protected helper methods
@ InjectTransactionManager ()
protected async updateBrandStatus_ (
id : string ,
isActive : boolean ,
@ MedusaContext () sharedContext : Context = {}
) : Promise < Brand > {
const brand = await this . brandService_ . retrieve ( id , {}, sharedContext )
if ( ! brand ) {
throw new MedusaError (
MedusaError . Types . NOT_FOUND ,
`Brand with id: ${ id } was not found`
)
}
brand . is_active = isActive
const [ updated ] = await this . brandService_ . update ([ brand ], sharedContext )
return updated
}
}
Working with Relationships
Define relationships in your entities:
src/modules/brand/models/brand.ts
import { Entity , Property , OneToMany , Collection } from "@mikro-orm/core"
import { BaseEntity } from "@medusajs/framework/utils"
@ Entity ()
export class Brand extends BaseEntity {
@ Property ()
name : string
@ OneToMany (() => Product , ( product ) => product . brand )
products = new Collection < Product >( this )
}
src/modules/brand/models/product.ts
import { Entity , ManyToOne } from "@mikro-orm/core"
@ Entity ()
export class Product extends BaseEntity {
@ ManyToOne (() => Brand )
brand : Brand
}
Query with relationships:
@ InjectManager ()
async getBrandWithProducts (
id : string ,
@ MedusaContext () sharedContext : Context = {}
): Promise < BrandDTO > {
const brand = await this . brandService_ . retrieve (
id ,
{
relations: [ "products" ],
},
sharedContext
)
return await this.baseRepository_.serialize(brand)
}
Error Handling
Use MedusaError for consistent error handling:
import { MedusaError } from "@medusajs/framework/utils"
@ InjectManager ()
async retrieveBrandByName (
name : string ,
@ MedusaContext () sharedContext : Context = {}
): Promise < BrandDTO > {
const brand = await this . findByName ( name , sharedContext )
if (! brand ) {
throw new MedusaError (
MedusaError . Types . NOT_FOUND ,
`Brand with name " ${ name } " was not found`
)
}
return brand
}
Common error types:
MedusaError.Types.NOT_FOUND - Resource not found
MedusaError.Types.INVALID_DATA - Invalid input data
MedusaError.Types.NOT_ALLOWED - Operation not permitted
MedusaError.Types.DUPLICATE_ERROR - Duplicate entry
Emitting Events
Use aggregatedEvents to emit domain events:
this . aggregatedEvents ({
action: "created" ,
object: "brand" ,
data: { id: brand . id },
context: sharedContext ,
})
The framework automatically formats and emits these events through the event bus.
Best Practices
Use @InjectManager() for public methods
Use @InjectTransactionManager() for protected methods that modify data
Always include @MedusaContext() parameter for database operations
Serialize entities before returning them: this.baseRepository_.serialize()
Use the auto-generated CRUD methods instead of reimplementing them
Throw MedusaError for error handling
Emit events using aggregatedEvents() for important state changes
Keep business logic in services, not in API routes
Use protected methods (ending with _) for internal operations
Next Steps
Create Workflows Compose services into workflows
Create Custom Modules Package services into modules