Medusa Examples

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:

src/api/hello-world/route.ts
1import type {2  MedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5
6export const GET = (7  req: MedusaRequest,8  res: MedusaResponse9) => {10  res.json({11    message: "[GET] Hello world!",12  })13}

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

Learn more in this documentation.

Resolve Resources in API Route#

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

Code
1import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"2import { Modules } from "@medusajs/framework/utils"3
4export const GET = async (5  req: MedusaRequest, 6  res: MedusaResponse7) => {8  const productModuleService = req.scope.resolve(9    Modules.PRODUCT10  )11
12  const [, count] = await productModuleService13    .listAndCountProducts()14
15  res.json({16    count,17  })18}

This resolves the Product Module's main service.

Learn more in this documentation.

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:

src/api/hello-world/[id]/route.ts
1import type {2  MedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5
6export const GET = async (7  req: MedusaRequest,8  res: MedusaResponse9) => {10  res.json({11    message: `[GET] Hello ${req.params.id}!`,12  })13}

Learn more about path parameters in this documentation.

Use Query Parameters#

API routes can accept query parameters:

Code
1import type {2  MedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5
6export const GET = async (7  req: MedusaRequest,8  res: MedusaResponse9) => {10  res.json({11    message: `Hello ${req.query.name}`,12  })13}

Learn more about query parameters in this documentation.

Use Body Parameters#

API routes can accept request body parameters:

Code
1import type {2  MedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5
6type HelloWorldReq = {7  name: string8}9
10export const POST = async (11  req: MedusaRequest<HelloWorldReq>,12  res: MedusaResponse13) => {14  res.json({15    message: `[POST] Hello ${req.body.name}!`,16  })17}

Learn more about request body parameters in this documentation.

Set Response Code#

You can change the response code of an API route:

Code
1import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"2
3export const GET = async (4  req: MedusaRequest,5  res: MedusaResponse6) => {7  res.status(201).json({8    message: "Hello, World!",9  })10}

Learn more about setting the response code in this documentation.

Execute a Workflow in an API Route#

To execute a workflow in an API route:

Code
1import type {2  MedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5import myWorkflow from "../../workflows/hello-world"6
7export async function GET(8  req: MedusaRequest,9  res: MedusaResponse10) {11  const { result } = await myWorkflow(req.scope)12    .run({13      input: {14        name: req.query.name as string,15      },16    })17
18  res.send(result)19}

Learn more in this documentation.

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:

Code
1import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"2
3export const GET = async (4  req: MedusaRequest,5  res: MedusaResponse6) => {7  res.writeHead(200, {8    "Content-Type": "text/event-stream",9    "Cache-Control": "no-cache",10    Connection: "keep-alive",11  })12
13  const interval = setInterval(() => {14    res.write("Streaming data...\n")15  }, 3000)16
17  req.on("end", () => {18    clearInterval(interval)19    res.end()20  })21}

This changes the response type to return an event stream.

Learn more in this documentation.

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:

src/api/middlewares.ts
1import { defineMiddlewares } from "@medusajs/medusa"2import type { 3  MedusaNextFunction, 4  MedusaRequest, 5  MedusaResponse, 6} from "@medusajs/framework/http"7
8export default defineMiddlewares({9  routes: [10    {11      matcher: "/custom*",12      middlewares: [13        (14          req: MedusaRequest, 15          res: MedusaResponse, 16          next: MedusaNextFunction17        ) => {18          console.log("Received a request!")19
20          next()21        },22      ],23    },24    {25      matcher: "/custom/:id",26      middlewares: [27        (28          req: MedusaRequest, 29          res: MedusaResponse, 30          next: MedusaNextFunction31        ) => {32          console.log("With Path Parameter")33
34          next()35        },36      ],37    },38  ],39})

Learn more about middlewares in this documentation.

Restrict HTTP Methods in Middleware#

To restrict a middleware to an HTTP method:

src/api/middlewares.ts
1import { defineMiddlewares } from "@medusajs/medusa"2import type { 3  MedusaNextFunction, 4  MedusaRequest, 5  MedusaResponse, 6} from "@medusajs/framework/http"7
8export default defineMiddlewares({9  routes: [10    {11      matcher: "/custom*",12      method: ["POST", "PUT"],13      middlewares: [14        // ...15      ],16    },17  ],18})

Add Validation for Custom Routes#

  1. Create a Zod schema in the file src/api/custom/validators.ts:
src/api/custom/validators.ts
1import { z } from "zod"2
3export const PostStoreCustomSchema = z.object({4  a: z.number(),5  b: z.number(),6})
  1. Add a validation middleware to the custom route in src/api/middlewares.ts:
src/api/middlewares.ts
1import { defineMiddlewares } from "@medusajs/medusa"2import { validateAndTransformBody } from "@medusajs/framework/utils"3import { PostStoreCustomSchema } from "./custom/validators"4
5export default defineMiddlewares({6  routes: [7    {8      matcher: "/custom",9      method: "POST",10      middlewares: [11        validateAndTransformBody(PostStoreCustomSchema),12      ],13    },14  ],15})
  1. Use the validated body in the /custom API route:
src/api/custom/route.ts
1import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"2import { z } from "zod"3import { PostStoreCustomSchema } from "./validators"4
5type PostStoreCustomSchemaType = z.infer<6  typeof PostStoreCustomSchema7>8
9export const POST = async (10  req: MedusaRequest<PostStoreCustomSchemaType>,11  res: MedusaResponse12) => {13  res.json({14    sum: req.validatedBody.a + req.validatedBody.b,15  })16}

Learn more about request body validation in this documentation.

Pass Additional Data to API Route#

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

NoteFind this example in details in this documentation .
  1. Create the file src/api/middlewares.ts with the following content:
src/api/middlewares.ts
1import { defineMiddlewares } from "@medusajs/medusa"2import { z } from "zod"3
4export default defineMiddlewares({5  routes: [6    {7      matcher: "/admin/products",8      method: ["POST"],9      additionalDataValidator: {10        brand_id: z.string().optional(),11      },12    },13  ],14})
NoteLearn more about additional data in this documentation .
  1. Create the file src/workflows/hooks/created-product.ts with the following content:
Code
1import { createProductsWorkflow } from "@medusajs/medusa/core-flows"2import { StepResponse } from "@medusajs/framework/workflows-sdk"3
4createProductsWorkflow.hooks.productsCreated(5  (async ({ products, additional_data }, { container }) => {6    if (!additional_data.brand_id) {7      return new StepResponse([], [])8    }9
10    // TODO perform custom action11  }),12  (async (links, { container }) => {13    // TODO undo the action in the compensation14  })15
16)
NoteLearn more about workflow hooks in this documentation .

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:

src/api/middlewares.ts
1import { 2  defineMiddlewares,3  authenticate,4} from "@medusajs/medusa"5
6export default defineMiddlewares({7  routes: [8    {9      matcher: "/custom/admin*",10      middlewares: [11        authenticate(12          "user", 13          ["session", "bearer", "api-key"]14        )15      ],16    },17  ],18})

Learn more in this documentation.

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:

src/api/middlewares.ts
1import { 2  defineMiddlewares,3  authenticate,4} from "@medusajs/medusa"5
6export default defineMiddlewares({7  routes: [8    {9      matcher: "/custom/customer*",10      middlewares: [11        authenticate("customer", ["session", "bearer"])12      ],13    },14  ],15})

Learn more in this documentation.

Retrieve Logged-In Admin User#

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

NoteRequires setting up the authentication middleware as explained in this example .
Code
1import type {2  AuthenticatedMedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5import { Modules } from "@medusajs/framework/utils"6
7export const GET = async (8  req: AuthenticatedMedusaRequest,9  res: MedusaResponse10) => {11  const userModuleService = req.scope.resolve(12    Modules.USER13  )14
15  const user = await userModuleService.retrieveUser(16    req.auth_context.actor_id17  )18
19  // ...20}

Learn more in this documentation.

Retrieve Logged-In Customer#

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

NoteRequires setting up the authentication middleware as explained in this example .
Code
1import type {2  AuthenticatedMedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5import { Modules } from "@medusajs/framework/utils"6
7export const GET = async (8  req: AuthenticatedMedusaRequest,9  res: MedusaResponse10) => {11  if (req.auth_context?.actor_id) {12    // retrieve customer13    const customerModuleService = req.scope.resolve(14      Modules.CUSTOMER15    )16
17    const customer = await customerModuleService.retrieveCustomer(18      req.auth_context.actor_id19    )20  }21
22  // ...23}

Learn more in this documentation.

Throw Errors in API Route#

To throw errors in an API route, use the MedusaError utility:

Code
1import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"2import { MedusaError } from "@medusajs/framework/utils"3
4export const GET = async (5  req: MedusaRequest,6  res: MedusaResponse7) => {8  if (!req.query.q) {9    throw new MedusaError(10      MedusaError.Types.INVALID_DATA,11      "The `q` query parameter is required."12    )13  }14
15  // ...16}

Learn more in this documentation.

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:

src/api/middlewares.ts
1import { 2  defineMiddlewares, 3  MedusaNextFunction, 4  MedusaRequest, 5  MedusaResponse,6} from "@medusajs/framework/http"7import { MedusaError } from "@medusajs/framework/utils"8
9export default defineMiddlewares({10  errorHandler: (11    error: MedusaError | any, 12    req: MedusaRequest, 13    res: MedusaResponse, 14    next: MedusaNextFunction15  ) => {16    res.status(400).json({17      error: "Something happened.",18    })19  },20})

Learn more in this documentation,

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:

src/api/middlewares.ts
1import { defineMiddlewares } from "@medusajs/medusa"2import type { 3  MedusaNextFunction, 4  MedusaRequest, 5  MedusaResponse, 6} from "@medusajs/framework/http"7import { ConfigModule } from "@medusajs/framework/types"8import { parseCorsOrigins } from "@medusajs/framework/utils"9import cors from "cors"10
11export default defineMiddlewares({12  routes: [13    {14      matcher: "/custom*",15      middlewares: [16        (17          req: MedusaRequest, 18          res: MedusaResponse, 19          next: MedusaNextFunction20        ) => {21          const configModule: ConfigModule =22            req.scope.resolve("configModule")23
24          return cors({25            origin: parseCorsOrigins(26              configModule.projectConfig.http.storeCors27            ),28            credentials: true,29          })(req, res, next)30        },31      ],32    },33  ],34})

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:

src/api/middlewares.ts
1import { 2  defineMiddlewares, 3} from "@medusajs/framework/http"4
5export default defineMiddlewares({6  routes: [7    {8      matcher: "/webhooks/*",9      bodyParser: { preserveRawBody: true },10      method: ["POST"],11    }12  ]13})

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

src/api/webhooks/route.ts
1import type {2  MedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5
6export const POST = (7  req: MedusaRequest,8  res: MedusaResponse9) => {10  console.log(req.rawBody)11}

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#

NoteFind this example explained in details in this documentation .
  1. Create the directory src/modules/hello.
  2. Create the file src/modules/hello/models/my-custom.ts with the following data model:
src/modules/hello/models/my-custom.ts
1import { model } from "@medusajs/framework/utils"2
3const MyCustom = model.define("my_custom", {4  id: model.id().primaryKey(),5  name: model.text(),6})7
8export default MyCustom
  1. Create the file src/modules/hello/service.ts with the following service:
src/modules/hello/service.ts
1import { MedusaService } from "@medusajs/framework/utils"2import MyCustom from "./models/my-custom"3
4class HelloModuleService extends MedusaService({5  MyCustom,6}){7}8
9export default HelloModuleService
  1. Create the file src/modules/hello/index.ts that exports the module definition:
src/modules/hello/index.ts
1import HelloModuleService from "./service"2import { Module } from "@medusajs/framework/utils"3
4export const HELLO_MODULE = "helloModuleService"5
6export default Module(HELLO_MODULE, {7  service: HelloModuleService,8})
  1. Add the module to the configurations in medusa-config.ts:
medusa-config.ts
1module.exports = defineConfig({2  projectConfig: {3    // ...4  },5  modules: [6    {7      resolve: "./modules/hello",8    }9  ]10})
  1. Generate and run migrations:
Terminal
npx medusa db:generate helloModuleServicenpx medusa db:migrate
  1. Use the module's main service in an API route:
src/api/custom/route.ts
1import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"2import HelloModuleService from "../../modules/hello/service"3import { HELLO_MODULE } from "../../modules/hello"4
5export async function GET(6  req: MedusaRequest,7  res: MedusaResponse8): Promise<void> {9  const helloModuleService: HelloModuleService = req.scope.resolve(10    HELLO_MODULE11  )12
13  const my_custom = await helloModuleService.createMyCustoms({14    name: "test"15  })16
17  res.json({18    my_custom19  })20}

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/hello/services/custom.ts with the following content:

src/modules/hello/services/custom.ts
1export class CustomService {2  // TODO add methods3}

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

src/modules/hello/services/index.ts
export * from "./custom"

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

src/modules/hello/service.ts
1import { MedusaService } from "@medusajs/framework/utils"2import MyCustom from "./models/my-custom"3import { CustomService } from "./services"4
5type InjectedDependencies = {6  customService: CustomService7}8
9class HelloModuleService extends MedusaService({10  MyCustom,11}){12  private customService: CustomService13
14  constructor({ customService }: InjectedDependencies) {15    super(...arguments)16
17    this.customService = customService18  }19}20
21export default HelloModuleService

Learn more in this documentation.

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:
medusa-config.ts
1module.exports = defineConfig({2  // ...3  modules: [4    {5      resolve: "./modules/hello",6      options: {7        apiKey: true,8      },9    },10  ]11})
  1. Access the options in the module's main service:
src/modules/hello/service.ts
1import { MedusaService } from "@medusajs/framework/utils"2import MyCustom from "./models/my-custom"3
4// recommended to define type in another file5type ModuleOptions = {6  apiKey?: boolean7}8
9export default class HelloModuleService extends MedusaService({10  MyCustom,11}){12  protected options_: ModuleOptions13
14  constructor({}, options?: ModuleOptions) {15    super(...arguments)16
17    this.options_ = options || {18      apiKey: false,19    }20  }21
22  // ...23}

Learn more in this documentation.

Integrate Third-Party System in Module#

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

src/modules/hello/service.ts
1import { Logger } from "@medusajs/framework/types"2import { BRAND_MODULE } from ".."3
4export type ModuleOptions = {5  apiKey: string6}7
8type InjectedDependencies = {9  logger: Logger10}11
12export class BrandClient {13  private options_: ModuleOptions14  private logger_: Logger15
16  constructor(17    { logger }: InjectedDependencies, 18    options: ModuleOptions19  ) {20    this.logger_ = logger21    this.options_ = options22  }23
24  private async sendRequest(url: string, method: string, data?: any) {25    this.logger_.info(`Sending a ${26      method27    } request to ${url}. data: ${JSON.stringify(data, null, 2)}`)28    this.logger_.info(`Client Options: ${29      JSON.stringify(this.options_, null, 2)30    }`)31  }32}

Find a longer example of integrating a third-party service in this documentation.


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:

NoteThis assumes you already have a module. If not, follow this example .
  1. Create the file src/modules/hello/models/my-custom.ts with the following data model:
src/modules/hello/models/my-custom.ts
1import { model } from "@medusajs/framework/utils"2
3const MyCustom = model.define("my_custom", {4  id: model.id().primaryKey(),5  name: model.text(),6})7
8export default MyCustom
  1. Generate and run migrations:
Terminal
npx medusa db:generate helloModuleServicenpx medusa db:migrate

Learn more in this documentation.

Data Model Property Types#

A data model can have properties of the following types:

  1. ID property:
Code
1const MyCustom = model.define("my_custom", {2  id: model.id(),3  // ...4})
  1. Text property:
Code
1const MyCustom = model.define("my_custom", {2  name: model.text(),3  // ...4})
  1. Number property:
Code
1const MyCustom = model.define("my_custom", {2  age: model.number(),3  // ...4})
  1. Big Number property:
Code
1const MyCustom = model.define("my_custom", {2  price: model.bigNumber(),3  // ...4})
  1. Boolean property:
Code
1const MyCustom = model.define("my_custom", {2  hasAccount: model.boolean(),3  // ...4})
  1. Enum property:
Code
1const MyCustom = model.define("my_custom", {2  color: model.enum(["black", "white"]),3  // ...4})
  1. Date-Time property:
Code
1const MyCustom = model.define("my_custom", {2  date_of_birth: model.dateTime(),3  // ...4})
  1. JSON property:
Code
1const MyCustom = model.define("my_custom", {2  metadata: model.json(),3  // ...4})
  1. Array property:
Code
1const MyCustom = model.define("my_custom", {2  names: model.array(),3  // ...4})

Learn more in this documentation.

Set Primary Key#

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

Code
1import { model } from "@medusajs/framework/utils"2
3const MyCustom = model.define("my_custom", {4  id: model.id().primaryKey(),5  // ...6})7
8export default MyCustom

To set a text property as the primary key:

Code
1import { model } from "@medusajs/framework/utils"2
3const MyCustom = model.define("my_custom", {4  name: model.text().primaryKey(),5  // ...6})7
8export default MyCustom

To set a number property as the primary key:

Code
1import { model } from "@medusajs/framework/utils"2
3const MyCustom = model.define("my_custom", {4  age: model.number().primaryKey(),5  // ...6})7
8export default MyCustom

Learn more in this documentation.

Default Property Value#

To set the default value of a property:

Code
1import { model } from "@medusajs/framework/utils"2
3const MyCustom = model.define("my_custom", {4  color: model5    .enum(["black", "white"])6    .default("black"),7  age: model8    .number()9    .default(0),10  // ...11})12
13export default MyCustom

Learn more in this documentation.

Nullable Property#

To allow null values for a property:

Code
1import { model } from "@medusajs/framework/utils"2
3const MyCustom = model.define("my_custom", {4  price: model.bigNumber().nullable(),5  // ...6})7
8export default MyCustom

Learn more in this documentation.

Unique Property#

To create a unique index on a property:

Code
1import { model } from "@medusajs/framework/utils"2
3const User = model.define("user", {4  email: model.text().unique(),5  // ...6})7
8export default User

Learn more in this documentation.

Define Database Index on Property#

To define a database index on a property:

Code
1import { model } from "@medusajs/framework/utils"2
3const MyCustom = model.define("my_custom", {4  id: model.id().primaryKey(),5  name: model.text().index(6    "IDX_MY_CUSTOM_NAME"7  ),8})9
10export default MyCustom

Learn more in this documentation.

Define Composite Index on Data Model#

To define a composite index on a data model:

Code
1import { model } from "@medusajs/framework/utils"2
3const MyCustom = model.define("my_custom", {4  id: model.id().primaryKey(),5  name: model.text(),6  age: model.number().nullable(),7}).indexes([8  {9    on: ["name", "age"],10    where: {11      age: {12        $ne: null,13      },14    },15  },16])17
18export default MyCustom

Learn more in this documentation.

Make a Property Searchable#

To make a property searchable using terms or keywords:

Code
1import { model } from "@medusajs/framework/utils"2
3const MyCustom = model.define("my_custom", {4  name: model.text().searchable(),5  // ...6})7
8export default MyCustom

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

NotehelloModuleService is the main service that the data models belong to.
Code
1const myCustoms = await helloModuleService.listMyCustoms({2  q: "John",3})

Learn more in this documentation.

Create One-to-One Relationship#

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

Code
1import { model } from "@medusajs/framework/utils"2
3const User = model.define("user", {4  id: model.id().primaryKey(),5  email: model.hasOne(() => Email),6})7
8const Email = model.define("email", {9  id: model.id().primaryKey(),10  user: model.belongsTo(() => User, {11    mappedBy: "email",12  }),13})

Learn more in this documentation.

Create One-to-Many Relationship#

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

Code
1import { model } from "@medusajs/framework/utils"2
3const Store = model.define("store", {4  id: model.id().primaryKey(),5  products: model.hasMany(() => Product),6})7
8const Product = model.define("product", {9  id: model.id().primaryKey(),10  store: model.belongsTo(() => Store, {11    mappedBy: "products",12  }),13})

Learn more in this documentation.

Create Many-to-Many Relationship#

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

Code
1import { model } from "@medusajs/framework/utils"2
3const Order = model.define("order", {4  id: model.id().primaryKey(),5  products: model.manyToMany(() => Product, {6    mappedBy: "orders",7  }),8})9
10const Product = model.define("product", {11  id: model.id().primaryKey(),12  orders: model.manyToMany(() => Order, {13    mappedBy: "products",14  }),15})

Learn more in this documentation.

Configure Cascades of Data Model#

To configure cascade on a data model:

Code
1import { model } from "@medusajs/framework/utils"2// Product import3
4const Store = model.define("store", {5  id: model.id().primaryKey(),6  products: model.hasMany(() => Product),7})8.cascades({9  delete: ["products"],10})

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.

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:

NotehelloModuleService is the main service that the data models belong to.
Code
1// when creating an email2const email = await helloModuleService.createEmails({3  // other properties...4  user: "123",5})6
7// when updating an email8const email = await helloModuleService.updateEmails({9  id: "321",10  // other properties...11  user: "123",12})

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

Code
1// when creating a user2const user = await helloModuleService.createUsers({3  // other properties...4  email: "123",5})6
7// when updating a user8const user = await helloModuleService.updateUsers({9  id: "321",10  // other properties...11  email: "123",12})

Learn more in this documentation.

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:

NotehelloModuleService is the main service that the data models belong to.
Code
1// when creating a product2const product = await helloModuleService.createProducts({3  // other properties...4  store_id: "123",5})6
7// when updating a product8const product = await helloModuleService.updateProducts({9  id: "321",10  // other properties...11  store_id: "123",12})

Learn more in this documentation

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:

NotehelloModuleService is the main service that the data models belong to.
Code
1const product = await helloModuleService.createProducts({2  // other properties...3  orders: ["123", "321"],4})

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

Code
1const product = await helloModuleService.retrieveProduct(2  "123",3  {4    relations: ["orders"],5  }6)7
8const updatedProduct = await helloModuleService.updateProducts({9  id: product.id,10  // other properties...11  orders: [12    ...product.orders.map((order) => order.id),13    "321",14  ],15})

Learn more in this documentation.

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:

NotehelloModuleService is the main service that the data models belong to.
Code
1const product = await helloModuleService.retrieveProducts(2  "123",3  {4    relations: ["orders"],5  }6)

Learn more in this documentation.


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:

Code
1import { MedusaService } from "@medusajs/framework/utils"2import MyCustom from "./models/my-custom"3
4class HelloModuleService extends MedusaService({5  MyCustom,6}){7  // TODO implement custom methods8}9
10export default HelloModuleService

The HelloModuleService will now have data-management methods for MyCustom.

Refer to this reference for details on the generated methods.

Learn more about the service factory in this documentation.

Resolve Resources in the Service#

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

Learn more in this documentation.

Access Module Options in Service#

To access options passed to a module in its service:

Code
1import { MedusaService } from "@medusajs/framework/utils"2import MyCustom from "./models/my-custom"3
4// recommended to define type in another file5type ModuleOptions = {6  apiKey?: boolean7}8
9export default class HelloModuleService extends MedusaService({10  MyCustom,11}){12  protected options_: ModuleOptions13
14  constructor({}, options?: ModuleOptions) {15    super(...arguments)16
17    this.options_ = options || {18      apiKey: "",19    }20  }21
22  // ...23}

Learn more in this documentation.

Run Database Query in Service#

To run database query in your service:

Code
1// other imports...2import { 3  InjectManager,4  MedusaContext,5} from "@medusajs/framework/utils"6
7class HelloModuleService {8  // ...9
10  @InjectManager()11  async getCount(12    @MedusaContext() sharedContext?: Context<EntityManager>13  ): Promise<number> {14    return await sharedContext.manager.count("my_custom")15  }16  17  @InjectManager()18  async getCountSql(19    @MedusaContext() sharedContext?: Context<EntityManager>20  ): Promise<number> {21    const data = await sharedContext.manager.execute(22      "SELECT COUNT(*) as num FROM my_custom"23    ) 24    25    return parseInt(data[0].num)26  }27}

Learn more in this documentation

Execute Database Operations in Transactions#

To execute database operations within a transaction in your service:

Code
1import { 2  InjectManager,3  InjectTransactionManager,4  MedusaContext,5} from "@medusajs/framework/utils"6import { Context } from "@medusajs/framework/types"7import { EntityManager } from "@mikro-orm/knex"8
9class HelloModuleService {10  // ...11  @InjectTransactionManager()12  protected async update_(13    input: {14      id: string,15      name: string16    },17    @MedusaContext() sharedContext?: Context<EntityManager>18  ): Promise<any> {19    const transactionManager = sharedContext.transactionManager20    await transactionManager.nativeUpdate(21      "my_custom",22      {23        id: input.id,24      },25      {26        name: input.name,27      }28    )29
30    // retrieve again31    const updatedRecord = await transactionManager.execute(32      `SELECT * FROM my_custom WHERE id = '${input.id}'`33    )34
35    return updatedRecord36  }37
38  @InjectManager()39  async update(40    input: {41      id: string,42      name: string43    },44    @MedusaContext() sharedContext?: Context<EntityManager>45  ) {46    return await this.update_(input, sharedContext)47  }48}

Learn more in this documentation.


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

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

  1. Create the file src/links/hello-product.ts with the following content:
src/links/hello-product.ts
1import HelloModule from "../modules/hello"2import ProductModule from "@medusajs/medusa/product"3import { defineLink } from "@medusajs/framework/utils"4
5export default defineLink(6  ProductModule.linkable.product,7  HelloModule.linkable.myCustom8)
  1. Run the following command to sync the links:
Terminal
npx medusa db:migrate

Learn more in this documentation.

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

Code
1import HelloModule from "../modules/hello"2import ProductModule from "@medusajs/medusa/product"3import { defineLink } from "@medusajs/framework/utils"4
5export default defineLink(6  ProductModule.linkable.product,7  {8    linkable: HelloModule.linkable.myCustom,9    isList: true,10  }11)

Learn more about list links in this documentation.

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

Code
1import HelloModule from "../modules/hello"2import ProductModule from "@medusajs/medusa/product"3import { defineLink } from "@medusajs/framework/utils"4
5export default defineLink(6  ProductModule.linkable.product,7  {8    linkable: HelloModule.linkable.myCustom,9    deleteCascades: true,10  }11)

Learn more in this documentation.

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

Code
1import HelloModule from "../modules/hello"2import ProductModule from "@medusajs/medusa/product"3import { defineLink } from "@medusajs/framework/utils"4
5export default defineLink(6  ProductModule.linkable.product,7  HelloModule.linkable.myCustom,8  {9    database: {10      extraColumns: {11        metadata: {12          type: "json",13        },14      },15    },16  }17)

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

Code
1await remoteLink.create({2  [Modules.PRODUCT]: {3    product_id: "123",4  },5  HELLO_MODULE: {6    my_custom_id: "321",7  },8  data: {9    metadata: {10      test: true,11    },12  },13})

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

Code
1import productHelloLink from "../links/product-hello"2
3// ...4
5const { data } = await query.graph({6  entity: productHelloLink.entryPoint,7  fields: ["metadata", "product.*", "my_custom.*"],8  filters: {9    product_id: "prod_123",10  },11})

Learn more in this documentation.

To create a link between two records using remote link:

Code
1import { Modules } from "@medusajs/framework/utils"2import { HELLO_MODULE } from "../../modules/hello"3
4// ...5
6await remoteLink.create({7  [Modules.PRODUCT]: {8    product_id: "prod_123",9  },10  [HELLO_MODULE]: {11    my_custom_id: "mc_123",12  },13})

Learn more in this documentation.

To dismiss links between records using remote link:

Code
1import { Modules } from "@medusajs/framework/utils"2import { HELLO_MODULE } from "../../modules/hello"3
4// ...5
6await remoteLink.dismiss({7  [Modules.PRODUCT]: {8    product_id: "prod_123",9  },10  [HELLO_MODULE]: {11    my_custom_id: "mc_123",12  },13})

Learn more in this documentation.

Cascade Delete Linked Records#

To cascade delete records linked to a deleted record:

Code
1import { Modules } from "@medusajs/framework/utils"2
3// ...4
5await productModuleService.deleteVariants([variant.id])6
7await remoteLink.delete({8  [Modules.PRODUCT]: {9    product_id: "prod_123",10  },11})

Learn more in this documentation.

Restore Linked Records#

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

Code
1import { Modules } from "@medusajs/framework/utils"2
3// ...4
5await productModuleService.restoreProducts(["prod_123"])6
7await remoteLink.restore({8  [Modules.PRODUCT]: {9    product_id: "prod_123",10  },11})

Learn more in this documentation.


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:

Code
1import {2  MedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5import {6  ContainerRegistrationKeys,7} from "@medusajs/framework/utils"8
9export const GET = async (10  req: MedusaRequest,11  res: MedusaResponse12) => {13  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)14
15  const { data: myCustoms } = await query.graph({16    entity: "my_custom",17    fields: ["id", "name"],18  })19
20  res.json({ my_customs: myCustoms })21}

Learn more in this documentation.

Retrieve Linked Records of Data Model#

To retrieve records linked to a data model:

Code
1import {2  MedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5import {6  ContainerRegistrationKeys,7} from "@medusajs/framework/utils"8
9export const GET = async (10  req: MedusaRequest,11  res: MedusaResponse12) => {13  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)14
15  const { data: myCustoms } = await query.graph({16    entity: "my_custom",17    fields: [18      "id", 19      "name",20      "product.*",21    ],22  })23
24  res.json({ my_customs: myCustoms })25}

Learn more in this documentation.

Apply Filters to Retrieved Records#

To filter the retrieved records:

Code
1import {2  MedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5import {6  ContainerRegistrationKeys,7} from "@medusajs/framework/utils"8
9export const GET = async (10  req: MedusaRequest,11  res: MedusaResponse12) => {13  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)14
15  const { data: myCustoms } = await query.graph({16    entity: "my_custom",17    fields: ["id", "name"],18    filters: {19      id: [20        "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",21        "mc_01HWSVWK3KYHKQEE6QGS2JC3FX",22      ],23    },24  })25
26  res.json({ my_customs: myCustoms })27}

Learn more in this documentation.

Apply Pagination and Sort Records#

To paginate and sort retrieved records:

Code
1import {2  MedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5import {6  ContainerRegistrationKeys,7} from "@medusajs/framework/utils"8
9export const GET = async (10  req: MedusaRequest,11  res: MedusaResponse12) => {13  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)14
15  const { 16    data: myCustoms,17    metadata: { count, take, skip },18  } = await query.graph({19    entity: "my_custom",20    fields: ["id", "name"],21    pagination: {22      skip: 0,23      take: 10,24      order: {25        name: "DESC",26      },27    },28  })29
30  res.json({ 31    my_customs: myCustoms,32    count,33    take,34    skip35  })36}

Learn more in this documentation.


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:
src/workflows/hello-world/steps/step-1.ts
1import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"2
3export const step1 = createStep("step-1", async () => {4  return new StepResponse(`Hello from step one!`)5})
  1. Create the second step at src/workflows/hello-world/steps/step-2.ts with the following content:
src/workflows/hello-world/steps/step-2.ts
1import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"2
3type StepInput = {4  name: string5}6
7export const step2 = createStep(8  "step-2", 9  async ({ name }: StepInput) => {10    return new StepResponse(`Hello ${name} from step two!`)11  }12)
  1. Create the workflow at src/workflows/hello-world/index.ts with the following content:
src/workflows/hello-world/index.ts
1import {2  createWorkflow,3  WorkflowResponse,4} from "@medusajs/framework/workflows-sdk"5import { step1 } from "./steps/step-1"6import { step2 } from "./steps/step-2"7
8const myWorkflow = createWorkflow(9  "hello-world",10  function (input: WorkflowInput) {11    const str1 = step1()12    // to pass input13    const str2 = step2(input)14
15    return new WorkflowResponse({16      message: str1,17    })18  }19)20
21export default myWorkflow

Learn more in this documentation.

Execute a Workflow#

Learn more in this documentation.

Step with a Compensation Function#

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

Code
1import { 2  createStep,3  StepResponse,4} from "@medusajs/framework/workflows-sdk"5
6const step1 = createStep(7  "step-1",8  async () => {9    const message = `Hello from step one!`10
11    console.log(message)12
13    return new StepResponse(message)14  },15  async () => {16    console.log("Oops! Rolling back my changes...")17  }18)

Learn more in this documentation.

Manipulate Variables in Workflow#

To manipulate variables within a workflow's constructor function, use the transform utility:

Code
1import { 2  createWorkflow,3  WorkflowResponse,4  transform,5} from "@medusajs/framework/workflows-sdk"6// step imports...7
8const myWorkflow = createWorkflow(9  "hello-world", 10  function (input) {11    const str1 = step1(input)12    const str2 = step2(input)13
14    const str3 = transform(15      { str1, str2 },16      (data) => `${data.str1}${data.str2}`17    )18
19    return new WorkflowResponse(str3)20  }21)

Learn more in this documentation

Using Conditions in Workflow#

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

Code
1import { 2  createWorkflow,3  WorkflowResponse,4  when,5} from "@medusajs/framework/workflows-sdk"6// step imports...7
8const workflow = createWorkflow(9  "workflow", 10  function (input: {11    is_active: boolean12  }) {13
14    const result = when(15      input, 16      (input) => {17        return input.is_active18      }19    ).then(() => {20      const stepResult = isActiveStep()21      return stepResult22    })23
24    // executed without condition25    const anotherStepResult = anotherStep(result)26
27    return new WorkflowResponse(28      anotherStepResult29    )30  }31)

Run Workflow in Another#

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

Code
1import {2  createWorkflow,3} from "@medusajs/framework/workflows-sdk"4import { 5  createProductsWorkflow,6} from "@medusajs/medusa/core-flows"7
8const workflow = createWorkflow(9  "hello-world",10  async (input) => {11    const products = createProductsWorkflow.runAsStep({12      input: {13        products: [14          // ...15        ],16      },17    })18
19    // ...20  }21)

Learn more in this documentation.

Consume a Workflow Hook#

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

src/workflows/hooks/product-created.ts
1import { createProductsWorkflow } from "@medusajs/medusa/core-flows"2
3createProductsWorkflow.hooks.productsCreated(4  async ({ products, additional_data }, { container }) => {5    // TODO perform an action6  },7  async (dataFromStep, { container }) => {8    // undo the performed action9  }10)

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

Learn more in this documentation.

Expose a Hook#

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

Code
1import {2  createStep,3  createHook,4  createWorkflow,5  WorkflowResponse,6} from "@medusajs/framework/workflows-sdk"7import { createProductStep } from "./steps/create-product"8
9export const myWorkflow = createWorkflow(10  "my-workflow", 11  function (input) {12    const product = createProductStep(input)13    const productCreatedHook = createHook(14      "productCreated", 15      { productId: product.id }16    )17
18    return new WorkflowResponse(product, {19      hooks: [productCreatedHook],20    })21  }22)

Learn more in this documentation.

Retry Steps#

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

Code
1import { 2  createStep, 3} from "@medusajs/framework/workflows-sdk"4
5export const step1 = createStep(6  {7    name: "step-1",8    maxRetries: 2,9  },10  async () => {11    console.log("Executing step 1")12
13    throw new Error("Oops! Something happened.")14  }15)

Learn more in this documentation.

Run Steps in Parallel#

If steps in a workflow don't depend on one another, run them in parallel using the parallel utility:

Code
1import {2  createWorkflow,3  WorkflowResponse,4  parallelize,5} from "@medusajs/framework/workflows-sdk"6import {7  createProductStep,8  getProductStep,9  createPricesStep,10  attachProductToSalesChannelStep,11} from "./steps"12
13interface WorkflowInput {14  title: string15}16
17const myWorkflow = createWorkflow(18  "my-workflow", 19  (input: WorkflowInput) => {20   const product = createProductStep(input)21
22   const [prices, productSalesChannel] = parallelize(23     createPricesStep(product),24     attachProductToSalesChannelStep(product)25   )26
27   const id = product.id28   const refetchedProduct = getProductStep(product.id)29
30   return new WorkflowResponse(refetchedProduct)31 }32)

Learn more in this documentation.

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:

Code
1import { 2  createStep,  3  createWorkflow,4  WorkflowResponse,5} from "@medusajs/framework/workflows-sdk"6// step import...7
8const myWorkflow = createWorkflow({9  name: "hello-world",10  timeout: 2, // 2 seconds11}, function () {12  const str1 = step1()13
14  return new WorkflowResponse({15    message: str1,16  })17})18
19export default myWorkflow

Learn more in this documentation.

Configure Step Timeout#

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

Code
1const step1 = createStep(2  {3    name: "step-1",4    timeout: 2, // 2 seconds5  },6  async () => {7    // ...8  }9)

Learn more in this documentation.

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:

Code
1const step2 = createStep(2  {3    name: "step-2",4    async: true,5  },6  async () => {7    console.log("Waiting to be successful...")8  }9)

Learn more in this documentation.

Change Step Status in Long-Running Workflow#

To change a step's status:

  1. Grab the workflow's transaction ID when you run it:
Code
1const { transaction } = await myLongRunningWorkflow(req.scope)2  .run()
  1. In an API route, workflow, or other resource, change a step's status to successful using the Worfklow Engine Module:
Code
1const workflowEngineService = container.resolve(2  Modules.WORKFLOW_ENGINE3)4
5await workflowEngineService.setStepSuccess({6  idempotencyKey: {7    action: TransactionHandlerType.INVOKE,8    transactionId,9    stepId: "step-2",10    workflowId: "hello-world",11  },12  stepResponse: new StepResponse("Done!"),13  options: {14    container,15  },16})
  1. In an API route, workflow, or other resource, change a step's status to failure using the Worfklow Engine Module:
Code
1const workflowEngineService = container.resolve(2  Modules.WORKFLOW_ENGINE3)4
5await workflowEngineService.setStepFailure({6  idempotencyKey: {7    action: TransactionHandlerType.INVOKE,8    transactionId,9    stepId: "step-2",10    workflowId: "hello-world",11  },12  stepResponse: new StepResponse("Failed!"),13  options: {14    container,15  },16})

Learn more in this documentation.

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:

Code
1import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"2import myWorkflow from "../../../workflows/hello-world"3import { Modules } from "@medusajs/framework/utils"4
5export async function GET(req: MedusaRequest, res: MedusaResponse) {6  const { transaction, result } = await myWorkflow(req.scope).run()7
8  const workflowEngineService = req.scope.resolve(9    Modules.WORKFLOW_ENGINE10  )11
12  const subscriptionOptions = {13    workflowId: "hello-world",14    transactionId: transaction.transactionId,15    subscriberId: "hello-world-subscriber",16  }17
18  await workflowEngineService.subscribe({19    ...subscriptionOptions,20    subscriber: async (data) => {21      if (data.eventType === "onFinish") {22        console.log("Finished execution", data.result)23        // unsubscribe24        await workflowEngineService.unsubscribe({25          ...subscriptionOptions,26          subscriberOrId: subscriptionOptions.subscriberId,27        })28      } else if (data.eventType === "onStepFailure") {29        console.log("Workflow failed", data.step)30      }31    },32  })33
34  res.send(result)35}

Learn more in this documentation.


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:

src/subscribers/product-created.ts
1import type {2  SubscriberArgs,3  SubscriberConfig,4} from "@medusajs/framework"5
6export default async function productCreateHandler({7  event,8}: SubscriberArgs<{ id: string }>) {9  const productId = event.data.id10  console.log(`The product ${productId} was created`)11}12
13export const config: SubscriberConfig = {14  event: "product.created",15}

Learn more in this documentation.

Resolve Resources in Subscriber#

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

Code
1import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"2import { Modules } from "@medusajs/framework/utils"3
4export default async function productCreateHandler({5  event: { data },6  container,7}: SubscriberArgs<{ id: string }>) {8  const productModuleService = container.resolve(Modules.PRODUCT)9
10  const productId = data.id11
12  const product = await productModuleService.retrieveProduct(13    productId14  )15
16  console.log(`The product ${product.title} was created`)17}18
19export const config: SubscriberConfig = {20  event: `product.created`,21}

Learn more in this documentation.

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:

src/subscribers/handle-reset.ts
1import {2  SubscriberArgs,3  type SubscriberConfig,4} from "@medusajs/medusa"5import { Modules } from "@medusajs/framework/utils"6
7export default async function resetPasswordTokenHandler({8  event: { data: {9    entity_id: email,10    token,11    actor_type,12  } },13  container,14}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) {15  const notificationModuleService = container.resolve(16    Modules.NOTIFICATION17  )18
19  const urlPrefix = actor_type === "customer" ? 20    "https://storefront.com" : 21    "https://admin.com"22
23  await notificationModuleService.createNotifications({24    to: email,25    channel: "email",26    template: "reset-password-template",27    data: {28      // a URL to a frontend application29      url: `${urlPrefix}/reset-password?token=${token}&email=${email}`,30    },31  })32}33
34export const config: SubscriberConfig = {35  event: "auth.password_reset",36}

Learn more in this documentation.

Execute a Workflow in a Subscriber#

To execute a workflow in a subscriber:

Code
1import {2  type SubscriberConfig,3  type SubscriberArgs,4} from "@medusajs/framework"5import myWorkflow from "../workflows/hello-world"6import { Modules } from "@medusajs/framework/utils"7
8export default async function handleCustomerCreate({9  event: { data },10  container,11}: SubscriberArgs<{ id: string }>) {12  const userId = data.id13  const userModuleService = container.resolve(14    Modules.USER15  )16
17  const user = await userModuleService.retrieveUser(userId)18
19  const { result } = await myWorkflow(container)20    .run({21      input: {22        name: user.first_name,23      },24    })25
26  console.log(result)27}28
29export const config: SubscriberConfig = {30  event: "user.created",31}

Learn more in this documentation


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:

src/jobs/hello-world.ts
1// the scheduled-job function2export default function () {3  console.log("Time to say hello world!")4}5
6// the job's configurations7export const config = {8  name: "every-minute-message",9  // execute every minute10  schedule: "* * * * *",11}

Learn more in this documentation.

Resolve Resources in Scheduled Job#

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

Code
1import { MedusaContainer } from "@medusajs/framework/types"2import { Modules } from "@medusajs/framework/utils"3
4export default async function myCustomJob(5  container: MedusaContainer6) {7  const productModuleService = container.resolve(Modules.PRODUCT)8
9  const [, count] = await productModuleService.listAndCountProducts()10
11  console.log(12    `Time to check products! You have ${count} product(s)`13  )14}15
16export const config = {17  name: "every-minute-message",18  // execute every minute19  schedule: "* * * * *",20}

Learn more in this documentation

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:

Code
1export default async function myCustomJob() {2  console.log("I'll be executed three times only.")3}4
5export const config = {6  name: "hello-world",7  // execute every minute8  schedule: "* * * * *",9  numberOfExecutions: 3,10}

Learn more in this documentation.

Execute a Workflow in a Scheduled Job#

To execute a workflow in a scheduled job:

Code
1import { MedusaContainer } from "@medusajs/framework/types"2import myWorkflow from "../workflows/hello-world"3
4export default async function myCustomJob(5  container: MedusaContainer6) {7  const { result } = await myWorkflow(container)8    .run({9      input: {10        name: "John",11      },12    })13
14  console.log(result.message)15}16
17export const config = {18  name: "run-once-a-day",19  schedule: `0 0 * * *`,20}

Learn more in this documentation


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:

src/modules/hello/loaders/hello-world.ts
1export default async function helloWorldLoader() {2  console.log(3    "[HELLO MODULE] Just started the Medusa application!"4  )5}

Learn more in this documentation.

Resolve Resources in Loader#

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

Code
1import {2  LoaderOptions,3} from "@medusajs/framework/types"4import { 5  ContainerRegistrationKeys6} from "@medusajs/framework/utils"7
8export default async function helloWorldLoader({9  container,10}: LoaderOptions) {11  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)12
13  logger.info("[helloWorldLoader]: Hello, World!")14}

Learn more in this documentation.

Access Module Options#

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

Code
1import {2  LoaderOptions,3} from "@medusajs/framework/types"4
5// recommended to define type in another file6type ModuleOptions = {7  apiKey?: boolean8}9
10export default async function helloWorldLoader({11  options,12}: LoaderOptions<ModuleOptions>) {13  14  console.log(15    "[HELLO MODULE] Just started the Medusa application!",16    options17  )18}

Learn more in this documentation.

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:

Code
1import {2  LoaderOptions,3} from "@medusajs/framework/types"4import { asValue } from "awilix"5
6export default async function helloWorldLoader({7  container,8}: LoaderOptions) {9  container.registerAdd(10    "custom_data",11    asValue({12      test: true13    })14  )15}

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.

NoteFor a list of components to use in the admin dashboard, refere to this documentation .

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:

src/admin/widgets/products-widget.tsx
1import { defineWidgetConfig } from "@medusajs/admin-sdk"2import { Container, Heading } from "@medusajs/ui"3
4const ProductWidget = () => {5  return (6    <Container className="divide-y p-0">7      <div className="flex items-center justify-between px-6 py-4">8        <Heading level="h2">Product Widget</Heading>9      </div>10    </Container>11  )12}13
14export const config = defineWidgetConfig({15  zone: "product.list.before",16})17
18export default ProductWidget

Learn more about widgets in this documentation.

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):

Code
1import { defineWidgetConfig } from "@medusajs/admin-sdk"2import { Container, Heading } from "@medusajs/ui"3import { 4  DetailWidgetProps, 5  AdminProduct,6} from "@medusajs/framework/types"7
8// The widget9const ProductWidget = ({ 10  data,11}: DetailWidgetProps<AdminProduct>) => {12  return (13    <Container className="divide-y p-0">14      <div className="flex items-center justify-between px-6 py-4">15        <Heading level="h2">16          Product Widget {data.title}17        </Heading>18      </div>19    </Container>20  )21}22
23// The widget's configurations24export const config = defineWidgetConfig({25  zone: "product.details.before",26})27
28export default ProductWidget

Learn more in this documentation.

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:

src/admin/routes/custom/page.tsx
1import { defineRouteConfig } from "@medusajs/admin-sdk"2import { ChatBubbleLeftRight } from "@medusajs/icons"3import { Container, Heading } from "@medusajs/ui"4
5const CustomPage = () => {6  return (7    <Container className="divide-y p-0">8      <div className="flex items-center justify-between px-6 py-4">9        <Heading level="h2">This is my custom route</Heading>10      </div>11    </Container>12  )13}14
15export const config = defineRouteConfig({16  label: "Custom Route",17  icon: ChatBubbleLeftRight,18})19
20export default CustomPage

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

Learn more in this documentation.

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:

src/admin/routes/settings/custom/page.tsx
1import { defineRouteConfig } from "@medusajs/admin-sdk"2import { Container, Heading } from "@medusajs/ui"3
4const CustomSettingPage = () => {5  return (6    <Container className="divide-y p-0">7      <div className="flex items-center justify-between px-6 py-4">8        <Heading level="h1">Custom Setting Page</Heading>9      </div>10    </Container>11  )12}13
14export const config = defineRouteConfig({15  label: "Custom",16})17
18export default CustomSettingPage

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

Learn more in this documentation

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:

src/admin/routes/custom/[id]/page.tsx
1import { useParams } from "react-router-dom"2import { Container } from "@medusajs/ui"3
4const CustomPage = () => {5  const { id } = useParams()6
7  return (8    <Container className="divide-y p-0">9      <div className="flex items-center justify-between px-6 py-4">10        <Heading level="h1">Passed ID: {id}</Heading>11      </div>12    </Container>13  )14}15
16export default CustomPage

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

Learn more in this documentation

Send Request to API Route#

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

For example:

Code
1import { defineWidgetConfig } from "@medusajs/admin-sdk"2import { Container } from "@medusajs/ui"3import { useEffect, useState } from "react"4
5const ProductWidget = () => {6  const [productsCount, setProductsCount] = useState(0)7  const [loading, setLoading] = useState(true)8
9  useEffect(() => {10    if (!loading) {11      return12    }13
14    fetch(`/admin/products`, {15      credentials: "include",16    })17    .then((res) => res.json())18    .then(({ count }) => {19      setProductsCount(count)20      setLoading(false)21    })22  }, [loading])23
24  return (25    <Container className="divide-y p-0">26      {loading && <span>Loading...</span>}27      {!loading && <span>You have {productsCount} Product(s).</span>}28    </Container>29  )30}31
32export const config = defineWidgetConfig({33  zone: "product.list.before",34})35
36export default ProductWidget

Learn more in this documentation

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

Code
1import { defineWidgetConfig } from "@medusajs/admin-sdk"2import { Container } from "@medusajs/ui"3import { Link } from "react-router-dom"4
5// The widget6const ProductWidget = () => {7  return (8    <Container className="divide-y p-0">9      <Link to={"/orders"}>View Orders</Link>10    </Container>11  )12}13
14// The widget's configurations15export const config = defineWidgetConfig({16  zone: "product.details.before",17})18
19export default ProductWidget

Learn more in this documentation.


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.

NoteFor details on setting up your project for integration tests, refer to this documentation .

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:

integration-tests/http/custom-routes.spec.ts
1import { medusaIntegrationTestRunner } from "@medusajs/test-utils"2
3medusaIntegrationTestRunner({4  testSuite: ({ api, getContainer }) => {5    describe("Custom endpoints", () => {6      describe("GET /custom", () => {7        it("returns correct message", async () => {8          const response = await api.get(9            `/custom`10          )11  12          expect(response.status).toEqual(200)13          expect(response.data).toHaveProperty("message")14          expect(response.data.message).toEqual("Hello, World!")15        })16      })17    })18  },19})

Then, run the test with the following command:

Learn more in this documentation.

Test Workflow#

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

integration-tests/http/workflow.spec.ts
1import { medusaIntegrationTestRunner } from "@medusajs/test-utils"2import { helloWorldWorkflow } from "../../src/workflows/hello-world"3
4medusaIntegrationTestRunner({5  testSuite: ({ getContainer }) => {6    describe("Test hello-world workflow", () => {7      it("returns message", async () => {8        const { result } = await helloWorldWorkflow(getContainer())9          .run()10
11        expect(result).toEqual("Hello, World!")12      })13    })14  },15})

Then, run the test with the following command:

Learn more in this documentation.

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/hello/__tests__/service.spec.ts with the following content:

src/modules/hello/__tests__/service.spec.ts
1import { moduleIntegrationTestRunner } from "@medusajs/test-utils"2import { HELLO_MODULE } from ".."3import HelloModuleService from "../service"4import MyCustom from "../models/my-custom"5
6moduleIntegrationTestRunner<HelloModuleService>({7  moduleName: HELLO_MODULE,8  moduleModels: [MyCustom],9  resolve: "./modules/hello",10  testSuite: ({ service }) => {11    describe("HelloModuleService", () => {12      it("says hello world", () => {13        const message = service.getMessage()14
15        expect(message).toEqual("Hello, World!")16      })17    })18  },19})

Then, run the test with the following command:


Commerce Modules#

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

NoteRefer to the Commerce Modules documentation for concepts and reference of every module's main service.

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:
Code
1import { model } from "@medusajs/framework/utils"2
3const Manager = model.define("manager", {4  id: model.id().primaryKey(),5  firstName: model.text(),6  lastName: model.text(),7  email: model.text(),8})9
10export default Manager
  1. Use the setAuthAppMetadataStep as a step in a workflow that creates a manager:
Code
1import { 2  createWorkflow, 3  WorkflowResponse,4} from "@medusajs/framework/workflows-sdk"5import { 6  setAuthAppMetadataStep,7} from "@medusajs/medusa/core-flows"8// other imports...9
10const createManagerWorkflow = createWorkflow(11  "create-manager",12  function (input: CreateManagerWorkflowInput) {13    const manager = createManagerStep({14      manager: input.manager,15    })16
17    setAuthAppMetadataStep({18      authIdentityId: input.authIdentityId,19      actorType: "manager",20      value: manager.id,21    })22
23    return new WorkflowResponse(manager)24  }25)
  1. Use the workflow in an API route that creates a user (manager) of the actor type:
Code
1import type { 2  AuthenticatedMedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5import { MedusaError } from "@medusajs/framework/utils"6import createManagerWorkflow from "../../workflows/create-manager"7
8type RequestBody = {9  first_name: string10  last_name: string11  email: string12}13
14export async function POST(15  req: AuthenticatedMedusaRequest<RequestBody>, 16  res: MedusaResponse17) {18  // If `actor_id` is present, the request carries 19  // authentication for an existing manager20  if (req.auth_context.actor_id) {21    throw new MedusaError(22      MedusaError.Types.INVALID_DATA,23      "Request already authenticated as a manager."24    )25  }26
27  const { result } = await createManagerWorkflow(req.scope)28    .run({29      input: {30        manager: req.body,31        authIdentityId: req.auth_context.auth_identity_id,32      },33    })34  35    res.status(200).json({ manager: result })36}
  1. Apply the authenticate middleware on the new route in src/api/middlewares.ts:
src/api/middlewares.ts
1import { 2  defineMiddlewares,3  authenticate,4} from "@medusajs/medusa"5
6export default defineMiddlewares({7  routes: [8    {9      matcher: "/manager",10      method: "POST",11      middlewares: [12        authenticate("manager", ["session", "bearer"], {13          allowUnregistered: true,14        }),15      ],16    },17    {18      matcher: "/manager/me*",19      middlewares: [20        authenticate("manager", ["session", "bearer"]),21      ],22    },23  ],24})

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.

Apply Promotion on Cart Items and Shipping#

To apply a promotion on a cart's items and shipping methods using the Cart and Promotion modules:

Code
1import {2  ComputeActionAdjustmentLine,3  ComputeActionItemLine,4  ComputeActionShippingLine,5  AddItemAdjustmentAction,6  AddShippingMethodAdjustment,7  // ...8} from "@medusajs/framework/types"9
10// retrieve the cart11const cart = await cartModuleService.retrieveCart("cart_123", {12  relations: [13    "items.adjustments",14    "shipping_methods.adjustments",15  ],16})17
18// retrieve line item adjustments19const lineItemAdjustments: ComputeActionItemLine[] = []20cart.items.forEach((item) => {21  const filteredAdjustments = item.adjustments?.filter(22    (adjustment) => adjustment.code !== undefined23  ) as unknown as ComputeActionAdjustmentLine[]24  if (filteredAdjustments.length) {25    lineItemAdjustments.push({26      ...item,27      adjustments: filteredAdjustments,28    })29  }30})31
32// retrieve shipping method adjustments33const shippingMethodAdjustments: ComputeActionShippingLine[] =34  []35cart.shipping_methods.forEach((shippingMethod) => {36  const filteredAdjustments =37    shippingMethod.adjustments?.filter(38      (adjustment) => adjustment.code !== undefined39    ) as unknown as ComputeActionAdjustmentLine[]40  if (filteredAdjustments.length) {41    shippingMethodAdjustments.push({42      ...shippingMethod,43      adjustments: filteredAdjustments,44    })45  }46})47
48// compute actions49const actions = await promotionModuleService.computeActions(50  ["promo_123"],51  {52    items: lineItemAdjustments,53    shipping_methods: shippingMethodAdjustments,54  }55)56
57// set the adjustments on the line item58await cartModuleService.setLineItemAdjustments(59  cart.id,60  actions.filter(61    (action) => action.action === "addItemAdjustment"62  ) as AddItemAdjustmentAction[]63)64
65// set the adjustments on the shipping method66await cartModuleService.setShippingMethodAdjustments(67  cart.id,68  actions.filter(69    (action) =>70      action.action === "addShippingMethodAdjustment"71  ) as AddShippingMethodAdjustment[]72)

Learn more in this documentation.

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 and Tax modules:

Code
1// retrieve the cart2const cart = await cartModuleService.retrieveCart("cart_123", {3  relations: [4    "items.tax_lines",5    "shipping_methods.tax_lines",6    "shipping_address",7  ],8})9
10// retrieve the tax lines11const taxLines = await taxModuleService.getTaxLines(12  [13    ...(cart.items as TaxableItemDTO[]),14    ...(cart.shipping_methods as TaxableShippingDTO[]),15  ],16  {17    address: {18      ...cart.shipping_address,19      country_code:20        cart.shipping_address.country_code || "us",21    },22  }23)24
25// set line item tax lines26await cartModuleService.setLineItemTaxLines(27  cart.id,28  taxLines.filter((line) => "line_item_id" in line)29)30
31// set shipping method tax lines32await cartModuleService.setLineItemTaxLines(33  cart.id,34  taxLines.filter((line) => "shipping_line_id" in line)35)

Learn more in this documentation

Apply Promotion on an Order's Items and Shipping#

To apply a promotion on an order's items and shipping methods using the Order and Promotion modules:

Code
1import {2  ComputeActionAdjustmentLine,3  ComputeActionItemLine,4  ComputeActionShippingLine,5  AddItemAdjustmentAction,6  AddShippingMethodAdjustment,7  // ...8} from "@medusajs/framework/types"9
10// ...11
12// retrieve the order13const order = await orderModuleService.retrieveOrder("ord_123", {14  relations: [15    "items.item.adjustments",16    "shipping_methods.shipping_method.adjustments",17  ],18})19// retrieve the line item adjustments20const lineItemAdjustments: ComputeActionItemLine[] = []21order.items.forEach((item) => {22  const filteredAdjustments = item.adjustments?.filter(23    (adjustment) => adjustment.code !== undefined24  ) as unknown as ComputeActionAdjustmentLine[]25  if (filteredAdjustments.length) {26    lineItemAdjustments.push({27      ...item,28      ...item.detail,29      adjustments: filteredAdjustments,30    })31  }32})33
34//retrieve shipping method adjustments35const shippingMethodAdjustments: ComputeActionShippingLine[] =36  []37order.shipping_methods.forEach((shippingMethod) => {38  const filteredAdjustments =39    shippingMethod.adjustments?.filter(40      (adjustment) => adjustment.code !== undefined41    ) as unknown as ComputeActionAdjustmentLine[]42  if (filteredAdjustments.length) {43    shippingMethodAdjustments.push({44      ...shippingMethod,45      adjustments: filteredAdjustments,46    })47  }48})49
50// compute actions51const actions = await promotionModuleService.computeActions(52  ["promo_123"],53  {54    items: lineItemAdjustments,55    shipping_methods: shippingMethodAdjustments,56    // TODO infer from cart or region57    currency_code: "usd",58  }59)60
61// set the adjustments on the line items62await orderModuleService.setOrderLineItemAdjustments(63  order.id,64  actions.filter(65    (action) => action.action === "addItemAdjustment"66  ) as AddItemAdjustmentAction[]67)68
69// set the adjustments on the shipping methods70await orderModuleService.setOrderShippingMethodAdjustments(71  order.id,72  actions.filter(73    (action) =>74      action.action === "addShippingMethodAdjustment"75  ) as AddShippingMethodAdjustment[]76)

Learn more in this documentation

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:
Code
1import { 2  ContainerRegistrationKeys,3  Modules4} from "@medusajs/framework/utils"5
6// ...7
8const paymentCollection =9  await paymentModuleService.createPaymentCollections({10    region_id: "reg_123",11    currency_code: "usd",12    amount: 5000,13  })14
15// resolve the remote link16const remoteLink = container.resolve(17  ContainerRegistrationKeys18)19
20// create a link between the cart and payment collection21remoteLink.create({22  [Modules.CART]: {23    cart_id: "cart_123"24  },25  [Modules.PAYMENT]: {26    payment_collection_id: paymentCollection.id27  }28})
  1. Create a payment session in the collection:
Code
1const paymentSession =2  await paymentModuleService.createPaymentSession(3    paymentCollection.id,4    {5      provider_id: "stripe",6      currency_code: "usd",7      amount: 5000,8      data: {9        // any necessary data for the10        // payment provider11      },12    }13  )
  1. Authorize the payment session:
Code
1const payment =2  await paymentModuleService.authorizePaymentSession(3    paymentSession.id,4    {}5  )

Learn more in this documentation.

Get Variant's Prices for Region and Currency#

To get prices of a product variant for a region and currency using Query:

Code
1import { QueryContext } from "@medusajs/framework/utils"2
3// ...4
5const { data: products } = await query.graph({6  entity: "product",7  fields: [8    "*",9    "variants.*",10    "variants.calculated_price.*",11  ],12  filters: {13    id: "prod_123",14  },15  context: {16    variants: {17      calculated_price: QueryContext({18        region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS",19        currency_code: "eur",20      }),21    },22  },23})

Learn more in this documentation.

Get All Variant's Prices#

To get all prices of a product variant using Query:

Code
1const { data: products } = await query.graph({2  entity: "product",3  fields: [4    "*",5    "variants.*",6    "variants.prices.*",7  ],8  filters: {9    id: [10      "prod_123",11    ],12  },13})

Learn more in this documentation.

Get Variant Prices with Taxes#

To get a variant's prices with taxes using Query and the Tax Module

Code
1import {2  HttpTypes,3  TaxableItemDTO,4  ItemTaxLineDTO,5} from "@medusajs/framework/types"6import { 7  QueryContext,8  calculateAmountsWithTax9} from "@medusajs/framework/utils"10// other imports...11
12// ...13const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => {14  return product.variants15    ?.map((variant) => {16      if (!variant.calculated_price) {17        return18      }19
20      return {21        id: variant.id,22        product_id: product.id,23        product_name: product.title,24        product_categories: product.categories?.map((c) => c.name),25        product_category_id: product.categories?.[0]?.id,26        product_sku: variant.sku,27        product_type: product.type,28        product_type_id: product.type_id,29        quantity: 1,30        unit_price: variant.calculated_price.calculated_amount,31        currency_code: variant.calculated_price.currency_code,32      }33    })34    .filter((v) => !!v) as unknown as TaxableItemDTO[]35}36
37const { data: products } = await query.graph({38  entity: "product",39  fields: [40    "*",41    "variants.*",42    "variants.calculated_price.*",43  ],44  filters: {45    id: "prod_123",46  },47  context: {48    variants: {49      calculated_price: QueryContext({50        region_id: "region_123",51        currency_code: "usd",52      }),53    },54  },55})56
57const taxLines = (await taxModuleService.getTaxLines(58  products.map(asTaxItem).flat(),59  {60    // example of context properties. You can pass other ones.61    address: {62      country_code,63    },64  }65)) as unknown as ItemTaxLineDTO[]66
67const taxLinesMap = new Map<string, ItemTaxLineDTO[]>()68taxLines.forEach((taxLine) => {69  const variantId = taxLine.line_item_id70  if (!taxLinesMap.has(variantId)) {71    taxLinesMap.set(variantId, [])72  }73
74  taxLinesMap.get(variantId)?.push(taxLine)75})76
77products.forEach((product) => {78  product.variants?.forEach((variant) => {79    if (!variant.calculated_price) {80      return81    }82
83    const taxLinesForVariant = taxLinesMap.get(variant.id) || []84    const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({85      taxLines: taxLinesForVariant,86      amount: variant.calculated_price!.calculated_amount!,87      includesTax:88        variant.calculated_price!.is_calculated_price_tax_inclusive!,89    })90
91    // do something with prices...92  })93})

Learn more in this documentation.

Invite Users#

To invite a user using the User Module:

Code
1const invite = await userModuleService.createInvites({2  email: "user@example.com",3})

Accept User Invite#

To accept an invite and create a user using the User Module:

Code
1const invite =2  await userModuleService.validateInviteToken(inviteToken)3
4await userModuleService.updateInvites({5  id: invite.id,6  accepted: true,7})8
9const user = await userModuleService.createUsers({10  email: invite.email,11})
Was this page helpful?
Edit this page