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

> Learn about the MedusaService pattern, including decorators like @InjectManager, @InjectTransactionManager, @MedusaContext, and @EmitEvents.

# Services

Services in Medusa encapsulate business logic and data access. They follow the **MedusaService** pattern, which provides automatic transaction management, event emission, and context injection through decorators.

## MedusaService Pattern

All module services extend `MedusaService`, which provides:

* **Automatic CRUD operations** - Generated list, retrieve, create, update, delete methods
* **Transaction management** - Decorators for database transactions
* **Event emission** - Automatic domain event publishing
* **Context injection** - Shared context across operations
* **Type safety** - Strongly typed DTOs and entities

<Note>
  The MedusaService pattern standardizes how services are built across all Medusa modules, ensuring consistency and reducing boilerplate.
</Note>

## Creating a Service

### Basic Service Structure

```typescript theme={null}
import type {
  Context,
  DAL,
  InternalModuleDeclaration,
  ModulesSdkTypes,
} from "@medusajs/framework/types"
import {
  InjectManager,
  InjectTransactionManager,
  MedusaContext,
  MedusaService,
  EmitEvents,
} from "@medusajs/framework/utils"
import { ApiKey } from "@models"
import type { ApiKeyTypes } from "@medusajs/framework/types"

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

export class ApiKeyModuleService
  extends MedusaService<{
    ApiKey: { dto: ApiKeyTypes.ApiKeyDTO }
  }>({ ApiKey })
  implements IApiKeyModuleService
{
  protected baseRepository_: DAL.RepositoryService
  protected readonly apiKeyService_: ModulesSdkTypes.IMedusaInternalService

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

  __joinerConfig(): ModuleJoinerConfig {
    return joinerConfig
  }
}
```

Source: `packages/modules/api-key/src/services/api-key-module-service.ts:44-67`

### Service Type Definitions

Define entity-to-DTO mappings in the generic:

```typescript theme={null}
export default class ProductModuleService
  extends MedusaService<{
    Product: { dto: ProductTypes.ProductDTO }
    ProductCategory: { dto: ProductTypes.ProductCategoryDTO }
    ProductCollection: { dto: ProductTypes.ProductCollectionDTO }
    ProductOption: { dto: ProductTypes.ProductOptionDTO }
    ProductVariant: { dto: ProductTypes.ProductVariantDTO }
  }>({
    Product,
    ProductCategory,
    ProductCollection,
    ProductOption,
    ProductVariant,
  })
{
  // Service implementation
}
```

Source: `packages/modules/product/src/services/product-module-service.ts:82-120`

## Service Decorators

Medusa provides four key decorators for cross-cutting concerns:

### @InjectManager

Injects an entity manager for database operations. Use on **public methods**.

```typescript theme={null}
import { InjectManager, MedusaContext } from "@medusajs/framework/utils"
import type { Context } from "@medusajs/framework/types"

export class ApiKeyModuleService extends MedusaService {
  @InjectManager()
  async retrieveApiKey(
    id: string,
    config?: FindConfig<ApiKeyDTO>,
    @MedusaContext() sharedContext: Context = {}
  ): Promise<ApiKeyDTO> {
    const apiKey = await this.apiKeyService_.retrieve(
      id,
      config,
      sharedContext
    )
    return await this.baseRepository_.serialize<ApiKeyDTO>(apiKey)
  }
}
```

<Tip>
  Always use `@InjectManager()` on public service methods that perform database operations. This ensures a proper entity manager is available.
</Tip>

### @InjectTransactionManager

Injects a transaction manager for atomic operations. Use on **protected methods**.

```typescript theme={null}
import {
  InjectManager,
  InjectTransactionManager,
  MedusaContext,
  EmitEvents,
} from "@medusajs/framework/utils"
import type { Context } from "@medusajs/framework/types"

export class ApiKeyModuleService extends MedusaService {
  @InjectManager()
  @EmitEvents()
  async deleteApiKeys(
    ids: string | string[],
    @MedusaContext() sharedContext: Context = {}
  ) {
    return await this.deleteApiKeys_(ids, sharedContext)
  }

  @InjectTransactionManager()
  protected async deleteApiKeys_(
    ids: string | string[],
    @MedusaContext() sharedContext: Context = {}
  ) {
    const apiKeyIds = Array.isArray(ids) ? ids : [ids]

    const unrevokedApiKeys = await this.apiKeyService_.list(
      {
        id: ids,
        $or: [
          { revoked_at: { $eq: null } },
          { revoked_at: { $gt: new Date() } },
        ],
      },
      { select: ["id"] },
      sharedContext
    )

    if (unrevokedApiKeys.length) {
      throw new MedusaError(
        MedusaError.Types.NOT_ALLOWED,
        `Cannot delete api keys that are not revoked`
      )
    }

    return await super.deleteApiKeys(apiKeyIds, sharedContext)
  }
}
```

Source: `packages/modules/api-key/src/services/api-key-module-service.ts:69-110`

<Warning>
  Use the convention of public methods calling protected methods with a `_` suffix. The public method has `@InjectManager` and `@EmitEvents`, while the protected method has `@InjectTransactionManager`.
</Warning>

### @MedusaContext

Injects shared context into the decorated parameter. Always use as the last parameter.

```typescript theme={null}
import { MedusaContext, InjectManager } from "@medusajs/framework/utils"
import type { Context } from "@medusajs/framework/types"

export class MyService extends MedusaService {
  @InjectManager()
  async myMethod(
    id: string,
    data: UpdateData,
    @MedusaContext() sharedContext: Context = {}
  ) {
    // sharedContext contains:
    // - transactionManager (if in transaction)
    // - manager (entity manager)
    // - isolationLevel
    // - enableNestedTransactions
    // - eventGroupId (for grouped events)
    // - Custom context data
    
    return await this.internalService_.update(
      id,
      data,
      sharedContext
    )
  }
}
```

<Note>
  The `@MedusaContext()` decorator must always be applied to the last parameter of the method, and that parameter should default to an empty object `{}`.
</Note>

### @EmitEvents

Automatically emits domain events after the method completes successfully.

```typescript theme={null}
import { InjectManager, EmitEvents, MedusaContext } from "@medusajs/framework/utils"
import type { Context } from "@medusajs/framework/types"

export class ApiKeyModuleService extends MedusaService {
  @InjectManager()
  @EmitEvents()
  async createApiKeys(
    data: ApiKeyTypes.CreateApiKeyDTO | ApiKeyTypes.CreateApiKeyDTO[],
    @MedusaContext() sharedContext: Context = {}
  ): Promise<ApiKeyTypes.ApiKeyDTO | ApiKeyTypes.ApiKeyDTO[]> {
    const [createdApiKeys, generatedTokens] = await this.createApiKeys_(
      Array.isArray(data) ? data : [data],
      sharedContext
    )

    const serializedResponse = await this.baseRepository_.serialize(
      createdApiKeys,
      { populate: true }
    )

    return Array.isArray(data) ? serializedResponse : serializedResponse[0]
  }
}
```

Source: `packages/modules/api-key/src/services/api-key-module-service.ts:123-150`

Events are automatically emitted based on the entity name and operation:

* `apiKey.created` - After creating API keys
* `apiKey.updated` - After updating API keys
* `apiKey.deleted` - After deleting API keys

## Decorator Combinations

Common decorator patterns:

<CodeGroup>
  ```typescript Public Method Pattern theme={null}
  @InjectManager()
  @EmitEvents()
  async createEntity(
    data: CreateDTO,
    @MedusaContext() sharedContext: Context = {}
  ) {
    return await this.createEntity_(data, sharedContext)
  }
  ```

  ```typescript Protected Method Pattern theme={null}
  @InjectTransactionManager()
  protected async createEntity_(
    data: CreateDTO,
    @MedusaContext() sharedContext: Context = {}
  ) {
    return await this.entityService_.create(data, sharedContext)
  }
  ```

  ```typescript Read-Only Method theme={null}
  @InjectManager()
  async listEntities(
    filters: FilterableEntityProps,
    config: FindConfig<EntityDTO>,
    @MedusaContext() sharedContext: Context = {}
  ) {
    return await this.entityService_.list(filters, config, sharedContext)
  }
  ```
</CodeGroup>

## Transaction Management

Services handle transactions automatically through decorators:

### Automatic Transactions

```typescript theme={null}
export class OrderModuleService extends MedusaService {
  @InjectManager()
  @EmitEvents()
  async createOrder(
    data: CreateOrderDTO,
    @MedusaContext() sharedContext: Context = {}
  ) {
    // Public method delegates to protected method
    return await this.createOrder_(data, sharedContext)
  }

  @InjectTransactionManager()
  protected async createOrder_(
    data: CreateOrderDTO,
    @MedusaContext() sharedContext: Context = {}
  ) {
    // All operations in this method run in a transaction
    const order = await this.orderService_.create(data, sharedContext)
    
    await this.orderLineItemService_.create(
      data.items,
      sharedContext
    )
    
    // If any operation fails, entire transaction rolls back
    return order
  }
}
```

### Manual Transactions

For complex scenarios, manually manage transactions:

```typescript theme={null}
import { MedusaError } from "@medusajs/framework/utils"

export class MyService extends MedusaService {
  async complexOperation(data: ComplexData) {
    const manager = this.activeManager_
    
    return await manager.transaction(async (transactionManager) => {
      const context = { transactionManager }
      
      const result1 = await this.step1(data, context)
      const result2 = await this.step2(result1, context)
      
      if (!result2.isValid) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          "Operation failed validation"
        )
      }
      
      return result2
    })
  }
}
```

## Event Emission

Services emit events automatically with `@EmitEvents`:

### Automatic Event Names

Events follow the pattern `{entityName}.{operation}`:

```typescript theme={null}
// API Key operations
apiKey.created
apiKey.updated  
apiKey.deleted

// Product operations
product.created
product.updated
product.deleted

// Order operations
order.created
order.updated
order.canceled
```

### Event Data

Event data includes the entity IDs:

```typescript theme={null}
{
  id: "apikey_123",  // Single entity
  // or
  ids: ["apikey_123", "apikey_456"]  // Multiple entities
}
```

### Subscribing to Service Events

Create subscribers to handle service events:

```typescript theme={null}
import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"

export default async function handleApiKeyCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const apiKeyModule = container.resolve(Modules.API_KEY)
  const apiKey = await apiKeyModule.retrieveApiKey(data.id)
  
  // Send notification, log audit trail, etc.
  console.log(`API key created: ${apiKey.title}`)
}

export const config: SubscriberConfig = {
  event: "apiKey.created",
}
```

## Context Propagation

The shared context flows through all service calls:

```typescript theme={null}
export class OrderService extends MedusaService {
  @InjectManager()
  @EmitEvents()
  async createOrder(
    data: CreateOrderDTO,
    @MedusaContext() sharedContext: Context = {}
  ) {
    // Context is passed to all nested calls
    return await this.createOrder_(data, sharedContext)
  }

  @InjectTransactionManager()
  protected async createOrder_(
    data: CreateOrderDTO,
    @MedusaContext() sharedContext: Context = {}
  ) {
    // Same context propagates to child services
    const order = await this.orderService_.create(data, sharedContext)
    
    // Transaction manager is shared via context
    await this.lineItemService_.create(
      data.items,
      sharedContext  // Contains transactionManager
    )
    
    return order
  }
}
```

### Custom Context Data

Add custom data to the context:

```typescript theme={null}
export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const orderModule = req.scope.resolve(Modules.ORDER)
  
  const order = await orderModule.createOrder(data, {
    // Custom context data
    actor_id: req.auth_context.actor_id,
    actor_type: req.auth_context.actor_type,
  })
  
  res.json({ order })
}
```

## Error Handling

Services use `MedusaError` for consistent error handling:

```typescript theme={null}
import { MedusaError, MedusaContext, InjectManager } from "@medusajs/framework/utils"
import type { Context } from "@medusajs/framework/types"

export class ApiKeyModuleService extends MedusaService {
  @InjectManager()
  async revokeApiKey(
    id: string,
    @MedusaContext() sharedContext: Context = {}
  ) {
    const apiKey = await this.apiKeyService_.retrieve(
      id,
      { select: ["id", "revoked_at"] },
      sharedContext
    )

    if (apiKey.revoked_at && apiKey.revoked_at < new Date()) {
      throw new MedusaError(
        MedusaError.Types.NOT_ALLOWED,
        `API key ${id} is already revoked`
      )
    }

    return await this.apiKeyService_.update(
      id,
      { revoked_at: new Date() },
      sharedContext
    )
  }
}
```

### Common Error Types

<CodeGroup>
  ```typescript NOT_FOUND theme={null}
  throw new MedusaError(
    MedusaError.Types.NOT_FOUND,
    `Product with id ${id} not found`
  )
  ```

  ```typescript INVALID_DATA theme={null}
  throw new MedusaError(
    MedusaError.Types.INVALID_DATA,
    `Invalid email format: ${email}`
  )
  ```

  ```typescript NOT_ALLOWED theme={null}
  throw new MedusaError(
    MedusaError.Types.NOT_ALLOWED,
    `Cannot delete active API key`
  )
  ```
</CodeGroup>

Source: `packages/modules/api-key/src/services/api-key-module-service.ts:100-107`

## Best Practices

<Steps>
  <Step title="Use Public/Protected Pattern">
    Public methods have `@InjectManager` and `@EmitEvents`. Protected methods (suffixed with `_`) have `@InjectTransactionManager`.
  </Step>

  <Step title="Always Pass Context">
    Always pass `sharedContext` to nested service calls to maintain transaction scope.
  </Step>

  <Step title="Validate Early">
    Validate input data before starting database operations to fail fast.
  </Step>

  <Step title="Return Serialized DTOs">
    Use `baseRepository_.serialize()` to return properly typed DTOs.
  </Step>
</Steps>

<Warning>
  Never access `this.manager_` or `this.transactionManager_` directly. Always use decorators and pass context to maintain proper transaction boundaries.
</Warning>

## Testing Services

Services are testable through dependency injection:

```typescript theme={null}
import { createContainer, asValue, asClass } from "@medusajs/deps/awilix"
import { ApiKeyModuleService } from "../api-key-module-service"

describe("ApiKeyModuleService", () => {
  let container
  let service

  beforeEach(() => {
    container = createContainer()
    
    container.register({
      baseRepository: asValue({
        serialize: jest.fn((data) => data),
      }),
      apiKeyService: asValue({
        create: jest.fn(),
        retrieve: jest.fn(),
        list: jest.fn(),
        delete: jest.fn(),
      }),
    })
    
    container.register({
      apiKeyModuleService: asClass(ApiKeyModuleService).singleton(),
    })
    
    service = container.resolve("apiKeyModuleService")
  })

  it("should create an API key", async () => {
    const mockApiKey = { id: "apikey_123", title: "Test Key" }
    container.resolve("apiKeyService").create.mockResolvedValue(mockApiKey)
    
    const result = await service.createApiKeys({
      title: "Test Key",
      type: "secret",
    })
    
    expect(result).toEqual(mockApiKey)
  })
})
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Events" href="/concepts/events">
    Learn how to subscribe to service events
  </Card>

  <Card title="Workflows" href="/concepts/workflows">
    Use services in workflow steps
  </Card>
</CardGroup>
