import { CodeTabs, CodeTab } from "docs-ui"

export const metadata = {
  title: `Medusa Examples`,
}

# {metadata.title}

This documentation page has examples of customizations useful for your custom development in the Medusa application.

Each section links to the associated documentation page to learn more about it.

## API Routes

An API route is a REST API endpoint that exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems.

### Create API Route

Create the file `src/api/hello-world/route.ts` with the following content:

```ts title="src/api/hello-world/route.ts"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: "[GET] Hello world!",
  })
}
```

This creates a `GET` API route at `/hello-world`.

Learn more in [this documentation](!docs!/learn/fundamentals/api-routes).

### Resolve Resources in API Route

To resolve resources from the Medusa container in an API route:

```ts highlights={[["8", "resolve", "Resolve the Product Module's\nmain service from the Medusa container."]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest, 
  res: MedusaResponse
) => {
  const productModuleService = req.scope.resolve(
    Modules.PRODUCT
  )

  const [, count] = await productModuleService
    .listAndCountProducts()

  res.json({
    count,
  })
}
```

This resolves the Product Module's main service.

Learn more in [this documentation](!docs!/learn/fundamentals/medusa-container).

### Use Path Parameters

API routes can accept path parameters.

To do that, create the file `src/api/hello-world/[id]/route.ts` with the following content:

export const singlePathHighlights = [
  ["11", "req.params.id", "Access the path parameter `id`"]
]

```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: `[GET] Hello ${req.params.id}!`,
  })
}
```

Learn more about path parameters in [this documentation](!docs!/learn/fundamentals/api-routes/parameters#path-parameters).

### Use Query Parameters

API routes can accept query parameters:

export const queryHighlights = [
  ["11", "req.query.name", "Access the query parameter `name`"],
]

```ts highlights={queryHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: `Hello ${req.query.name}`,
  })
}
```

Learn more about query parameters in [this documentation](!docs!/learn/fundamentals/api-routes/parameters#query-parameters).

### Use Body Parameters

API routes can accept request body parameters:

export const bodyHighlights = [
  ["11", "HelloWorldReq", "Specify the type of the request body parameters."],
  ["15", "req.body.name", "Access the request body parameter `name`"],
]

```ts highlights={bodyHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

type HelloWorldReq = {
  name: string
}

export const POST = async (
  req: MedusaRequest<HelloWorldReq>,
  res: MedusaResponse
) => {
  res.json({
    message: `[POST] Hello ${req.body.name}!`,
  })
}
```

Learn more about request body parameters in [this documentation](!docs!/learn/fundamentals/api-routes/parameters#request-body-parameters).

### Set Response Code

You can change the response code of an API route:

```ts highlights={[["7", "status", "Change the response's status."]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.status(201).json({
    message: "Hello, World!",
  })
}
```

Learn more about setting the response code in [this documentation](!docs!/learn/fundamentals/api-routes/responses#set-response-status-code).

### Execute a Workflow in an API Route

To execute a workflow in an API route:

```ts
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import myWorkflow from "../../workflows/hello-world"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await myWorkflow(req.scope)
    .run({
      input: {
        name: req.query.name as string,
      },
    })

  res.send(result)
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/workflows#3-execute-the-workflow).

### Change Response Content Type

By default, an API route's response has the content type `application/json`.

To change it to another content type, use the `writeHead` method of `MedusaResponse`:

export const responseContentTypeHighlights = [
  ["7", "writeHead", "Change the content type in the header."]
]

```ts highlights={responseContentTypeHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  })

  const interval = setInterval(() => {
    res.write("Streaming data...\n")
  }, 3000)

  req.on("end", () => {
    clearInterval(interval)
    res.end()
  })
}
```

This changes the response type to return an event stream.

Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/responses#change-response-content-type).

### Create Middleware

A middleware is a function executed when a request is sent to an API Route.

Create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import type { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
  defineMiddlewares,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          console.log("Received a request!")

          next()
        },
      ],
    },
    {
      matcher: "/custom/:id",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          console.log("With Path Parameter")

          next()
        },
      ],
    },
  ],
})
```

Learn more about middlewares in [this documentation](!docs!/learn/fundamentals/api-routes/middlewares).

### Restrict HTTP Methods in Middleware

To restrict a middleware to an HTTP method:

export const middlewareMethodHighlights = [
  ["12", "method", "Apply the middleware on `POST` and `PUT` requests only."]
]

```ts title="src/api/middlewares.ts" highlights={middlewareMethodHighlights}
import type { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
  defineMiddlewares,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      method: ["POST", "PUT"],
      middlewares: [
        // ...
      ],
    },
  ],
})
```

### Add Validation for Custom Routes

1. Create a [Zod](https://zod.dev/) schema in the file `src/api/custom/validators.ts`:

```ts title="src/api/custom/validators.ts"
import { z } from "zod"

export const PostStoreCustomSchema = z.object({
  a: z.number(),
  b: z.number(),
})
```

2. Add a validation middleware to the custom route in `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts" highlights={[["13", "validateAndTransformBody"]]}
import { 
  validateAndTransformBody,
  defineMiddlewares,
} from "@medusajs/framework/http"
import { PostStoreCustomSchema } from "./custom/validators"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom",
      method: "POST",
      middlewares: [
        validateAndTransformBody(PostStoreCustomSchema),
      ],
    },
  ],
})
```

3. Use the validated body in the `/custom` API route:

```ts title="src/api/custom/route.ts" highlights={[["14", "validatedBody"]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { z } from "zod"
import { PostStoreCustomSchema } from "./validators"

type PostStoreCustomSchemaType = z.infer<
  typeof PostStoreCustomSchema
>

export const POST = async (
  req: MedusaRequest<PostStoreCustomSchemaType>,
  res: MedusaResponse
) => {
  res.json({
    sum: req.validatedBody.a + req.validatedBody.b,
  })
}
```

Learn more about request body validation in [this documentation](!docs!/learn/fundamentals/api-routes/validation).

### Pass Additional Data to API Route

In this example, you'll pass additional data to the Create Product API route, then consume its hook:

<Note>

Find this example in details in [this documentation](!docs!/learn/customization/extend-features/extend-create-product).

</Note>

1. Create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={[["10", "brand_id", "Replace with your custom field."]]}
import { defineMiddlewares } from "@medusajs/framework/http"
import { z } from "zod"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/products",
      method: ["POST"],
      additionalDataValidator: {
        brand_id: z.string().optional(),
      },
    },
  ],
})
```

<Note>

Learn more about additional data in [this documentation](!docs!/learn/fundamentals/api-routes/additional-data).

</Note>

2. Create the file `src/workflows/hooks/created-product.ts` with the following content:

```ts
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
import { StepResponse } from "@medusajs/framework/workflows-sdk"

createProductsWorkflow.hooks.productsCreated(
  (async ({ products, additional_data }, { container }) => {
    if (!additional_data.brand_id) {
      return new StepResponse([], [])
    }

    // TODO perform custom action
  }),
  (async (links, { container }) => {
    // TODO undo the action in the compensation
  })

)
```

<Note>

Learn more about workflow hooks in [this documentation](!docs!/learn/fundamentals/workflows/workflow-hooks).

</Note>

### Restrict an API Route to Admin Users

You can protect API routes by restricting access to authenticated admin users only.

Add the following middleware in `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts" highlights={[["11", "authenticate"]]}
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom/admin*",
      middlewares: [
        authenticate(
          "user", 
          ["session", "bearer", "api-key"]
        ),
      ],
    },
  ],
})
```

Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/protected-routes).

### Restrict an API Route to Logged-In Customers

You can protect API routes by restricting access to authenticated customers only.

Add the following middleware in `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts" highlights={[["11", "authenticate"]]}
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom/customer*",
      middlewares: [
        authenticate("customer", ["session", "bearer"]),
      ],
    },
  ],
})
```

Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/protected-routes).

### Retrieve Logged-In Admin User

To retrieve the currently logged-in user in an API route:

<Note>

Requires setting up the authentication middleware as explained in [this example](#restrict-an-api-route-to-admin-users).

</Note>

```ts highlights={[["16", "req.auth_context.actor_id", "Access the user's ID."]]}
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const userModuleService = req.scope.resolve(
    Modules.USER
  )

  const user = await userModuleService.retrieveUser(
    req.auth_context.actor_id
  )

  // ...
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/protected-routes#retrieve-logged-in-admin-users-details).

### Retrieve Logged-In Customer

To retrieve the currently logged-in customer in an API route:

<Note>

Requires setting up the authentication middleware as explained in [this example](#restrict-an-api-route-to-logged-in-customers).

</Note>

```ts highlights={[["18", "req.auth_context.actor_id", "Access the customer's ID."]]}
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  if (req.auth_context?.actor_id) {
    // retrieve customer
    const customerModuleService = req.scope.resolve(
      Modules.CUSTOMER
    )

    const customer = await customerModuleService.retrieveCustomer(
      req.auth_context.actor_id
    )
  }

  // ...
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/protected-routes#retrieve-logged-in-customers-details).

### Throw Errors in API Route

To throw errors in an API route, use `MedusaError` from the Medusa Framework:

```ts highlights={[["9", "MedusaError"]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  if (!req.query.q) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "The `q` query parameter is required."
    )
  }

  // ...
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/errors).

### Override Error Handler of API Routes

To override the error handler of API routes, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={[["10", "errorHandler"]]}
import { 
  defineMiddlewares, 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"

export default defineMiddlewares({
  errorHandler: (
    error: MedusaError | any, 
    req: MedusaRequest, 
    res: MedusaResponse, 
    next: MedusaNextFunction
  ) => {
    res.status(400).json({
      error: "Something happened.",
    })
  },
})
```

Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/errors#override-error-handler),

### Setting up CORS for Custom API Routes

By default, Medusa configures CORS for all routes starting with `/admin`, `/store`, and `/auth`.

To configure CORS for routes under other prefixes, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import type { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
  defineMiddlewares,
} from "@medusajs/framework/http"
import { ConfigModule } from "@medusajs/framework/types"
import { parseCorsOrigins } from "@medusajs/framework/utils"
import cors from "cors"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          const configModule: ConfigModule =
            req.scope.resolve("configModule")

          return cors({
            origin: parseCorsOrigins(
              configModule.projectConfig.http.storeCors
            ),
            credentials: true,
          })(req, res, next)
        },
      ],
    },
  ],
})
```

### Parse Webhook Body

By default, the Medusa application parses a request's body using JSON.

To parse a webhook's body, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={[["9"]]}
import { 
  defineMiddlewares, 
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/webhooks/*",
      bodyParser: { preserveRawBody: true },
      method: ["POST"],
    },
  ],
})
```

To access the raw body data in your route, use the `req.rawBody` property:

```ts title="src/api/webhooks/route.ts"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const POST = (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  console.log(req.rawBody)
}
```

---

## Modules

A module is a package of reusable commerce or architectural functionalities. They handle business logic in a class called a service, and define and manage data models that represent tables in the database.

### Create Module

<Note>

Find this example explained in details in [this documentation](!docs!/learn/fundamentals/modules).

</Note>

1. Create the directory `src/modules/blog`.
2. Create the file `src/modules/blog/models/post.ts` with the following data model:

```ts title="src/modules/blog/models/post.ts"
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  title: model.text(),
})

export default Post
```

3. Create the file `src/modules/blog/service.ts` with the following service:

```ts title="src/modules/blog/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

class BlogModuleService extends MedusaService({
  Post,
}){
}

export default BlogModuleService
```

4. Create the file `src/modules/blog/index.ts` that exports the module definition:

```ts title="src/modules/blog/index.ts"
import BlogModuleService from "./service"
import { Module } from "@medusajs/framework/utils"

export const BLOG_MODULE = "blog"

export default Module(BLOG_MODULE, {
  service: BlogModuleService,
})
```

5. Add the module to the configurations in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    // ...
  },
  modules: [
    {
      resolve: "./modules/blog",
    },
  ],
})
```

6. Generate and run migrations:

```bash
npx medusa db:generate blog
npx medusa db:migrate
```

7. Use the module's main service in an API route:

```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import BlogModuleService from "../../modules/blog/service"
import { BLOG_MODULE } from "../../modules/blog"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
): Promise<void> {
  const blogModuleService: BlogModuleService = req.scope.resolve(
    BLOG_MODULE
  )

  const post = await blogModuleService.createPosts({
    title: "test",
  })

  res.json({
    post,
  })
}
```

### Module with Multiple Services

To add services in your module other than the main one, create them in the `services` directory of the module.

For example, create the file `src/modules/blog/services/category.ts` with the following content:

```ts title="src/modules/blog/services/category.ts"
export class CategoryService {
  // TODO add methods
}
```

Then, export the service in the file `src/modules/blog/services/index.ts`:

```ts title="src/modules/blog/services/index.ts"
export * from "./category"
```

Finally, resolve the service in your module's main service or loader:

```ts title="src/modules/blog/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
import { CategoryService } from "./services"

type InjectedDependencies = {
  categoryService: CategoryService
}

class BlogModuleService extends MedusaService({
  Post,
}){
  private categoryService: CategoryService

  constructor({ categoryService }: InjectedDependencies) {
    super(...arguments)

    this.categoryService = categoryService
  }
}

export default BlogModuleService
```

Learn more in [this documentation](!docs!/learn/fundamentals/modules/multiple-services).

### Accept Module Options

A module can accept options for configurations and secrets.

To accept options in your module:

1. Pass options to the module in `medusa-config.ts`:

```ts title="medusa-config.ts" highlights={[["6", "options"]]}
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./modules/blog",
      options: {
        apiKey: true,
      },
    },
  ],
})
```

2. Access the options in the module's main service:

```ts title="src/modules/blog/service.ts" highlights={[["14", "options"]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

// recommended to define type in another file
type ModuleOptions = {
  apiKey?: boolean
}

export default class BlogModuleService extends MedusaService({
  Post,
}){
  protected options_: ModuleOptions

  constructor({}, options?: ModuleOptions) {
    super(...arguments)

    this.options_ = options || {
      apiKey: false,
    }
  }

  // ...
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/modules/options).

### Integrate Third-Party System in Module

An example of integrating a dummy third-party system in a module's service:

```ts title="src/modules/blog/service.ts"
import { Logger } from "@medusajs/framework/types"
import { BLOG_MODULE } from ".."

export type ModuleOptions = {
  apiKey: string
}

type InjectedDependencies = {
  logger: Logger
}

export class BlogClient {
  private options_: ModuleOptions
  private logger_: Logger

  constructor(
    { logger }: InjectedDependencies, 
    options: ModuleOptions
  ) {
    this.logger_ = logger
    this.options_ = options
  }

  private async sendRequest(url: string, method: string, data?: any) {
    this.logger_.info(`Sending a ${
      method
    } request to ${url}. data: ${JSON.stringify(data, null, 2)}`)
    this.logger_.info(`Client Options: ${
      JSON.stringify(this.options_, null, 2)
    }`)
  }
}
```

Find a longer example of integrating a third-party service in [this documentation](!docs!/learn/customization/integrate-systems/service).

---

## Data Models

A data model represents a table in the database. Medusa provides a data model language to intuitively create data models.

### Create Data Model

To create a data model in a module:

<Note>

This assumes you already have a module. If not, follow [this example](#create-module).

</Note>

1. Create the file `src/modules/blog/models/post.ts` with the following data model:

```ts title="src/modules/blog/models/post.ts"
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  title: model.text(),
})

export default Post
```

2. Generate and run migrations:

```bash
npx medusa db:generate blog
npx medusa db:migrate
```

Learn more in [this documentation](!docs!/learn/fundamentals/modules#1-create-data-model).

### Data Model Property Types

A data model can have properties of the following types:

1. ID property:

```ts
const Post = model.define("post", {
  id: model.id(),
  // ...
})
```

2. Text property:

```ts
const Post = model.define("post", {
  title: model.text(),
  // ...
})
```

3. Number property:

```ts
const Post = model.define("post", {
  views: model.number(),
  // ...
})
```

4. Big Number property:

```ts
const Post = model.define("post", {
  price: model.bigNumber(),
  // ...
})
```

5. Boolean property:

```ts
const Post = model.define("post", {
  isPublished: model.boolean(),
  // ...
})
```

6. Enum property:

```ts
const Post = model.define("post", {
  status: model.enum(["draft", "published"]),
  // ...
})
```

7. Date-Time property:

```ts
const Post = model.define("post", {
  publishedAt: model.dateTime(),
  // ...
})
```

8. JSON property:

```ts
const Post = model.define("post", {
  metadata: model.json(),
  // ...
})
```

9. Array property:

```ts
const Post = model.define("post", {
  tags: model.array(),
  // ...
})
```

Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties).

### Set Primary Key

To set an `id` property as the primary key of a data model:

```ts highlights={[["4", "primaryKey"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  // ...
})

export default Post
```

To set a `text` property as the primary key:

```ts highlights={[["4", "primaryKey"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  title: model.text().primaryKey(),
  // ...
})

export default Post
```

To set a `number` property as the primary key:

```ts highlights={[["4", "primaryKey"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  views: model.number().primaryKey(),
  // ...
})

export default Post
```

Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#set-primary-key-property).

### Default Property Value

To set the default value of a property:

```ts highlights={[["6"], ["9"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  status: model
    .enum(["draft", "published"])
    .default("draft"),
  views: model
    .number()
    .default(0),
  // ...
})

export default Post
```

Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#property-default-value).

### Nullable Property

To allow `null` values for a property:

```ts highlights={[["4", "nullable"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  price: model.bigNumber().nullable(),
  // ...
})

export default Post
```

Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#make-property-optional).

### Unique Property

To create a unique index on a property:

```ts highlights={[["4", "unique"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  title: model.text().unique(),
  // ...
})

export default Post
```

Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#unique-property).

### Define Database Index on Property

To define a database index on a property:

```ts highlights={[["5", "index"]]}
import { model } from "@medusajs/framework/utils"

const MyCustom = model.define("my_custom", {
  id: model.id().primaryKey(),
  title: model.text().index(
    "IDX_POST_TITLE"
  ),
})

export default MyCustom
```

Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#define-database-index-on-property).

### Define Composite Index on Data Model

To define a composite index on a data model:

```ts highlights={[["7", "indexes"]]}
import { model } from "@medusajs/framework/utils"

const MyCustom = model.define("my_custom", {
  id: model.id().primaryKey(),
  name: model.text(),
  age: model.number().nullable(),
}).indexes([
  {
    on: ["name", "age"],
    where: {
      age: {
        $ne: null,
      },
    },
  },
])

export default MyCustom
```

Learn more in [this documentation](!docs!/learn/fundamentals/data-models/index).

### Make a Property Searchable

To make a property searchable using terms or keywords:

```ts highlights={[["4", "searchable"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  title: model.text().searchable(),
  // ...
})

export default Post
```

Then, to search by that property, pass the `q` filter to the `list` or `listAndCount` generated methods of the module's main service:

<Note>

`blogModuleService` is the main service that manages the `Post` data model.

</Note>

```ts
const posts = await blogModuleService.listPosts({
  q: "John",
})
```

Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#searchable-property).

### Create One-to-One Relationship

The following creates a one-to-one relationship between the `User` and `Email` data models:

```ts highlights={[["5", "hasOne"], ["10", "belongsTo"]]}
import { model } from "@medusajs/framework/utils"

const User = model.define("user", {
  id: model.id().primaryKey(),
  email: model.hasOne(() => Email),
})

const Email = model.define("email", {
  id: model.id().primaryKey(),
  user: model.belongsTo(() => User, {
    mappedBy: "email",
  }),
})
```

Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relationships#one-to-one-relationship).

### Create One-to-Many Relationship

The following creates a one-to-many relationship between the `Store` and `Product` data models:

```ts highlights={[["5", "hasMany"], ["10", "belongsTo"]]}
import { model } from "@medusajs/framework/utils"

const Store = model.define("store", {
  id: model.id().primaryKey(),
  products: model.hasMany(() => Product),
})

const Product = model.define("product", {
  id: model.id().primaryKey(),
  store: model.belongsTo(() => Store, {
    mappedBy: "products",
  }),
})
```

Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relationships#one-to-many-relationship).

### Create Many-to-Many Relationship

The following creates a many-to-many relationship between the `Order` and `Product` data models:

```ts highlights={[["5", "manyToMany"], ["12", "manyToMany"]]}
import { model } from "@medusajs/framework/utils"

const Order = model.define("order", {
  id: model.id().primaryKey(),
  products: model.manyToMany(() => Product, {
    mappedBy: "orders",
  }),
})

const Product = model.define("product", {
  id: model.id().primaryKey(),
  orders: model.manyToMany(() => Order, {
    mappedBy: "products",
  }),
})
```

Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relationships#many-to-many-relationship).

### Configure Cascades of Data Model

To configure cascade on a data model:

```ts highlights={[["7", "cascades"]]}
import { model } from "@medusajs/framework/utils"
// Product import

const Store = model.define("store", {
  id: model.id().primaryKey(),
  products: model.hasMany(() => Product),
})
.cascades({
  delete: ["products"],
})
```

This configures the delete cascade on the `Store` data model so that, when a store is delete, its products are also deleted.

Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relationships#cascades).


### Manage One-to-One Relationship

Consider you have a one-to-one relationship between `Email` and `User` data models, where an email belongs to a user.

To set the ID of the user that an email belongs to:

<Note>

`blogModuleService` is the main service that manages the `Email` and `User` data models.

</Note>

```ts
// when creating an email
const email = await blogModuleService.createEmails({
  // other properties...
  user: "123",
})

// when updating an email
const email = await blogModuleService.updateEmails({
  id: "321",
  // other properties...
  user: "123",
})
```

And to set the ID of a user's email when creating or updating it:

```ts
// when creating a user
const user = await blogModuleService.createUsers({
  // other properties...
  email: "123",
})

// when updating a user
const user = await blogModuleService.updateUsers({
  id: "321",
  // other properties...
  email: "123",
})
```

Learn more in [this documentation](!docs!/learn/fundamentals/data-models/manage-relationships#manage-one-to-one-relationship).

### Manage One-to-Many Relationship

Consider you have a one-to-many relationship between `Product` and `Store` data models, where a store has many products.

To set the ID of the store that a product belongs to:

<Note>

`blogModuleService` is the main service that manages the `Product` and `Store` data models.

</Note>

```ts
// when creating a product
const product = await blogModuleService.createProducts({
  // other properties...
  store_id: "123",
})

// when updating a product
const product = await blogModuleService.updateProducts({
  id: "321",
  // other properties...
  store_id: "123",
})
```

Learn more in [this documentation](!docs!/learn/fundamentals/data-models/manage-relationships#manage-one-to-many-relationship)

### Manage Many-to-Many Relationship

Consider you have a many-to-many relationship between `Order` and `Product` data models.

To set the orders a product has when creating it:

<Note>

`blogModuleService` is the main service that manages the `Product` and `Order` data models.

</Note>

```ts
const product = await blogModuleService.createProducts({
  // other properties...
  orders: ["123", "321"],
})
```

To add new orders to a product without removing the previous associations:

```ts
const product = await blogModuleService.retrieveProduct(
  "123",
  {
    relations: ["orders"],
  }
)

const updatedProduct = await blogModuleService.updateProducts({
  id: product.id,
  // other properties...
  orders: [
    ...product.orders.map((order) => order.id),
    "321",
  ],
})
```

Learn more in [this documentation](!docs!/learn/fundamentals/data-models/manage-relationships#manage-many-to-many-relationship).

### Retrieve Related Records

To retrieve records related to a data model's records through a relation, pass the `relations` field to the `list`, `listAndCount`, or `retrieve` generated methods:

<Note>

`blogModuleService` is the main service that manages the `Product` and `Order` data models.

</Note>

```ts highlights={[["4", "relations"]]}
const product = await blogModuleService.retrieveProducts(
  "123",
  {
    relations: ["orders"],
  }
)
```

Learn more in [this documentation](!docs!/learn/fundamentals/data-models/manage-relationships#retrieve-records-of-relation).

---

## Services

A service is the main resource in a module. It manages the records of your custom data models in the database, or integrate third-party systems.

### Extend Service Factory

The service factory `MedusaService` generates data-management methods for your data models.

To extend the service factory in your module's service:

```ts highlights={[["4", "MedusaService"]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

class BlogModuleService extends MedusaService({
  Post,
}){
  // TODO implement custom methods
}

export default BlogModuleService
```

The `BlogModuleService` will now have data-management methods for `Post`.

Refer to [this reference](../service-factory-reference/page.mdx) for details on the generated methods.

Learn more about the service factory in [this documentation](!docs!/learn/fundamentals/modules/service-factory).

### Resolve Resources in the Service

To resolve resources from the module's container in a service:

<CodeTabs group="service-type">
  <CodeTab label="With Service Factory" value="service-factory">

```ts highlights={[["14"]]}
import { Logger } from "@medusajs/framework/types"
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

type InjectedDependencies = {
  logger: Logger
}

class BlogModuleService extends MedusaService({
  Post,
}){
  protected logger_: Logger

  constructor({ logger }: InjectedDependencies) {
    super(...arguments)
    this.logger_ = logger

    this.logger_.info("[BlogModuleService]: Hello World!")
  }

  // ...
}

export default BlogModuleService
```

  </CodeTab>
  <CodeTab label="Without Service Factory" value="no-service-factory">

```ts highlights={[["10"]]}
import { Logger } from "@medusajs/framework/types"

type InjectedDependencies = {
  logger: Logger
}

export default class BlogModuleService {
  protected logger_: Logger

  constructor({ logger }: InjectedDependencies) {
    this.logger_ = logger

    this.logger_.info("[BlogModuleService]: Hello World!")
  }

  // ...
}
```

  </CodeTab>
</CodeTabs>

Learn more in [this documentation](!docs!/learn/fundamentals/modules/container).

### Access Module Options in Service

To access options passed to a module in its service:

```ts highlights={[["14", "options"]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

// recommended to define type in another file
type ModuleOptions = {
  apiKey?: boolean
}

export default class BlogModuleService extends MedusaService({
  Post,
}){
  protected options_: ModuleOptions

  constructor({}, options?: ModuleOptions) {
    super(...arguments)

    this.options_ = options || {
      apiKey: "",
    }
  }

  // ...
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/modules/options).

### Run Database Query in Service

To run database query in your service:

```ts highlights={[["14", "count"], ["21", "execute"]]}
// other imports...
import { 
  InjectManager,
  MedusaContext,
} from "@medusajs/framework/utils"

class BlogModuleService {
  // ...

  @InjectManager()
  async getCount(
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<number> {
    return await sharedContext.manager.count("post")
  }
  
  @InjectManager()
  async getCountSql(
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<number> {
    const data = await sharedContext.manager.execute(
      "SELECT COUNT(*) as num FROM post"
    ) 
    
    return parseInt(data[0].num)
  }
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/modules/db-operations#run-queries)

### Execute Database Operations in Transactions

To execute database operations within a transaction in your service:

```ts
import { 
  InjectManager,
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  protected async update_(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<any> {
    const transactionManager = sharedContext.transactionManager
    await transactionManager.nativeUpdate(
      "post",
      {
        id: input.id,
      },
      {
        name: input.name,
      }
    )

    // retrieve again
    const updatedRecord = await transactionManager.execute(
      `SELECT * FROM post WHERE id = '${input.id}'`
    )

    return updatedRecord
  }

  @InjectManager()
  async update(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ) {
    return await this.update_(input, sharedContext)
  }
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/modules/db-operations#execute-operations-in-transactions).

---

## Module Links

A module link forms an association between two data models of different modules, while maintaining module isolation.

### Define a Link

To define a link between your custom module and a commerce module, such as the Product Module:

1. Create the file `src/links/blog-product.ts` with the following content:

```ts title="src/links/blog-product.ts"
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  BlogModule.linkable.post
)
```

2. Run the following command to sync the links:

```bash
npx medusa db:migrate
```

Learn more in [this documentation](!docs!/learn/fundamentals/module-links).

### Define a List Link

To define a list link, where multiple records of a model can be linked to a record in another:

```ts highlights={[["9", "isList"]]}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  {
    linkable: BlogModule.linkable.post,
    isList: true,
  }
)
```

Learn more about list links in [this documentation](!docs!/learn/fundamentals/module-links#define-a-list-link).

### Set Delete Cascade on Link Definition

To ensure a model's records linked to another model are deleted when the linked model is deleted:

```ts highlights={[["9", "deleteCascades"]]}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  {
    linkable: BlogModule.linkable.post,
    deleteCascades: true,
  }
)
```

Learn more in [this documentation](!docs!/learn/fundamentals/module-links#define-a-list-link).

### Add Custom Columns to Module Link

To add a custom column to the table that stores the linked records of two data models:

```ts highlights={[["9", "database"]]}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  BlogModule.linkable.post,
  {
    database: {
      extraColumns: {
        metadata: {
          type: "json",
        },
      },
    },
  }
)
```

Then, to set the custom column when creating or updating a link between records:

```ts
await link.create({
  [Modules.PRODUCT]: {
    product_id: "123",
  },
  HELLO_MODULE: {
    my_custom_id: "321",
  },
  data: {
    metadata: {
      test: true,
    },
  },
})
```

To retrieve the custom column when retrieving linked records using Query:

```ts
import productBlogLink from "../links/product-blog"

// ...

const { data } = await query.graph({
  entity: productBlogLink.entryPoint,
  fields: ["metadata", "product.*", "post.*"],
  filters: {
    product_id: "prod_123",
  },
})
```

Learn more in [this documentation](!docs!/learn/fundamentals/module-links/custom-columns).

### Create Link Between Records

To create a link between two records using Link:

```ts
import { Modules } from "@medusajs/framework/utils"
import { BLOG_MODULE } from "../../modules/blog"

// ...

await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [HELLO_MODULE]: {
    my_custom_id: "mc_123",
  },
})
```

Learn more in [this documentation](!docs!/learn/fundamentals/module-links/link#create-link).

### Dismiss Link Between Records

To dismiss links between records using Link:

```ts
import { Modules } from "@medusajs/framework/utils"
import { BLOG_MODULE } from "../../modules/blog"

// ...

await link.dismiss({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [BLOG_MODULE]: {
    post_id: "mc_123",
  },
})
```

Learn more in [this documentation](!docs!/learn/fundamentals/module-links/link#dismiss-link).

### Cascade Delete Linked Records

To cascade delete records linked to a deleted record:

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await productModuleService.deleteVariants([variant.id])

await link.delete({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
})
```

Learn more in [this documentation](!docs!/learn/fundamentals/module-links/link#cascade-delete-linked-records).

### Restore Linked Records

To restore records that were soft-deleted because they were linked to a soft-deleted record:

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await productModuleService.restoreProducts(["prod_123"])

await link.restore({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
})
```

Learn more in [this documentation](!docs!/learn/fundamentals/module-links/link#restore-linked-records).

---

## Query

Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key.

### Retrieve Records of Data Model

To retrieve records using Query in an API route:

```ts highlights={[["15", "graph"]]}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: myCustoms } = await query.graph({
    entity: "my_custom",
    fields: ["id", "name"],
  })

  res.json({ my_customs: myCustoms })
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/module-links/query).

### Retrieve Linked Records of Data Model

To retrieve records linked to a data model:

```ts highlights={[["20"]]}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: myCustoms } = await query.graph({
    entity: "my_custom",
    fields: [
      "id", 
      "name",
      "product.*",
    ],
  })

  res.json({ my_customs: myCustoms })
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/module-links/query#retrieve-linked-records).

### Apply Filters to Retrieved Records

To filter the retrieved records:

```ts highlights={[["18", "filters"]]}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: myCustoms } = await query.graph({
    entity: "my_custom",
    fields: ["id", "name"],
    filters: {
      id: [
        "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",
        "mc_01HWSVWK3KYHKQEE6QGS2JC3FX",
      ],
    },
  })

  res.json({ my_customs: myCustoms })
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/module-links/query#apply-filters).

### Apply Pagination and Sort Records

To paginate and sort retrieved records:

```ts highlights={[["21", "pagination"]]}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { 
    data: myCustoms,
    metadata: { count, take, skip } = {},
  } = await query.graph({
    entity: "my_custom",
    fields: ["id", "name"],
    pagination: {
      skip: 0,
      take: 10,
      order: {
        name: "DESC",
      },
    },
  })

  res.json({ 
    my_customs: myCustoms,
    count,
    take,
    skip,
  })
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/module-links/query#sort-records).

---

## Workflows

A workflow is a series of queries and actions that complete a task.

A workflow allows you to track its execution's progress, provide roll-back logic for each step to mitigate data inconsistency when errors occur, automatically retry failing steps, and more.

### Create a Workflow

To create a workflow:

1. Create the first step at `src/workflows/hello-world/steps/step-1.ts` with the following content:

```ts title="src/workflows/hello-world/steps/step-1.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

export const step1 = createStep("step-1", async () => {
  return new StepResponse(`Hello from step one!`)
})
```

2. Create the second step at `src/workflows/hello-world/steps/step-2.ts` with the following content:

```ts title="src/workflows/hello-world/steps/step-2.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

type StepInput = {
  name: string
}

export const step2 = createStep(
  "step-2", 
  async ({ name }: StepInput) => {
    return new StepResponse(`Hello ${name} from step two!`)
  }
)
```

3. Create the workflow at `src/workflows/hello-world/index.ts` with the following content:

```ts title="src/workflows/hello-world/index.ts"
import {
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { step1 } from "./steps/step-1"
import { step2 } from "./steps/step-2"

const myWorkflow = createWorkflow(
  "hello-world",
  function (input: WorkflowInput) {
    const str1 = step1()
    // to pass input
    const str2 = step2(input)

    return new WorkflowResponse({
      message: str1,
    })
  }
)

export default myWorkflow
```

Learn more in [this documentation](!docs!/learn/fundamentals/workflows).

### Execute a Workflow

<CodeTabs group="resource-types">
  <CodeTab label="API Route" value="api-route">
    
    ```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
    import type {
      MedusaRequest,
      MedusaResponse,
    } from "@medusajs/framework/http"
    import myWorkflow from "../../workflows/hello-world"

    export async function GET(
      req: MedusaRequest,
      res: MedusaResponse
    ) {
      const { result } = await myWorkflow(req.scope)
        .run({
          input: {
            name: req.query.name as string,
          },
        })

      res.send(result)
    }
    ```

  </CodeTab>
  <CodeTab label="Subscriber" value="subscriber">
    
    ```ts title="src/subscribers/customer-created.ts" highlights={[["20"], ["21"], ["22"], ["23"], ["24"], ["25"]]} collapsibleLines="1-9" expandButtonLabel="Show Imports"
    import {
      type SubscriberConfig,
      type SubscriberArgs,
    } from "@medusajs/framework"
    import myWorkflow from "../workflows/hello-world"
    import { Modules } from "@medusajs/framework/utils"
    import { IUserModuleService } from "@medusajs/framework/types"

    export default async function handleCustomerCreate({
      event: { data },
      container,
    }: SubscriberArgs<{ id: string }>) {
      const userId = data.id
      const userModuleService: IUserModuleService = container.resolve(
        Modules.USER
      )

      const user = await userModuleService.retrieveUser(userId)

      const { result } = await myWorkflow(container)
        .run({
          input: {
            name: user.first_name,
          },
        })

      console.log(result)
    }

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

  </CodeTab>
  <CodeTab label="Scheduled Job" value="scheduled-job">
    
    ```ts title="src/jobs/message-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"], ["11"], ["12"]]}
    import { MedusaContainer } from "@medusajs/framework/types"
    import myWorkflow from "../workflows/hello-world"

    export default async function myCustomJob(
      container: MedusaContainer
    ) {
      const { result } = await myWorkflow(container)
        .run({
          input: {
            name: "John",
          },
        })

      console.log(result.message)
    }

    export const config = {
      name: "run-once-a-day",
      schedule: `0 0 * * *`,
    };
    ```

  </CodeTab>
</CodeTabs>

Learn more in [this documentation](!docs!/learn/fundamentals/workflows#3-execute-the-workflow).

### Step with a Compensation Function

Pass a compensation function that undoes what a step did as a second parameter to `createStep`:

```ts highlights={[["15"]]}
import { 
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async () => {
    const message = `Hello from step one!`

    console.log(message)

    return new StepResponse(message)
  },
  async () => {
    console.log("Oops! Rolling back my changes...")
  }
)
```

Learn more in [this documentation](!docs!/learn/fundamentals/workflows/compensation-function).

### Manipulate Variables in Workflow

To manipulate variables within a workflow's constructor function, use `transform` from the Workflows SDK:

```ts highlights={[["14", "transform"]]}
import { 
  createWorkflow,
  WorkflowResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"
// step imports...

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
    const str1 = step1(input)
    const str2 = step2(input)

    const str3 = transform(
      { str1, str2 },
      (data) => `${data.str1}${data.str2}`
    )

    return new WorkflowResponse(str3)
  }
)
```

Learn more in [this documentation](!docs!/learn/fundamentals/workflows/variable-manipulation)

### Using Conditions in Workflow

To perform steps or set a variable's value based on a condition, use `when-then` from the Workflows SDK:

```ts highlights={[["14", "when"]]}
import { 
  createWorkflow,
  WorkflowResponse,
  when,
} from "@medusajs/framework/workflows-sdk"
// step imports...

const workflow = createWorkflow(
  "workflow", 
  function (input: {
    is_active: boolean
  }) {

    const result = when(
      input, 
      (input) => {
        return input.is_active
      }
    ).then(() => {
      return isActiveStep()
    })

    // executed without condition
    const anotherStepResult = anotherStep(result)

    return new WorkflowResponse(
      anotherStepResult
    )
  }
)
```

### Run Workflow in Another

To run a workflow in another, use the workflow's `runAsStep` special method:

```ts highlights={[["11", "runAsStep"]]}
import {
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { 
  createProductsWorkflow,
} from "@medusajs/medusa/core-flows"

const workflow = createWorkflow(
  "hello-world",
  async (input) => {
    const products = createProductsWorkflow.runAsStep({
      input: {
        products: [
          // ...
        ],
      },
    })

    // ...
  }
)
```

Learn more in [this documentation](!docs!/learn/fundamentals/workflows/execute-another-workflow).

### Consume a Workflow Hook

To consume a workflow hook, create a file under `src/workflows/hooks`:

```ts title="src/workflows/hooks/product-created.ts"
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"

createProductsWorkflow.hooks.productsCreated(
  async ({ products, additional_data }, { container }) => {
    // TODO perform an action
  },
  async (dataFromStep, { container }) => {
    // undo the performed action
  }
)
```

This executes a custom step at the hook's designated point in the workflow.

Learn more in [this documentation](!docs!/learn/fundamentals/workflows/workflow-hooks).

### Expose a Hook

To expose a hook in a workflow, pass it in the second parameter of the returned `WorkflowResponse`:

```ts highlights={[["19", "hooks"]]}
import {
  createStep,
  createHook,
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { createProductStep } from "./steps/create-product"

export const myWorkflow = createWorkflow(
  "my-workflow", 
  function (input) {
    const product = createProductStep(input)
    const productCreatedHook = createHook(
      "productCreated", 
      { productId: product.id }
    )

    return new WorkflowResponse(product, {
      hooks: [productCreatedHook],
    })
  }
)
```

Learn more in [this documentation](!book!/learn/fundamentals/workflows/add-workflow-hook).

### Retry Steps

To configure steps to retry in case of errors, pass the `maxRetries` step option:

```ts highlights={[["10"]]}
import { 
  createStep, 
} from "@medusajs/framework/workflows-sdk"

export const step1 = createStep(
  {
    name: "step-1",
    maxRetries: 2,
  },
  async () => {
    console.log("Executing step 1")

    throw new Error("Oops! Something happened.")
  }
)
```

Learn more in [this documentation](!docs!/learn/fundamentals/workflows/retry-failed-steps).

### Run Steps in Parallel

If steps in a workflow don't depend on one another, run them in parallel using `parallel` from the Workflows SDK:

```ts highlights={[["22", "parallelize"]]}
import {
  createWorkflow,
  WorkflowResponse,
  parallelize,
} from "@medusajs/framework/workflows-sdk"
import {
  createProductStep,
  getProductStep,
  createPricesStep,
  attachProductToSalesChannelStep,
} from "./steps"

interface WorkflowInput {
  title: string
}

const myWorkflow = createWorkflow(
  "my-workflow", 
  (input: WorkflowInput) => {
   const product = createProductStep(input)

   const [prices, productSalesChannel] = parallelize(
     createPricesStep(product),
     attachProductToSalesChannelStep(product)
   )

   const id = product.id
   const refetchedProduct = getProductStep(product.id)

   return new WorkflowResponse(refetchedProduct)
 }
)
```

Learn more in [this documentation](!docs!/learn/fundamentals/workflows/parallel-steps).

### Configure Workflow Timeout

To configure the timeout of a workflow, at which the workflow's status is changed, but its execution isn't stopped, use the `timeout` configuration:

```ts highlights={[["10"]]}
import { 
  createStep,  
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
// step import...

const myWorkflow = createWorkflow({
  name: "hello-world",
  timeout: 2, // 2 seconds
}, function () {
  const str1 = step1()

  return new WorkflowResponse({
    message: str1,
  })
})

export default myWorkflow
```

Learn more in [this documentation](!docs!/learn/fundamentals/workflows/workflow-timeout).

### Configure Step Timeout

To configure a step's timeout, at which its state changes but its execution isn't stopped, use the `timeout` property:

```ts highlights={[["4"]]}
const step1 = createStep(
  {
    name: "step-1",
    timeout: 2, // 2 seconds
  },
  async () => {
    // ...
  }
)
```

Learn more in [this documentation](!docs!/learn/fundamentals/workflows/workflow-timeout#configure-step-timeout).

### Long-Running Workflow

A long-running workflow is a workflow that runs in the background. You can wait before executing some of its steps until another external or separate action occurs.

To create a long-running workflow, configure any of its steps to be `async` without returning any data:

```ts highlights={[["4"]]}
const step2 = createStep(
  {
    name: "step-2",
    async: true,
  },
  async () => {
    console.log("Waiting to be successful...")
  }
)
```

Learn more in [this documentation](!docs!/learn/fundamentals/workflows/long-running-workflow).

### Change Step Status in Long-Running Workflow

To change a step's status:

1. Grab the workflow's transaction ID when you run it:

```ts
const { transaction } = await myLongRunningWorkflow(req.scope)
  .run()
```

2. In an API route, workflow, or other resource, change a step's status to successful using the [Worfklow Engine Module](../architectural-modules/workflow-engine/page.mdx):

export const stepSuccessHighlights = [
  ["5", "setStepSuccess", "Change a step's status to success"],
  ["8", "transactionId", "Pass the workflow's transaction ID"],
  ["9", "stepId", "The ID of the step to change its status."],
  ["10", "workflowId", "The ID of the workflow that the step belongs to."]
]

```ts highlights={stepSuccessHighlights}
const workflowEngineService = container.resolve(
  Modules.WORKFLOW_ENGINE
)

await workflowEngineService.setStepSuccess({
  idempotencyKey: {
    action: TransactionHandlerType.INVOKE,
    transactionId,
    stepId: "step-2",
    workflowId: "hello-world",
  },
  stepResponse: new StepResponse("Done!"),
  options: {
    container,
  },
})
```

3. In an API route, workflow, or other resource, change a step's status to failure using the [Worfklow Engine Module](../architectural-modules/workflow-engine/page.mdx):

export const stepFailureHighlights = [
  ["5", "setStepFailure", "Change a step's status to failure"],
  ["8", "transactionId", "Pass the workflow's transaction ID"],
  ["9", "stepId", "The ID of the step to change its status."],
  ["10", "workflowId", "The ID of the workflow that the step belongs to."]
]

```ts highlights={stepFailureHighlights}
const workflowEngineService = container.resolve(
  Modules.WORKFLOW_ENGINE
)

await workflowEngineService.setStepFailure({
  idempotencyKey: {
    action: TransactionHandlerType.INVOKE,
    transactionId,
    stepId: "step-2",
    workflowId: "hello-world",
  },
  stepResponse: new StepResponse("Failed!"),
  options: {
    container,
  },
})
```

Learn more in [this documentation](!docs!/learn/fundamentals/workflows/long-running-workflow).

### Access Long-Running Workflow's Result

Use the Workflow Engine Module's `subscribe` and `unsubscribe` methods to access the status of a long-running workflow.

For example, in an API route:

```ts highlights={[["18", "subscribe", "Subscribe to the workflow's status changes."]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import myWorkflow from "../../../workflows/hello-world"
import { Modules } from "@medusajs/framework/utils"

export async function GET(req: MedusaRequest, res: MedusaResponse) {
  const { transaction, result } = await myWorkflow(req.scope).run()

  const workflowEngineService = req.scope.resolve(
    Modules.WORKFLOW_ENGINE
  )

  const subscriptionOptions = {
    workflowId: "hello-world",
    transactionId: transaction.transactionId,
    subscriberId: "hello-world-subscriber",
  }

  await workflowEngineService.subscribe({
    ...subscriptionOptions,
    subscriber: async (data) => {
      if (data.eventType === "onFinish") {
        console.log("Finished execution", data.result)
        // unsubscribe
        await workflowEngineService.unsubscribe({
          ...subscriptionOptions,
          subscriberOrId: subscriptionOptions.subscriberId,
        })
      } else if (data.eventType === "onStepFailure") {
        console.log("Workflow failed", data.step)
      }
    },
  })

  res.send(result)
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result).

---

## Subscribers

A subscriber is a function executed whenever the event it listens to is emitted.

### Create a Subscriber

To create a subscriber that listens to the `product.created` event, create the file `src/subscribers/product-created.ts` with the following content:

```ts title="src/subscribers/product-created.ts"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"

export default async function productCreateHandler({
  event,
}: SubscriberArgs<{ id: string }>) {
  const productId = event.data.id
  console.log(`The product ${productId} was created`)
}

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

Learn more in [this documentation](!docs!/learn/fundamentals/events-and-subscribers).

### Resolve Resources in Subscriber

To resolve resources from the Medusa container in a subscriber, use the `container` property of its parameter:

```ts highlights={[["6", "container"], ["8", "resolve", "Resolve the Product Module's main service."]]}
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"

export default async function productCreateHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const productModuleService = container.resolve(Modules.PRODUCT)

  const productId = data.id

  const product = await productModuleService.retrieveProduct(
    productId
  )

  console.log(`The product ${product.title} was created`)
}

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

Learn more in [this documentation](!docs!/learn/fundamentals/events-and-subscribers#resolve-resources).

### Send a Notification to Reset Password

To send a notification, such as an email when a user requests to reset their password, create a subscriber at `src/subscribers/handle-reset.ts` with the following content:

```ts title="src/subscribers/handle-reset.ts"
import {
  SubscriberArgs,
  type SubscriberConfig,
} from "@medusajs/medusa"
import { Modules } from "@medusajs/framework/utils"

export default async function resetPasswordTokenHandler({
  event: { data: {
    entity_id: email,
    token,
    actor_type,
  } },
  container,
}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) {
  const notificationModuleService = container.resolve(
    Modules.NOTIFICATION
  )

  const urlPrefix = actor_type === "customer" ? 
    "https://storefront.com" : 
    "https://admin.com"

  await notificationModuleService.createNotifications({
    to: email,
    channel: "email",
    template: "reset-password-template",
    data: {
      // a URL to a frontend application
      url: `${urlPrefix}/reset-password?token=${token}&email=${email}`,
    },
  })
}

export const config: SubscriberConfig = {
  event: "auth.password_reset",
}
```

Learn more in [this documentation](../commerce-modules/auth/reset-password/page.mdx).

### Execute a Workflow in a Subscriber

To execute a workflow in a subscriber:

```ts
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import myWorkflow from "../workflows/hello-world"
import { Modules } from "@medusajs/framework/utils"

export default async function handleCustomerCreate({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const userId = data.id
  const userModuleService = container.resolve(
    Modules.USER
  )

  const user = await userModuleService.retrieveUser(userId)

  const { result } = await myWorkflow(container)
    .run({
      input: {
        name: user.first_name,
      },
    })

  console.log(result)
}

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

Learn more in [this documentation](!docs!/learn/fundamentals/workflows#3-execute-the-workflow)

---

## Scheduled Jobs

A scheduled job is a function executed at a specified interval of time in the background of your Medusa application.

### Create a Scheduled Job

To create a scheduled job, create the file `src/jobs/hello-world.ts` with the following content:

```ts title="src/jobs/hello-world.ts"
// the scheduled-job function
export default function () {
  console.log("Time to say hello world!")
}

// the job's configurations
export const config = {
  name: "every-minute-message",
  // execute every minute
  schedule: "* * * * *",
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/scheduled-jobs).

### Resolve Resources in Scheduled Job

To resolve resources in a scheduled job, use the `container` accepted as a first parameter:

```ts highlights={[["5", "container"], ["7", "resolve", "Resolve the Product Module's main service."]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const productModuleService = container.resolve(Modules.PRODUCT)

  const [, count] = await productModuleService.listAndCountProducts()

  console.log(
    `Time to check products! You have ${count} product(s)`
  )
}

export const config = {
  name: "every-minute-message",
  // execute every minute
  schedule: "* * * * *",
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/scheduled-jobs#resolve-resources)

### Specify a Job's Execution Number

To limit the scheduled job's execution to a number of times during the Medusa application's runtime, use the `numberOfExecutions` configuration:

```ts highlights={[["9", "numberOfExecutions"]]}
export default async function myCustomJob() {
  console.log("I'll be executed three times only.")
}

export const config = {
  name: "hello-world",
  // execute every minute
  schedule: "* * * * *",
  numberOfExecutions: 3,
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/scheduled-jobs/execution-number).

### Execute a Workflow in a Scheduled Job

To execute a workflow in a scheduled job:

```ts
import { MedusaContainer } from "@medusajs/framework/types"
import myWorkflow from "../workflows/hello-world"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await myWorkflow(container)
    .run({
      input: {
        name: "John",
      },
    })

  console.log(result.message)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/workflows#3-execute-the-workflow)

---

## Loaders

A loader is a function defined in a module that's executed when the Medusa application starts.

### Create a Loader

To create a loader, add it to a module's `loaders` directory.

For example, create the file `src/modules/hello/loaders/hello-world.ts` with the following content:

```ts title="src/modules/hello/loaders/hello-world.ts"
export default async function helloWorldLoader() {
  console.log(
    "[HELLO MODULE] Just started the Medusa application!"
  )
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/modules/loaders).

### Resolve Resources in Loader

To resolve resources in a loader, use the `container` property of its first parameter:

```ts highlights={[["9", "container"], ["11", "resolve", "Resolve the Logger from the module's container."]]}
import {
  LoaderOptions,
} from "@medusajs/framework/types"
import { 
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export default async function helloWorldLoader({
  container,
}: LoaderOptions) {
  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)

  logger.info("[helloWorldLoader]: Hello, World!")
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/modules/container).

### Access Module Options

To access a module's options in its loader, use the `options` property of its first parameter:

```ts highlights={[["11", "options"]]}
import {
  LoaderOptions,
} from "@medusajs/framework/types"

// recommended to define type in another file
type ModuleOptions = {
  apiKey?: boolean
}

export default async function helloWorldLoader({
  options,
}: LoaderOptions<ModuleOptions>) {
  
  console.log(
    "[HELLO MODULE] Just started the Medusa application!",
    options
  )
}
```

Learn more in [this documentation](!docs!/learn/fundamentals/modules/options).

### Register Resources in the Module's Container

To register a resource in the Module's container using a loader, use the `container`'s `registerAdd` method:

```ts highlights={[["9", "registerAdd"]]}
import {
  LoaderOptions,
} from "@medusajs/framework/types"
import { asValue } from "awilix"

export default async function helloWorldLoader({
  container,
}: LoaderOptions) {
  container.registerAdd(
    "custom_data",
    asValue({
      test: true,
    })
  )
}
```

Where the first parameter of `registerAdd` is the name to register the resource under, and the second parameter is the resource to register.

---

## Admin Customizations

You can customize the Medusa Admin to inject widgets in existing pages, or create new pages using UI routes.

<Note>

For a list of components to use in the admin dashboard, refere to [this documentation](../admin-components/page.mdx).

</Note>

### Create Widget

A widget is a React component that can be injected into an existing page in the admin dashboard.

To create a widget in the admin dashboard, create the file `src/admin/widgets/products-widget.tsx` with the following content:

```tsx title="src/admin/widgets/products-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const ProductWidget = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Product Widget</Heading>
      </div>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.list.before",
})

export default ProductWidget
```

Learn more about widgets in [this documentation](!docs!/learn/fundamentals/admin/widgets).

### Receive Details Props in Widgets

Widgets created in a details page, such as widgets in the `product.details.before` injection zone, receive a prop of the data of the details page (for example, the product):

```tsx highlights={[["10", "data"]]}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"
import { 
  DetailWidgetProps, 
  AdminProduct,
} from "@medusajs/framework/types"

// The widget
const ProductWidget = ({ 
  data,
}: DetailWidgetProps<AdminProduct>) => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">
          Product Widget {data.title}
        </Heading>
      </div>
    </Container>
  )
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

Learn more in [this documentation](!docs!/learn/fundamentals/admin/widgets#detail-widget-props).

### Create a UI Route

A UI route is a React Component that adds a new page to your admin dashboard. The UI Route can be shown in the sidebar or added as a nested page.

To create a UI route in the admin dashboard, create the file `src/admin/routes/custom/page.tsx` with the following content:

```tsx title="src/admin/routes/custom/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"

const CustomPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">This is my custom route</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Custom Route",
  icon: ChatBubbleLeftRight,
})

export default CustomPage
```

This adds a new page at `localhost:9000/app/custom`.

Learn more in [this documentation](!docs!/learn/fundamentals/admin/ui-routes).

### Create Settings Page

To create a settings page, create a UI route under the `src/admin/routes/settings` directory.

For example, create the file `src/admin/routes/settings/custom/page.tsx` with the following content:

```tsx title="src/admin/routes/settings/custom/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const CustomSettingPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Custom Setting Page</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Custom",
})

export default CustomSettingPage
```

This adds a setting page at `localhost:9000/app/settings/custom`.

Learn more in [this documentation](!docs!/learn/fundamentals/admin/ui-routes#create-settings-page)

### Accept Path Parameters in UI Routes

To accept a path parameter in a UI route, name one of the directories in its path in the format `[param]`.

For example, create the file `src/admin/routes/custom/[id]/page.tsx` with the following content:

```tsx title="src/admin/routes/custom/[id]/page.tsx"
import { useParams } from "react-router-dom"
import { Container } from "@medusajs/ui"

const CustomPage = () => {
  const { id } = useParams()

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Passed ID: {id}</Heading>
      </div>
    </Container>
  )
}

export default CustomPage
```

This creates a UI route at `localhost:9000/app/custom/:id`, where `:id` is a path parameter.

Learn more in [this documentation](!docs!/learn/fundamentals/admin/ui-routes#path-parameters)

### Send Request to API Route

To send a request to custom API routes from the admin dashboard, use the Fetch API.

For example:

```tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "@medusajs/ui"
import { useEffect, useState } from "react"

const ProductWidget = () => {
  const [productsCount, setProductsCount] = useState(0)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    if (!loading) {
      return
    }

    fetch(`/admin/products`, {
      credentials: "include",
    })
    .then((res) => res.json())
    .then(({ count }) => {
      setProductsCount(count)
      setLoading(false)
    })
  }, [loading])

  return (
    <Container className="divide-y p-0">
      {loading && <span>Loading...</span>}
      {!loading && <span>You have {productsCount} Product(s).</span>}
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.list.before",
})

export default ProductWidget
```

Learn more in [this documentation](!docs!/learn/fundamentals/admin/tips#send-requests-to-api-routes)

### Add Link to Another Page

To add a link to another page in a UI route or a widget, use `react-router-dom`'s `Link` component:

```tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "@medusajs/ui"
import { Link } from "react-router-dom"

// The widget
const ProductWidget = () => {
  return (
    <Container className="divide-y p-0">
      <Link to={"/orders"}>View Orders</Link>
    </Container>
  )
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

Learn more in [this documentation](!docs!/learn/fundamentals/admin/tips#routing-functionalities).

---

## Integration Tests

Medusa provides a `@medusajs/test-utils` package with utility tools to create integration tests for your custom API routes, modules, or other Medusa customizations.

<Note>

For details on setting up your project for integration tests, refer to [this documentation](!docs!/learn/debugging-and-testing/testing-tools).

</Note>

### Test Custom API Route

To create a test for a custom API route, create the file `integration-tests/http/custom-routes.spec.ts` with the following content:

```ts title="integration-tests/http/custom-routes.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      describe("GET /custom", () => {
        it("returns correct message", async () => {
          const response = await api.get(
            `/custom`
          )
  
          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("message")
          expect(response.data.message).toEqual("Hello, World!")
        })
      })
    })
  },
})
```

Then, run the test with the following command:

```bash npm2yarn
npm run test:integration
```

Learn more in [this documentation](!docs!/learn/debugging-and-testing/testing-tools/integration-tests/api-routes).

### Test Workflow

To create a test for a workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content:

```ts title="integration-tests/http/workflow.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { helloWorldWorkflow } from "../../src/workflows/hello-world"

medusaIntegrationTestRunner({
  testSuite: ({ getContainer }) => {
    describe("Test hello-world workflow", () => {
      it("returns message", async () => {
        const { result } = await helloWorldWorkflow(getContainer())
          .run()

        expect(result).toEqual("Hello, World!")
      })
    })
  },
})
```

Then, run the test with the following command:

```bash npm2yarn
npm run test:integration
```

Learn more in [this documentation](!docs!/learn/debugging-and-testing/testing-tools/integration-tests/workflows).

### Test Module's Service

To create a test for a module's service, create the test under the `__tests__` directory of the module.

For example, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content:

```ts title="src/modules/blog/__tests__/service.spec.ts"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { BLOG_MODULE } from ".."
import BlogModuleService from "../service"
import Post from "../models/post"

moduleIntegrationTestRunner<BlogModuleService>({
  moduleName: BLOG_MODULE,
  moduleModels: [Post],
  resolve: "./modules/blog",
  testSuite: ({ service }) => {
    describe("BlogModuleService", () => {
      it("says hello world", () => {
        const message = service.getMessage()

        expect(message).toEqual("Hello, World!")
      })
    })
  },
})
```

Then, run the test with the following command:

```bash npm2yarn
npm run test:modules
```

---

## Commerce Modules

Medusa provides all its commerce features as separate commerce modules, such as the Product or Order modules.

<Note>

Refer to the [Commerce Modules](../commerce-modules/page.mdx) documentation for concepts and reference of every module's main service.

</Note>

### Create an Actor Type to Authenticate

To create an actor type that can authenticate to the Medusa application, such as a `manager`:

1. Create the data model in a module:

```ts
import { model } from "@medusajs/framework/utils"

const Manager = model.define("manager", {
  id: model.id().primaryKey(),
  firstName: model.text(),
  lastName: model.text(),
  email: model.text(),
})

export default Manager
```

2. Use the `setAuthAppMetadataStep` as a step in a workflow that creates a manager:

```ts
import { 
  createWorkflow, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  setAuthAppMetadataStep,
} from "@medusajs/medusa/core-flows"
// other imports...

const createManagerWorkflow = createWorkflow(
  "create-manager",
  function (input: CreateManagerWorkflowInput) {
    const manager = createManagerStep({
      manager: input.manager,
    })

    setAuthAppMetadataStep({
      authIdentityId: input.authIdentityId,
      actorType: "manager",
      value: manager.id,
    })

    return new WorkflowResponse(manager)
  }
)
```

3. Use the workflow in an API route that creates a user (manager) of the actor type:

```ts
import type { 
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import createManagerWorkflow from "../../workflows/create-manager"

type RequestBody = {
  first_name: string
  last_name: string
  email: string
}

export async function POST(
  req: AuthenticatedMedusaRequest<RequestBody>, 
  res: MedusaResponse
) {
  // If `actor_id` is present, the request carries 
  // authentication for an existing manager
  if (req.auth_context.actor_id) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Request already authenticated as a manager."
    )
  }

  const { result } = await createManagerWorkflow(req.scope)
    .run({
      input: {
        manager: req.body,
        authIdentityId: req.auth_context.auth_identity_id,
      },
    })
  
    res.status(200).json({ manager: result })
}
```

4. Apply the `authenticate` middleware on the new route in `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/manager",
      method: "POST",
      middlewares: [
        authenticate("manager", ["session", "bearer"], {
          allowUnregistered: true,
        }),
      ],
    },
    {
      matcher: "/manager/me*",
      middlewares: [
        authenticate("manager", ["session", "bearer"]),
      ],
    },
  ],
})
```

Now, manager users can use the `/manager` API route to register, and all routes starting with `/manager/me` are only accessible by authenticated managers.

Find an elaborate example and learn more in [this documentation](../commerce-modules/auth/create-actor-type/page.mdx).

### Apply Promotion on Cart Items and Shipping

To apply a promotion on a cart's items and shipping methods using the [Cart](../commerce-modules/cart/page.mdx) and [Promotion](../commerce-modules/promotion/page.mdx) modules:

```ts
import {
  ComputeActionAdjustmentLine,
  ComputeActionItemLine,
  ComputeActionShippingLine,
  AddItemAdjustmentAction,
  AddShippingMethodAdjustment,
  // ...
} from "@medusajs/framework/types"

// retrieve the cart
const cart = await cartModuleService.retrieveCart("cart_123", {
  relations: [
    "items.adjustments",
    "shipping_methods.adjustments",
  ],
})

// retrieve line item adjustments
const lineItemAdjustments: ComputeActionItemLine[] = []
cart.items.forEach((item) => {
  const filteredAdjustments = item.adjustments?.filter(
    (adjustment) => adjustment.code !== undefined
  ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    lineItemAdjustments.push({
      ...item,
      adjustments: filteredAdjustments,
    })
  }
})

// retrieve shipping method adjustments
const shippingMethodAdjustments: ComputeActionShippingLine[] =
  []
cart.shipping_methods.forEach((shippingMethod) => {
  const filteredAdjustments =
    shippingMethod.adjustments?.filter(
      (adjustment) => adjustment.code !== undefined
    ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    shippingMethodAdjustments.push({
      ...shippingMethod,
      adjustments: filteredAdjustments,
    })
  }
})

// compute actions
const actions = await promotionModuleService.computeActions(
  ["promo_123"],
  {
    items: lineItemAdjustments,
    shipping_methods: shippingMethodAdjustments,
  }
)

// set the adjustments on the line item
await cartModuleService.setLineItemAdjustments(
  cart.id,
  actions.filter(
    (action) => action.action === "addItemAdjustment"
  ) as AddItemAdjustmentAction[]
)

// set the adjustments on the shipping method
await cartModuleService.setShippingMethodAdjustments(
  cart.id,
  actions.filter(
    (action) =>
      action.action === "addShippingMethodAdjustment"
  ) as AddShippingMethodAdjustment[]
)
```

Learn more in [this documentation](../commerce-modules/cart/tax-lines/page.mdx).

### Retrieve Tax Lines of a Cart's Items and Shipping

To retrieve the tax lines of a cart's items and shipping methods using the [Cart](../commerce-modules/cart/page.mdx) and [Tax](../commerce-modules/tax/page.mdx) modules:

```ts
// retrieve the cart
const cart = await cartModuleService.retrieveCart("cart_123", {
  relations: [
    "items.tax_lines",
    "shipping_methods.tax_lines",
    "shipping_address",
  ],
})

// retrieve the tax lines
const taxLines = await taxModuleService.getTaxLines(
  [
    ...(cart.items as TaxableItemDTO[]),
    ...(cart.shipping_methods as TaxableShippingDTO[]),
  ],
  {
    address: {
      ...cart.shipping_address,
      country_code:
        cart.shipping_address.country_code || "us",
    },
  }
)

// set line item tax lines
await cartModuleService.setLineItemTaxLines(
  cart.id,
  taxLines.filter((line) => "line_item_id" in line)
)

// set shipping method tax lines
await cartModuleService.setLineItemTaxLines(
  cart.id,
  taxLines.filter((line) => "shipping_line_id" in line)
)
```

Learn more in [this documentation](../commerce-modules/cart/tax-lines/page.mdx)

### Apply Promotion on an Order's Items and Shipping

To apply a promotion on an order's items and shipping methods using the [Order](../commerce-modules/order/page.mdx) and [Promotion](../commerce-modules/promotion/page.mdx) modules:

```ts
import {
  ComputeActionAdjustmentLine,
  ComputeActionItemLine,
  ComputeActionShippingLine,
  AddItemAdjustmentAction,
  AddShippingMethodAdjustment,
  // ...
} from "@medusajs/framework/types"

// ...

// retrieve the order
const order = await orderModuleService.retrieveOrder("ord_123", {
  relations: [
    "items.item.adjustments",
    "shipping_methods.shipping_method.adjustments",
  ],
})
// retrieve the line item adjustments
const lineItemAdjustments: ComputeActionItemLine[] = []
order.items.forEach((item) => {
  const filteredAdjustments = item.adjustments?.filter(
    (adjustment) => adjustment.code !== undefined
  ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    lineItemAdjustments.push({
      ...item,
      ...item.detail,
      adjustments: filteredAdjustments,
    })
  }
})

//retrieve shipping method adjustments
const shippingMethodAdjustments: ComputeActionShippingLine[] =
  []
order.shipping_methods.forEach((shippingMethod) => {
  const filteredAdjustments =
    shippingMethod.adjustments?.filter(
      (adjustment) => adjustment.code !== undefined
    ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    shippingMethodAdjustments.push({
      ...shippingMethod,
      adjustments: filteredAdjustments,
    })
  }
})

// compute actions
const actions = await promotionModuleService.computeActions(
  ["promo_123"],
  {
    items: lineItemAdjustments,
    shipping_methods: shippingMethodAdjustments,
    // TODO infer from cart or region
    currency_code: "usd",
  }
)

// set the adjustments on the line items
await orderModuleService.setOrderLineItemAdjustments(
  order.id,
  actions.filter(
    (action) => action.action === "addItemAdjustment"
  ) as AddItemAdjustmentAction[]
)

// set the adjustments on the shipping methods
await orderModuleService.setOrderShippingMethodAdjustments(
  order.id,
  actions.filter(
    (action) =>
      action.action === "addShippingMethodAdjustment"
  ) as AddShippingMethodAdjustment[]
)
```

Learn more in [this documentation](../commerce-modules/order/promotion-adjustments/page.mdx)

### Accept Payment using Module

To accept payment using the Payment Module's main service:

1. Create a payment collection and link it to the cart:

```ts
import { 
  ContainerRegistrationKeys,
  Modules,
} from "@medusajs/framework/utils"

// ...

const paymentCollection =
  await paymentModuleService.createPaymentCollections({
    region_id: "reg_123",
    currency_code: "usd",
    amount: 5000,
  })

// resolve Link
const link = container.resolve(
  ContainerRegistrationKeys.LINK
)

// create a link between the cart and payment collection
link.create({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: paymentCollection.id,
  },
})
```

2. Create a payment session in the collection:

```ts
const paymentSession =
  await paymentModuleService.createPaymentSession(
    paymentCollection.id,
    {
      provider_id: "stripe",
      currency_code: "usd",
      amount: 5000,
      data: {
        // any necessary data for the
        // payment provider
      },
    }
  )
```

3. Authorize the payment session:

```ts
const payment =
  await paymentModuleService.authorizePaymentSession(
    paymentSession.id,
    {}
  )
```

Learn more in [this documentation](../commerce-modules/payment/payment-flow/page.mdx).

### Get Variant's Prices for Region and Currency

To get prices of a product variant for a region and currency using [Query](!docs!/learn/fundamentals/module-links/query):

```ts
import { QueryContext } from "@medusajs/framework/utils"

// ...

const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "*",
    "variants.*",
    "variants.calculated_price.*",
  ],
  filters: {
    id: "prod_123",
  },
  context: {
    variants: {
      calculated_price: QueryContext({
        region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS",
        currency_code: "eur",
      }),
    },
  },
})
```

Learn more in [this documentation](../commerce-modules/product/guides/price/page.mdx#retrieve-calculated-price-for-a-context).

### Get All Variant's Prices

To get all prices of a product variant using [Query](!docs!/learn/fundamentals/module-links/query):

```ts
const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "*",
    "variants.*",
    "variants.prices.*",
  ],
  filters: {
    id: [
      "prod_123",
    ],
  },
})
```

Learn more in [this documentation](../commerce-modules/product/guides/price/page.mdx).

### Get Variant Prices with Taxes

To get a variant's prices with taxes using [Query](!docs!/learn/fundamentals/module-links/query) and the [Tax Module](../commerce-modules/tax/page.mdx)

```ts
import {
  HttpTypes,
  TaxableItemDTO,
  ItemTaxLineDTO,
} from "@medusajs/framework/types"
import { 
  QueryContext,
  calculateAmountsWithTax,
} from "@medusajs/framework/utils"
// other imports...

// ...
const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => {
  return product.variants
    ?.map((variant) => {
      if (!variant.calculated_price) {
        return
      }

      return {
        id: variant.id,
        product_id: product.id,
        product_name: product.title,
        product_categories: product.categories?.map((c) => c.name),
        product_category_id: product.categories?.[0]?.id,
        product_sku: variant.sku,
        product_type: product.type,
        product_type_id: product.type_id,
        quantity: 1,
        unit_price: variant.calculated_price.calculated_amount,
        currency_code: variant.calculated_price.currency_code,
      }
    })
    .filter((v) => !!v) as unknown as TaxableItemDTO[]
}

const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "*",
    "variants.*",
    "variants.calculated_price.*",
  ],
  filters: {
    id: "prod_123",
  },
  context: {
    variants: {
      calculated_price: QueryContext({
        region_id: "region_123",
        currency_code: "usd",
      }),
    },
  },
})

const taxLines = (await taxModuleService.getTaxLines(
  products.map(asTaxItem).flat(),
  {
    // example of context properties. You can pass other ones.
    address: {
      country_code,
    },
  }
)) as unknown as ItemTaxLineDTO[]

const taxLinesMap = new Map<string, ItemTaxLineDTO[]>()
taxLines.forEach((taxLine) => {
  const variantId = taxLine.line_item_id
  if (!taxLinesMap.has(variantId)) {
    taxLinesMap.set(variantId, [])
  }

  taxLinesMap.get(variantId)?.push(taxLine)
})

products.forEach((product) => {
  product.variants?.forEach((variant) => {
    if (!variant.calculated_price) {
      return
    }

    const taxLinesForVariant = taxLinesMap.get(variant.id) || []
    const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({
      taxLines: taxLinesForVariant,
      amount: variant.calculated_price!.calculated_amount!,
      includesTax:
        variant.calculated_price!.is_calculated_price_tax_inclusive!,
    })

    // do something with prices...
  })
})
```

Learn more in [this documentation](../commerce-modules/product/guides/price-with-taxes/page.mdx).

### Invite Users

To invite a user using the [User Module](../commerce-modules/user/page.mdx):

```ts
const invite = await userModuleService.createInvites({
  email: "user@example.com",
})
```

### Accept User Invite

To accept an invite and create a user using the [User Module](../commerce-modules/user/page.mdx):

```ts
const invite =
  await userModuleService.validateInviteToken(inviteToken)

await userModuleService.updateInvites({
  id: invite.id,
  accepted: true,
})

const user = await userModuleService.createUsers({
  email: invite.email,
})
```
