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

# How to Create a Custom Module

> Keep a module thin: it owns one domain's data and its CRUD, with no orchestration, events, or cross-module calls.

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. It is the kind of custom module you 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.

Define the model with plain columns.

```ts 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(),
})
```

Extend `MedusaService` to get the generated CRUD methods.

```ts 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
```

Add a custom method only when the logic is about this module's own data and can't be expressed with the generated methods, such as a specialised query. When you do, use Medusa's DI decorators so the method runs in the ambient context.

```ts src/modules/brand/service.ts 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>
  Some logic 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

Follow four conventions so the module reads like the built-in ones.

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

  ```ts 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 such as `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`, and `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 such as `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. This is covered in full on [Module links](/rc/resources/best-practices/module-links).

```ts 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 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 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 })` and 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` plus `@MedusaContext`.
* DTOs are exported and imported, never redeclared inline.
* No orchestration, no events, no HTTP. Those live in workflows and routes.

## Next steps

<CardGroup cols={2}>
  <Card title="Workflows" href="/rc/resources/best-practices/workflows">
    Orchestrate business operations across modules, with compensation on failure.
  </Card>

  <Card title="Module links" href="/rc/resources/best-practices/module-links">
    Relate two modules with `defineLink` and read the relationship through Query.
  </Card>

  <Card title="Types & augmentation" href="/rc/resources/best-practices/types">
    Export DTOs and share a model's shape instead of redeclaring it inline.
  </Card>

  <Card title="Best practices overview" href="/rc/resources/best-practices/overview">
    See the layered architecture and the logic-placement cheat sheet.
  </Card>
</CardGroup>
