- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
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:
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:
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:
Learn more about path parameters in this documentation.
Use Query Parameters#
API routes can accept query parameters:
Learn more about query parameters in this documentation.
Use Body Parameters#
API routes can accept request body parameters:
Learn more about request body parameters in this documentation.
Set Response Code#
You can change the response code of an API route:
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:
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
:
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:
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:
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#
- Create a Zod schema in the file
src/api/custom/validators.ts
:
- Add a validation middleware to the custom route in
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})
- Use the validated body in the
/custom
API route:
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:
- Create the file
src/api/middlewares.ts
with the following content:
- Create the file
src/workflows/hooks/created-product.ts
with the following content:
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)
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
:
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
:
Learn more in this documentation.
Retrieve Logged-In Admin User#
To retrieve the currently logged-in user in an API route:
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:
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:
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:
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:
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:
To access the raw body data in your route, use the req.rawBody
property:
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#
- Create the directory
src/modules/hello
. - Create the file
src/modules/hello/models/my-custom.ts
with the following data model:
- Create the file
src/modules/hello/service.ts
with the following service:
- Create the file
src/modules/hello/index.ts
that exports the module definition:
- Add the module to the configurations in
medusa-config.ts
:
- Generate and run migrations:
- Use the module's main service in an API route:
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_custom,19 })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:
Then, export the service in the file src/modules/hello/services/index.ts
:
Finally, resolve the service in your module's main service or loader:
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:
- Pass options to the module in
medusa-config.ts
:
- Access the options in the module's main service:
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:
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:
- Create the file
src/modules/hello/models/my-custom.ts
with the following data model:
- Generate and run migrations:
Learn more in this documentation.
Data Model Property Types#
A data model can have properties of the following types:
- ID property:
- Text property:
- Number property:
- Big Number property:
- Boolean property:
- Enum property:
- Date-Time property:
- JSON property:
- Array property:
Learn more in this documentation.
Set Primary Key#
To set an id
property as the primary key of a data model:
To set a text
property as the primary key:
To set a number
property as the primary key:
Learn more in this documentation.
Default Property Value#
To set the default value of a property:
Learn more in this documentation.
Nullable Property#
To allow null
values for a property:
Learn more in this documentation.
Unique Property#
To create a unique index on a property:
Learn more in this documentation.
Define Database Index on Property#
To define a database index on a property:
Learn more in this documentation.
Define Composite Index on Data Model#
To define a composite index on a data model:
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:
Then, to search by that property, pass the q
filter to the list
or listAndCount
generated methods of the module's main service:
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:
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:
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:
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:
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:
And to set the ID of a user's email when creating or updating it:
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:
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:
To add new orders to a product without removing the previous associations:
Learn more in this documentation.
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:
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:
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:
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:
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:
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.
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:
- Create the file
src/links/hello-product.ts
with the following content:
- Run the following command to sync the links:
Learn more in this documentation.
Define a List Link#
To define a list link, where multiple records of a model can be linked to a record in another:
Learn more about list links in this documentation.
Set Delete Cascade on Link Definition#
To ensure a model's records linked to another model are deleted when the linked model is deleted:
Learn more in this documentation.
Add Custom Columns to Module Link#
To add a custom column to the table that stores the linked records of two data models:
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:
To retrieve the custom column when retrieving linked records using Query:
Learn more in this documentation.
Create Link Between Records#
To create a link between two records using remote link:
Learn more in this documentation.
Dismiss Link Between Records#
To dismiss links between records using remote link:
Learn more in this documentation.
Cascade Delete Linked Records#
To cascade delete records linked to a deleted record:
Learn more in this documentation.
Restore Linked Records#
To restore records that were soft-deleted because they were linked to a soft-deleted record:
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:
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:
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:
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:
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 skip,35 })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:
- Create the first step at
src/workflows/hello-world/steps/step-1.ts
with the following content:
- Create the second step at
src/workflows/hello-world/steps/step-2.ts
with the following content:
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)
- Create the workflow at
src/workflows/hello-world/index.ts
with the following content:
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
:
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:
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:
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:
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
:
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
:
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:
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:
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:
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:
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:
Learn more in this documentation.
Change Step Status in Long-Running Workflow#
To change a step's status:
- Grab the workflow's transaction ID when you run it:
- In an API route, workflow, or other resource, change a step's status to successful using the Worfklow Engine Module:
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})
- In an API route, workflow, or other resource, change a step's status to failure using the Worfklow Engine Module:
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:
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:
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:
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:
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:
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:
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:
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:
Learn more in this documentation.
Execute a Workflow in a Scheduled Job#
To execute a workflow in a scheduled job:
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:
Learn more in this documentation.
Resolve Resources in Loader#
To resolve resources in a loader, use the container
property of its first parameter:
1import {2 LoaderOptions,3} from "@medusajs/framework/types"4import { 5 ContainerRegistrationKeys,6} 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:
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:
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.
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:
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):
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:
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:
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:
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:
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
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:
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.
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:
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:
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:
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.
Create an Actor Type to Authenticate#
To create an actor type that can authenticate to the Medusa application, such as a manager
:
- Create the data model in a module:
- Use the
setAuthAppMetadataStep
as a step in a workflow that creates a manager:
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)
- Use the workflow in an API route that creates a user (manager) of the actor type:
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}
- Apply the
authenticate
middleware on the new route insrc/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:
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:
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:
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:
- Create a payment collection and link it to the cart:
1import { 2 ContainerRegistrationKeys,3 Modules,4} 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.id,27 },28})
- Create a payment session in the collection:
- Authorize the payment session:
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:
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:
Learn more in this documentation.
Get Variant Prices with Taxes#
To get a variant's prices with taxes using Query and the Tax Module
1import {2 HttpTypes,3 TaxableItemDTO,4 ItemTaxLineDTO,5} from "@medusajs/framework/types"6import { 7 QueryContext,8 calculateAmountsWithTax,9} 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:
Accept User Invite#
To accept an invite and create a user using the User Module: