> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mercurjs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Modules

> Keep modules thin — data access and CRUD only. Naming, decorators, and what must never live in a module service.

A module is the lowest layer of the [architecture](/rc/resources/best-practices/overview): it owns exactly one domain's data and nothing else. Modules are isolated — they never reach into another module, never orchestrate a business operation, and never react to events. All of that lives one layer up, in [workflows](/rc/resources/best-practices/workflows).

<Note>
  A Mercur module is a standard [Medusa module](https://docs.medusajs.com/learn/fundamentals/modules). The examples below build a small **Brand** module — the kind of custom module you'd add to your own project alongside the built-in ones — so the rules stand on their own rather than relying on Mercur internals.
</Note>

## Thin CRUD only

A module service exists to read and write its own tables. Extend `MedusaService({ ...models })` and you get typed `list`, `listAndCount`, `retrieve`, `create`, `update`, and `delete` methods for every model for free — use them.

```ts title="src/modules/brand/models/brand.ts" theme={null}
import { model } from "@medusajs/framework/utils"

export const Brand = model.define("brand", {
  id: model.id().primaryKey(),
  name: model.text(),
})
```

```ts title="src/modules/brand/service.ts" theme={null}
import { MedusaService } from "@medusajs/framework/utils"
import { Brand } from "./models/brand"

class BrandModuleService extends MedusaService({
  Brand,
}) {
  // Generated for you: listBrands, retrieveBrand, createBrands,
  // updateBrands, deleteBrands, listAndCountBrands, ...
}

export default BrandModuleService
```

Only add a custom method when the logic is **about this module's own data** and can't be expressed with the generated methods — for example, a specialised query. When you do, use Medusa's DI decorators so the method runs in the ambient context:

```ts title="A justified custom method — still single-module" theme={null}
class BrandModuleService extends MedusaService({ Brand }) {
  @InjectManager()
  async listActiveBrands(
    filters: FindConfig<BrandDTO> = {},
    @MedusaContext() sharedContext: Context = {}
  ): Promise<BrandDTO[]> {
    return this.listBrands({ ...filters, is_active: true }, {}, sharedContext)
  }
}
```

<Warning>
  **What must never live in a module service:** business orchestration, calls to another module's service, event emission, HTTP concerns, or anything that mutates data outside this module. If a method needs a second module's data or writes across a boundary, it belongs in a [workflow](/rc/resources/best-practices/workflows), not here. See the [logic-placement cheat sheet](/rc/resources/best-practices/overview#logic-placement-cheat-sheet).
</Warning>

## Naming

* **Register the module by a stable id constant.** Export the module id and register the service against it:

  ```ts title="src/modules/brand/index.ts" theme={null}
  import { Module } from "@medusajs/framework/utils"
  import BrandModuleService from "./service"

  export const BRAND_MODULE = "brand"

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

  <Tip>
    Mercur's own modules follow the same pattern but read their id from the shared `MercurModules` enum in `@mercurjs/types` (e.g. `Module(MercurModules.SELLER, …)`). For a project-local module, a single exported constant like `BRAND_MODULE` is enough — just never inline the raw string in more than one place.
  </Tip>

* **Methods are `camelCase` and model-suffixed.** Medusa generates `listBrands`, `createBrands`, `retrieveBrand` — match that casing and pluralisation when you add or override methods. Private helpers end with a trailing underscore (`computeBrandStats_`).

* **Models are lowercase-defined, referenced by their key.** `model.define("brand", { ... })`; the object key you pass to `MedusaService` (`Brand`) is what drives the generated method names.

* **Types live next to the module (or in a shared types package).** Export DTOs like `BrandDTO` and import them; never redeclare a model's shape ad hoc. See [Types & augmentation](/rc/resources/best-practices/types).

## Do not call `.linkable()` — links are declared separately

It is tempting to relate two modules by pointing a model at another module's table. Don't. A module model must not reference another module's data, and you should not wire relationships inside the model definition.

<Warning>
  Cross-module relationships are declared **outside** the modules, with `defineLink`, and read through **Query**. A module never imports another module's `.linkable` shape to build a foreign key into it. Keeping models link-free is what lets modules stay independently migratable and upgrade-safe.
</Warning>

Define the relationship as its own link file instead — covered in full on [Module links](/rc/resources/best-practices/module-links):

```ts title="Relationship declared as a link, not inside the model" theme={null}
import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"
import BrandModule from "../modules/brand"

export default defineLink(
  ProductModule.linkable.product,
  BrandModule.linkable.brand
)
```

The model itself stays flat — plain columns, no relations pointing at other modules:

```ts title="src/modules/brand/models/brand.ts" theme={null}
export const Brand = model.define("brand", {
  id: model.id().primaryKey(),
  name: model.text(),
  is_active: model.boolean().default(true),
  metadata: model.json().nullable(),
})
```

## Decorators

Custom service methods that touch the database use Medusa's dependency-injection decorators so they participate in the ambient transaction and shared context:

| Decorator                     | Use it on                              | Purpose                                                               |
| ----------------------------- | -------------------------------------- | --------------------------------------------------------------------- |
| `@InjectManager()`            | Read methods                           | Injects the entity manager so the method runs in the current context. |
| `@InjectTransactionManager()` | Write methods                          | Runs the method inside a transaction, enabling rollback.              |
| `@MedusaContext()`            | The trailing `sharedContext` parameter | Threads the request/transaction context through the call.             |

```ts title="Decorator pattern for a custom write" theme={null}
@InjectTransactionManager()
async deactivateBrand(
  id: string,
  @MedusaContext() sharedContext: Context = {}
): Promise<BrandDTO> {
  return this.updateBrands({ id, is_active: false }, sharedContext)
}
```

<Tip>
  If you don't need a custom method, don't write one. The generated `MedusaService` methods already carry the right decorators and transaction behaviour — reaching for them first keeps modules thin by default.
</Tip>

## Checklist for a module

* Extends `MedusaService({ ...models })`; leans on generated CRUD.
* Registered with `Module(BRAND_MODULE, { service })` against a stable id.
* No import of, or call into, any other module's service.
* Models are flat — no `.linkable()` wiring, no cross-module foreign keys.
* Custom methods use `@InjectManager` / `@InjectTransactionManager` + `@MedusaContext`.
* DTOs are exported and imported, never redeclared inline.
* No orchestration, no events, no HTTP — those live in workflows and routes.
