Skip to main content
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

1

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
}
2

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>
3

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

Build docs developers (and LLMs) love