www/apps/resources/app/examples/page.mdx
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: 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.
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 the file src/api/hello-world/route.ts with the following content:
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
export const GET = (
req: MedusaRequest,
res: MedusaResponse
) => {
res.json({
message: "[GET] Hello world!",
})
}
This creates a GET API route at /hello-world.
Learn more in this documentation.
To resolve resources from the Medusa container in an API route:
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const productModuleService = req.scope.resolve(
Modules.PRODUCT
)
const [, count] = await productModuleService
.listAndCountProducts()
res.json({
count,
})
}
This resolves the Product Module's main service.
Learn more in this documentation.
API routes can accept path parameters.
To do that, create the file src/api/hello-world/[id]/route.ts with the following content:
export const singlePathHighlights = [
["11", "req.params.id", "Access the path parameter id"]
]
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
res.json({
message: `[GET] Hello ${req.params.id}!`,
})
}
Learn more about path parameters in this documentation.
API routes can accept query parameters:
export const queryHighlights = [
["11", "req.query.name", "Access the query parameter name"],
]
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
res.json({
message: `Hello ${req.query.name}`,
})
}
Learn more about query parameters in this documentation.
API routes can accept request body parameters:
export const bodyHighlights = [
["11", "HelloWorldReq", "Specify the type of the request body parameters."],
["15", "req.body.name", "Access the request body parameter name"],
]
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
type HelloWorldReq = {
name: string
}
export const POST = async (
req: MedusaRequest<HelloWorldReq>,
res: MedusaResponse
) => {
res.json({
message: `[POST] Hello ${req.body.name}!`,
})
}
Learn more about request body parameters in this documentation.
You can change the response code of an API route:
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
res.status(200).json({
message: "Hello, World!",
})
}
Learn more about setting the response code in this documentation.
To execute a workflow in an API route:
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import myWorkflow from "../../workflows/hello-world"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
) {
const { result } = await myWorkflow(req.scope)
.run({
input: {
name: req.query.name as string,
},
})
res.send(result)
}
Learn more in this documentation.
By default, an API route's response has the content type application/json.
To change it to another content type, use the writeHead method of MedusaResponse:
export const responseContentTypeHighlights = [ ["7", "writeHead", "Change the content type in the header."] ]
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
})
const interval = setInterval(() => {
res.write("Streaming data...\n")
}, 3000)
req.on("end", () => {
clearInterval(interval)
res.end()
})
}
This changes the response type to return an event stream.
Learn more in this documentation.
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:
import type {
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
defineMiddlewares,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/custom*",
middlewares: [
(
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) => {
console.log("Received a request!")
next()
},
],
},
{
matcher: "/custom/:id",
middlewares: [
(
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) => {
console.log("With Path Parameter")
next()
},
],
},
],
})
Learn more about middlewares in this documentation.
To restrict a middleware to an HTTP method:
export const middlewareMethodHighlights = [
["12", "method", "Apply the middleware on POST and PUT requests only."]
]
import type {
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
defineMiddlewares,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/custom*",
method: ["POST", "PUT"],
middlewares: [
// ...
],
},
],
})
src/api/custom/validators.ts:As of Medusa v2.13.0, Zod should be imported from @medusajs/framework/zod.
import { z } from "@medusajs/framework/zod"
export const PostStoreCustomSchema = z.object({
a: z.number(),
b: z.number(),
})
src/api/middlewares.ts:import {
validateAndTransformBody,
defineMiddlewares,
} from "@medusajs/framework/http"
import { PostStoreCustomSchema } from "./custom/validators"
export default defineMiddlewares({
routes: [
{
matcher: "/custom",
method: "POST",
middlewares: [
validateAndTransformBody(PostStoreCustomSchema),
],
},
],
})
/custom API route:import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { z } from "@medusajs/framework/zod"
import { PostStoreCustomSchema } from "./validators"
type PostStoreCustomSchemaType = z.infer<
typeof PostStoreCustomSchema
>
export const POST = async (
req: MedusaRequest<PostStoreCustomSchemaType>,
res: MedusaResponse
) => {
res.json({
sum: req.validatedBody.a + req.validatedBody.b,
})
}
Learn more about request body validation in this documentation.
In this example, you'll pass additional data to the Create Product API route, then consume its hook:
<Note>Find this example in details in this documentation.
</Note>src/api/middlewares.ts with the following content:import { defineMiddlewares } from "@medusajs/framework/http"
import { z } from "@medusajs/framework/zod"
export default defineMiddlewares({
routes: [
{
matcher: "/admin/products",
method: ["POST"],
additionalDataValidator: {
brand_id: z.string().optional(),
},
},
],
})
Learn more about additional data in this documentation.
</Note>src/workflows/hooks/created-product.ts with the following content:import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
import { StepResponse } from "@medusajs/framework/workflows-sdk"
createProductsWorkflow.hooks.productsCreated(
(async ({ products, additional_data }, { container }) => {
if (!additional_data.brand_id) {
return new StepResponse([], [])
}
// TODO perform custom action
}),
(async (links, { container }) => {
// TODO undo the action in the compensation
})
)
Learn more about workflow hooks in this documentation.
</Note>You can protect API routes by restricting access to authenticated admin users only.
Add the following middleware in src/api/middlewares.ts:
import {
defineMiddlewares,
authenticate,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/custom/admin*",
middlewares: [
authenticate(
"user",
["session", "bearer", "api-key"]
),
],
},
],
})
Learn more in this documentation.
You can protect API routes by restricting access to authenticated customers only.
Add the following middleware in src/api/middlewares.ts:
import {
defineMiddlewares,
authenticate,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/custom/customer*",
middlewares: [
authenticate("customer", ["session", "bearer"]),
],
},
],
})
Learn more in this documentation.
To retrieve the currently logged-in user in an API route:
<Note>Requires setting up the authentication middleware as explained in this example.
</Note>import type {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const userModuleService = req.scope.resolve(
Modules.USER
)
const user = await userModuleService.retrieveUser(
req.auth_context.actor_id
)
// ...
}
Learn more in this documentation.
To retrieve the currently logged-in customer in an API route:
<Note>Requires setting up the authentication middleware as explained in this example.
</Note>import type {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
if (req.auth_context?.actor_id) {
// retrieve customer
const customerModuleService = req.scope.resolve(
Modules.CUSTOMER
)
const customer = await customerModuleService.retrieveCustomer(
req.auth_context.actor_id
)
}
// ...
}
Learn more in this documentation.
To throw errors in an API route, use MedusaError from the Medusa Framework:
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
if (!req.query.q) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The `q` query parameter is required."
)
}
// ...
}
Learn more in this documentation.
To override the error handler of API routes, create the file src/api/middlewares.ts with the following content:
import {
defineMiddlewares,
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
export default defineMiddlewares({
errorHandler: (
error: MedusaError | any,
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) => {
res.status(400).json({
error: "Something happened.",
})
},
})
Learn more in this documentation,
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:
import type {
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
defineMiddlewares,
} from "@medusajs/framework/http"
import { ConfigModule } from "@medusajs/framework/types"
import { parseCorsOrigins } from "@medusajs/framework/utils"
import cors from "cors"
export default defineMiddlewares({
routes: [
{
matcher: "/custom*",
middlewares: [
(
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) => {
const configModule: ConfigModule =
req.scope.resolve("configModule")
return cors({
origin: parseCorsOrigins(
configModule.projectConfig.http.storeCors
),
credentials: true,
})(req, res, next)
},
],
},
],
})
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:
import {
defineMiddlewares,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/webhooks/*",
bodyParser: { preserveRawBody: true },
method: ["POST"],
},
],
})
To access the raw body data in your route, use the req.rawBody property:
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
export const POST = (
req: MedusaRequest,
res: MedusaResponse
) => {
console.log(req.rawBody)
}
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.
Find this example explained in details in this documentation.
</Note>src/modules/blog.src/modules/blog/models/post.ts with the following data model:import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
id: model.id().primaryKey(),
title: model.text(),
})
export default Post
src/modules/blog/service.ts with the following service:import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
Post,
}){
}
export default BlogModuleService
src/modules/blog/index.ts that exports the module definition:import BlogModuleService from "./service"
import { Module } from "@medusajs/framework/utils"
export const BLOG_MODULE = "blog"
export default Module(BLOG_MODULE, {
service: BlogModuleService,
})
medusa-config.ts:module.exports = defineConfig({
projectConfig: {
// ...
},
modules: [
{
resolve: "./modules/blog",
},
],
})
npx medusa db:generate blog
npx medusa db:migrate
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import BlogModuleService from "../../modules/blog/service"
import { BLOG_MODULE } from "../../modules/blog"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const blogModuleService: BlogModuleService = req.scope.resolve(
BLOG_MODULE
)
const post = await blogModuleService.createPosts({
title: "test",
})
res.json({
post,
})
}
To add services in your module other than the main one, create them in the services directory of the module.
For example, create the file src/modules/blog/services/category.ts with the following content:
export class CategoryService {
// TODO add methods
}
Then, export the service in the file src/modules/blog/services/index.ts:
export * from "./category"
Finally, resolve the service in your module's main service or loader:
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
import { CategoryService } from "./services"
type InjectedDependencies = {
categoryService: CategoryService
}
class BlogModuleService extends MedusaService({
Post,
}){
private categoryService: CategoryService
constructor({ categoryService }: InjectedDependencies) {
super(...arguments)
this.categoryService = categoryService
}
}
export default BlogModuleService
Learn more in this documentation.
A module can accept options for configurations and secrets.
To accept options in your module:
medusa-config.ts:module.exports = defineConfig({
// ...
modules: [
{
resolve: "./modules/blog",
options: {
apiKey: true,
},
},
],
})
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
// recommended to define type in another file
type ModuleOptions = {
apiKey?: boolean
}
export default class BlogModuleService extends MedusaService({
Post,
}){
protected options_: ModuleOptions
constructor({}, options?: ModuleOptions) {
super(...arguments)
this.options_ = options || {
apiKey: false,
}
}
// ...
}
Learn more in this documentation.
An example of integrating a dummy third-party system in a module's service:
import { Logger } from "@medusajs/framework/types"
import { BLOG_MODULE } from ".."
export type ModuleOptions = {
apiKey: string
}
type InjectedDependencies = {
logger: Logger
}
export class BlogClient {
private options_: ModuleOptions
private logger_: Logger
constructor(
{ logger }: InjectedDependencies,
options: ModuleOptions
) {
this.logger_ = logger
this.options_ = options
}
private async sendRequest(url: string, method: string, data?: any) {
this.logger_.info(`Sending a ${
method
} request to ${url}. data: ${JSON.stringify(data, null, 2)}`)
this.logger_.info(`Client Options: ${
JSON.stringify(this.options_, null, 2)
}`)
}
}
Find a longer example of integrating a third-party service in this documentation.
A data model represents a table in the database. Medusa provides a data model language to intuitively create data models.
To create a data model in a module:
<Note>This assumes you already have a module. If not, follow this example.
</Note>src/modules/blog/models/post.ts with the following data model:import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
id: model.id().primaryKey(),
title: model.text(),
})
export default Post
npx medusa db:generate blog
npx medusa db:migrate
Learn more in this documentation.
A data model can have properties of the following types:
const Post = model.define("post", {
id: model.id(),
// ...
})
const Post = model.define("post", {
title: model.text(),
// ...
})
const Post = model.define("post", {
views: model.number(),
// ...
})
const Post = model.define("post", {
price: model.bigNumber(),
// ...
})
const Post = model.define("post", {
isPublished: model.boolean(),
// ...
})
const Post = model.define("post", {
status: model.enum(["draft", "published"]),
// ...
})
const Post = model.define("post", {
publishedAt: model.dateTime(),
// ...
})
const Post = model.define("post", {
metadata: model.json(),
// ...
})
const Post = model.define("post", {
tags: model.array(),
// ...
})
Learn more in this documentation.
To set an id property as the primary key of a data model:
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
id: model.id().primaryKey(),
// ...
})
export default Post
To set a text property as the primary key:
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
title: model.text().primaryKey(),
// ...
})
export default Post
To set a number property as the primary key:
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
views: model.number().primaryKey(),
// ...
})
export default Post
Learn more in this documentation.
To set the default value of a property:
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
status: model
.enum(["draft", "published"])
.default("draft"),
views: model
.number()
.default(0),
// ...
})
export default Post
Learn more in this documentation.
To allow null values for a property:
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
price: model.bigNumber().nullable(),
// ...
})
export default Post
Learn more in this documentation.
To create a unique index on a property:
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
title: model.text().unique(),
// ...
})
export default Post
Learn more in this documentation.
To define a database index on a property:
import { model } from "@medusajs/framework/utils"
const MyCustom = model.define("my_custom", {
id: model.id().primaryKey(),
title: model.text().index(
"IDX_POST_TITLE"
),
})
export default MyCustom
Learn more in this documentation.
To define a composite index on a data model:
import { model } from "@medusajs/framework/utils"
const MyCustom = model.define("my_custom", {
id: model.id().primaryKey(),
name: model.text(),
age: model.number().nullable(),
}).indexes([
{
on: ["name", "age"],
where: {
age: {
$ne: null,
},
},
},
])
export default MyCustom
Learn more in this documentation.
To make a property searchable using terms or keywords:
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
title: model.text().searchable(),
// ...
})
export default Post
Then, to search by that property, pass the q filter to the list or listAndCount generated methods of the module's main service:
blogModuleService is the main service that manages the Post data model.
const posts = await blogModuleService.listPosts({
q: "John",
})
Learn more in this documentation.
The following creates a one-to-one relationship between the User and Email data models:
import { model } from "@medusajs/framework/utils"
const User = model.define("user", {
id: model.id().primaryKey(),
email: model.hasOne(() => Email, {
mappedBy: "user",
}),
})
const Email = model.define("email", {
id: model.id().primaryKey(),
user: model.belongsTo(() => User, {
mappedBy: "email",
}),
})
Learn more in this documentation.
The following creates a one-to-many relationship between the Store and Product data models:
import { model } from "@medusajs/framework/utils"
const Store = model.define("store", {
id: model.id().primaryKey(),
products: model.hasMany(() => Product, {
mappedBy: "store",
}),
})
const Product = model.define("product", {
id: model.id().primaryKey(),
store: model.belongsTo(() => Store, {
mappedBy: "products",
}),
})
Learn more in this documentation.
The following creates a many-to-many relationship between the Order and Product data models:
import { model } from "@medusajs/framework/utils"
const Order = model.define("order", {
id: model.id().primaryKey(),
products: model.manyToMany(() => Product, {
mappedBy: "orders",
}),
})
const Product = model.define("product", {
id: model.id().primaryKey(),
orders: model.manyToMany(() => Order, {
mappedBy: "products",
}),
})
Learn more in this documentation.
To configure cascade on a data model:
import { model } from "@medusajs/framework/utils"
import Product from "./product"
const Store = model.define("store", {
id: model.id().primaryKey(),
products: model.hasMany(() => Product, {
mappedBy: "store",
}),
})
.cascades({
delete: ["products"],
})
This configures the delete cascade on the Store data model so that, when a store is delete, its products are also deleted.
Learn more in this documentation.
Consider you have a one-to-one relationship between Email and User data models, where an email belongs to a user.
To set the ID of the user that an email belongs to:
<Note>blogModuleService is the main service that manages the Email and User data models.
// when creating an email
const email = await blogModuleService.createEmails({
// other properties...
user: "123",
})
// when updating an email
const email = await blogModuleService.updateEmails({
id: "321",
// other properties...
user: "123",
})
And to set the ID of a user's email when creating or updating it:
// when creating a user
const user = await blogModuleService.createUsers({
// other properties...
email: "123",
})
// when updating a user
const user = await blogModuleService.updateUsers({
id: "321",
// other properties...
email: "123",
})
Learn more in this documentation.
Consider you have a one-to-many relationship between Product and Store data models, where a store has many products.
To set the ID of the store that a product belongs to:
<Note>blogModuleService is the main service that manages the Product and Store data models.
// when creating a product
const product = await blogModuleService.createProducts({
// other properties...
store_id: "123",
})
// when updating a product
const product = await blogModuleService.updateProducts({
id: "321",
// other properties...
store_id: "123",
})
Learn more in this documentation
Consider you have a many-to-many relationship between Order and Product data models.
To set the orders a product has when creating it:
<Note>blogModuleService is the main service that manages the Product and Order data models.
const product = await blogModuleService.createProducts({
// other properties...
orders: ["123", "321"],
})
To add new orders to a product without removing the previous associations:
const product = await blogModuleService.retrieveProduct(
"123",
{
relations: ["orders"],
}
)
const updatedProduct = await blogModuleService.updateProducts({
id: product.id,
// other properties...
orders: [
...product.orders.map((order) => order.id),
"321",
],
})
Learn more in this documentation.
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:
blogModuleService is the main service that manages the Product and Order data models.
const product = await blogModuleService.retrieveProducts(
"123",
{
relations: ["orders"],
}
)
Learn more in this documentation.
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.
The service factory MedusaService generates data-management methods for your data models.
To extend the service factory in your module's service:
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
Post,
}){
// TODO implement custom methods
}
export default BlogModuleService
The BlogModuleService will now have data-management methods for Post.
Refer to this reference for details on the generated methods.
Learn more about the service factory in this documentation.
To resolve resources from the module's container in a service:
<CodeTabs group="service-type"> <CodeTab label="With Service Factory" value="service-factory">import { Logger } from "@medusajs/framework/types"
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
type InjectedDependencies = {
logger: Logger
}
class BlogModuleService extends MedusaService({
Post,
}){
protected logger_: Logger
constructor({ logger }: InjectedDependencies) {
super(...arguments)
this.logger_ = logger
this.logger_.info("[BlogModuleService]: Hello World!")
}
// ...
}
export default BlogModuleService
import { Logger } from "@medusajs/framework/types"
type InjectedDependencies = {
logger: Logger
}
export default class BlogModuleService {
protected logger_: Logger
constructor({ logger }: InjectedDependencies) {
this.logger_ = logger
this.logger_.info("[BlogModuleService]: Hello World!")
}
// ...
}
Learn more in this documentation.
To access options passed to a module in its service:
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
// recommended to define type in another file
type ModuleOptions = {
apiKey?: boolean
}
export default class BlogModuleService extends MedusaService({
Post,
}){
protected options_: ModuleOptions
constructor({}, options?: ModuleOptions) {
super(...arguments)
this.options_ = options || {
apiKey: "",
}
}
// ...
}
Learn more in this documentation.
To run database query in your service:
// other imports...
import {
InjectManager,
MedusaContext,
} from "@medusajs/framework/utils"
class BlogModuleService {
// ...
@InjectManager()
async getCount(
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<number> {
return await sharedContext.manager.count("post")
}
@InjectManager()
async getCountSql(
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<number> {
const data = await sharedContext.manager.execute(
"SELECT COUNT(*) as num FROM post"
)
return parseInt(data[0].num)
}
}
Learn more in this documentation
To execute database operations within a transaction in your service:
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@medusajs/framework/mikro-orm/knex"
class BlogModuleService {
// ...
@InjectTransactionManager()
protected async update_(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
const transactionManager = sharedContext.transactionManager
await transactionManager.nativeUpdate(
"post",
{
id: input.id,
},
{
name: input.name,
}
)
// retrieve again
const updatedRecord = await transactionManager.execute(
`SELECT * FROM post WHERE id = '${input.id}'`
)
return updatedRecord
}
@InjectManager()
async update(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
) {
return await this.update_(input, sharedContext)
}
}
Learn more in this documentation.
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:
src/links/blog-product.ts with the following content:import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"
export default defineLink(
ProductModule.linkable.product,
BlogModule.linkable.post
)
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:
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"
export default defineLink(
ProductModule.linkable.product,
{
linkable: BlogModule.linkable.post,
isList: true,
}
)
Learn more about list links in this documentation.
To ensure a model's records linked to another model are deleted when the linked model is deleted:
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"
export default defineLink(
ProductModule.linkable.product,
{
linkable: BlogModule.linkable.post,
deleteCascades: true,
}
)
Learn more in this documentation.
To add a custom column to the table that stores the linked records of two data models:
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"
export default defineLink(
ProductModule.linkable.product,
BlogModule.linkable.post,
{
database: {
extraColumns: {
metadata: {
type: "json",
},
},
},
}
)
Then, to set the custom column when creating or updating a link between records:
await link.create({
[Modules.PRODUCT]: {
product_id: "123",
},
HELLO_MODULE: {
my_custom_id: "321",
},
data: {
metadata: {
test: true,
},
},
})
To retrieve the custom column when retrieving linked records using Query:
import productBlogLink from "../links/product-blog"
// ...
const { data } = await query.graph({
entity: productBlogLink.entryPoint,
fields: ["metadata", "product.*", "post.*"],
filters: {
product_id: "prod_123",
},
})
Learn more in this documentation.
To create a link between two records using Link:
import { Modules } from "@medusajs/framework/utils"
import { BLOG_MODULE } from "../../modules/blog"
// ...
await link.create({
[Modules.PRODUCT]: {
product_id: "prod_123",
},
[HELLO_MODULE]: {
my_custom_id: "mc_123",
},
})
Learn more in this documentation.
To dismiss links between records using Link:
import { Modules } from "@medusajs/framework/utils"
import { BLOG_MODULE } from "../../modules/blog"
// ...
await link.dismiss({
[Modules.PRODUCT]: {
product_id: "prod_123",
},
[BLOG_MODULE]: {
post_id: "mc_123",
},
})
Learn more in this documentation.
To cascade delete records linked to a deleted record:
import { Modules } from "@medusajs/framework/utils"
// ...
await productModuleService.deleteVariants([variant.id])
await link.delete({
[Modules.PRODUCT]: {
product_id: "prod_123",
},
})
Learn more in this documentation.
To restore records that were soft-deleted because they were linked to a soft-deleted record:
import { Modules } from "@medusajs/framework/utils"
// ...
await productModuleService.restoreProducts(["prod_123"])
await link.restore({
[Modules.PRODUCT]: {
product_id: "prod_123",
},
})
Learn more in this documentation.
Query fetches data across modules. It’s a set of methods registered in the Medusa container under the query key.
To retrieve records using Query in an API route:
import {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import {
ContainerRegistrationKeys,
} from "@medusajs/framework/utils"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: myCustoms } = await query.graph({
entity: "my_custom",
fields: ["id", "name"],
})
res.json({ my_customs: myCustoms })
}
Learn more in this documentation.
To retrieve records linked to a data model:
import {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import {
ContainerRegistrationKeys,
} from "@medusajs/framework/utils"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: myCustoms } = await query.graph({
entity: "my_custom",
fields: [
"id",
"name",
"product.*",
],
})
res.json({ my_customs: myCustoms })
}
Learn more in this documentation.
To filter the retrieved records:
import {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import {
ContainerRegistrationKeys,
} from "@medusajs/framework/utils"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: myCustoms } = await query.graph({
entity: "my_custom",
fields: ["id", "name"],
filters: {
id: [
"mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",
"mc_01HWSVWK3KYHKQEE6QGS2JC3FX",
],
},
})
res.json({ my_customs: myCustoms })
}
Learn more in this documentation.
To paginate and sort retrieved records:
import {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import {
ContainerRegistrationKeys,
} from "@medusajs/framework/utils"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const {
data: myCustoms,
metadata: { count, take, skip } = {},
} = await query.graph({
entity: "my_custom",
fields: ["id", "name"],
pagination: {
skip: 0,
take: 10,
order: {
name: "DESC",
},
},
})
res.json({
my_customs: myCustoms,
count,
take,
skip,
})
}
Learn more in this documentation.
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.
To create a workflow:
src/workflows/hello-world/steps/step-1.ts with the following content:import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
export const step1 = createStep("step-1", async () => {
return new StepResponse(`Hello from step one!`)
})
src/workflows/hello-world/steps/step-2.ts with the following content:import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
type StepInput = {
name: string
}
export const step2 = createStep(
"step-2",
async ({ name }: StepInput) => {
return new StepResponse(`Hello ${name} from step two!`)
}
)
src/workflows/hello-world/index.ts with the following content:import {
createWorkflow,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { step1 } from "./steps/step-1"
import { step2 } from "./steps/step-2"
const myWorkflow = createWorkflow(
"hello-world",
function (input: WorkflowInput) {
const str1 = step1()
// to pass input
const str2 = step2(input)
return new WorkflowResponse({
message: str1,
})
}
)
export default myWorkflow
Learn more in this documentation.
```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import myWorkflow from "../../workflows/hello-world"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
) {
const { result } = await myWorkflow(req.scope)
.run({
input: {
name: req.query.name as string,
},
})
res.send(result)
}
```
```ts title="src/subscribers/customer-created.ts" highlights={[["20"], ["21"], ["22"], ["23"], ["24"], ["25"]]} collapsibleLines="1-9" expandButtonLabel="Show Imports"
import {
type SubscriberConfig,
type SubscriberArgs,
} from "@medusajs/framework"
import myWorkflow from "../workflows/hello-world"
import { Modules } from "@medusajs/framework/utils"
import { IUserModuleService } from "@medusajs/framework/types"
export default async function handleCustomerCreate({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const userId = data.id
const userModuleService: IUserModuleService = container.resolve(
Modules.USER
)
const user = await userModuleService.retrieveUser(userId)
const { result } = await myWorkflow(container)
.run({
input: {
name: user.first_name,
},
})
console.log(result)
}
export const config: SubscriberConfig = {
event: "user.created",
}
```
```ts title="src/jobs/message-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"], ["11"], ["12"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import myWorkflow from "../workflows/hello-world"
export default async function myCustomJob(
container: MedusaContainer
) {
const { result } = await myWorkflow(container)
.run({
input: {
name: "John",
},
})
console.log(result.message)
}
export const config = {
name: "run-once-a-day",
schedule: `0 0 * * *`,
};
```
Learn more in this documentation.
Pass a compensation function that undoes what a step did as a second parameter to createStep:
import {
createStep,
StepResponse,
} from "@medusajs/framework/workflows-sdk"
const step1 = createStep(
"step-1",
async () => {
const message = `Hello from step one!`
console.log(message)
return new StepResponse(message)
},
async () => {
console.log("Oops! Rolling back my changes...")
}
)
Learn more in this documentation.
To manipulate variables within a workflow's constructor function, use transform from the Workflows SDK:
import {
createWorkflow,
WorkflowResponse,
transform,
} from "@medusajs/framework/workflows-sdk"
// step imports...
const myWorkflow = createWorkflow(
"hello-world",
function (input) {
const str1 = step1(input)
const str2 = step2(input)
const str3 = transform(
{ str1, str2 },
(data) => `${data.str1}${data.str2}`
)
return new WorkflowResponse(str3)
}
)
Learn more in this documentation
To perform steps or set a variable's value based on a condition, use when-then from the Workflows SDK:
import {
createWorkflow,
WorkflowResponse,
when,
} from "@medusajs/framework/workflows-sdk"
// step imports...
const workflow = createWorkflow(
"workflow",
function (input: {
is_active: boolean
}) {
const result = when(
input,
(input) => {
return input.is_active
}
).then(() => {
return isActiveStep()
})
// executed without condition
const anotherStepResult = anotherStep(result)
return new WorkflowResponse(
anotherStepResult
)
}
)
To run a workflow in another, use the workflow's runAsStep special method:
import {
createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import {
createProductsWorkflow,
} from "@medusajs/medusa/core-flows"
const workflow = createWorkflow(
"hello-world",
async (input) => {
const products = createProductsWorkflow.runAsStep({
input: {
products: [
// ...
],
},
})
// ...
}
)
Learn more in this documentation.
To consume a workflow hook, create a file under src/workflows/hooks:
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
createProductsWorkflow.hooks.productsCreated(
async ({ products, additional_data }, { container }) => {
// TODO perform an action
},
async (dataFromStep, { container }) => {
// undo the performed action
}
)
This executes a custom step at the hook's designated point in the workflow.
Learn more in this documentation.
To expose a hook in a workflow, pass it in the second parameter of the returned WorkflowResponse:
import {
createStep,
createHook,
createWorkflow,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { createProductStep } from "./steps/create-product"
export const myWorkflow = createWorkflow(
"my-workflow",
function (input) {
const product = createProductStep(input)
const productCreatedHook = createHook(
"productCreated",
{ productId: product.id }
)
return new WorkflowResponse(product, {
hooks: [productCreatedHook],
})
}
)
Learn more in this documentation.
To configure steps to retry in case of errors, pass the maxRetries step option:
import {
createStep,
} from "@medusajs/framework/workflows-sdk"
export const step1 = createStep(
{
name: "step-1",
maxRetries: 2,
},
async () => {
console.log("Executing step 1")
throw new Error("Oops! Something happened.")
}
)
Learn more in this documentation.
If steps in a workflow don't depend on one another, run them in parallel using parallel from the Workflows SDK:
import {
createWorkflow,
WorkflowResponse,
parallelize,
} from "@medusajs/framework/workflows-sdk"
import {
createProductStep,
getProductStep,
createPricesStep,
attachProductToSalesChannelStep,
} from "./steps"
interface WorkflowInput {
title: string
}
const myWorkflow = createWorkflow(
"my-workflow",
(input: WorkflowInput) => {
const product = createProductStep(input)
const [prices, productSalesChannel] = parallelize(
createPricesStep(product),
attachProductToSalesChannelStep(product)
)
const id = product.id
const refetchedProduct = getProductStep(product.id)
return new WorkflowResponse(refetchedProduct)
}
)
Learn more in this documentation.
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:
import {
createStep,
createWorkflow,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
// step import...
const myWorkflow = createWorkflow({
name: "hello-world",
timeout: 2, // 2 seconds
}, function () {
const str1 = step1()
return new WorkflowResponse({
message: str1,
})
})
export default myWorkflow
Learn more in this documentation.
To configure a step's timeout, at which its state changes but its execution isn't stopped, use the timeout property:
const step1 = createStep(
{
name: "step-1",
timeout: 2, // 2 seconds
},
async () => {
// ...
}
)
Learn more in this documentation.
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:
const step2 = createStep(
{
name: "step-2",
async: true,
},
async () => {
console.log("Waiting to be successful...")
}
)
Learn more in this documentation.
To change a step's status:
const { transaction } = await myLongRunningWorkflow(req.scope)
.run()
export const stepSuccessHighlights = [ ["5", "setStepSuccess", "Change a step's status to success"], ["8", "transactionId", "Pass the workflow's transaction ID"], ["9", "stepId", "The ID of the step to change its status."], ["10", "workflowId", "The ID of the workflow that the step belongs to."] ]
const workflowEngineService = container.resolve(
Modules.WORKFLOW_ENGINE
)
await workflowEngineService.setStepSuccess({
idempotencyKey: {
action: TransactionHandlerType.INVOKE,
transactionId,
stepId: "step-2",
workflowId: "hello-world",
},
stepResponse: new StepResponse("Done!"),
options: {
container,
},
})
export const stepFailureHighlights = [ ["5", "setStepFailure", "Change a step's status to failure"], ["8", "transactionId", "Pass the workflow's transaction ID"], ["9", "stepId", "The ID of the step to change its status."], ["10", "workflowId", "The ID of the workflow that the step belongs to."] ]
const workflowEngineService = container.resolve(
Modules.WORKFLOW_ENGINE
)
await workflowEngineService.setStepFailure({
idempotencyKey: {
action: TransactionHandlerType.INVOKE,
transactionId,
stepId: "step-2",
workflowId: "hello-world",
},
stepResponse: new StepResponse("Failed!"),
options: {
container,
},
})
Learn more in this documentation.
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:
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import myWorkflow from "../../../workflows/hello-world"
import { Modules } from "@medusajs/framework/utils"
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const { transaction, result } = await myWorkflow(req.scope).run()
const workflowEngineService = req.scope.resolve(
Modules.WORKFLOW_ENGINE
)
const subscriptionOptions = {
workflowId: "hello-world",
transactionId: transaction.transactionId,
subscriberId: "hello-world-subscriber",
}
await workflowEngineService.subscribe({
...subscriptionOptions,
subscriber: async (data) => {
if (data.eventType === "onFinish") {
console.log("Finished execution", data.result)
// unsubscribe
await workflowEngineService.unsubscribe({
...subscriptionOptions,
subscriberOrId: subscriptionOptions.subscriberId,
})
} else if (data.eventType === "onStepFailure") {
console.log("Workflow failed", data.step)
}
},
})
res.send(result)
}
Learn more in this documentation.
A subscriber is a function executed whenever the event it listens to is emitted.
To create a subscriber that listens to the product.created event, create the file src/subscribers/product-created.ts with the following content:
import type {
SubscriberArgs,
SubscriberConfig,
} from "@medusajs/framework"
export default async function productCreateHandler({
event,
}: SubscriberArgs<{ id: string }>) {
const productId = event.data.id
console.log(`The product ${productId} was created`)
}
export const config: SubscriberConfig = {
event: "product.created",
}
Learn more in this documentation.
To resolve resources from the Medusa container in a subscriber, use the container property of its parameter:
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"
export default async function productCreateHandler({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const productModuleService = container.resolve(Modules.PRODUCT)
const productId = data.id
const product = await productModuleService.retrieveProduct(
productId
)
console.log(`The product ${product.title} was created`)
}
export const config: SubscriberConfig = {
event: `product.created`,
}
Learn more in this documentation.
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:
import {
SubscriberArgs,
type SubscriberConfig,
} from "@medusajs/medusa"
import { Modules } from "@medusajs/framework/utils"
export default async function resetPasswordTokenHandler({
event: { data: {
entity_id: email,
token,
actor_type,
} },
container,
}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) {
const notificationModuleService = container.resolve(
Modules.NOTIFICATION
)
const urlPrefix = actor_type === "customer" ?
"https://storefront.com" :
"https://admin.com"
await notificationModuleService.createNotifications({
to: email,
channel: "email",
template: "reset-password-template",
data: {
// a URL to a frontend application
url: `${urlPrefix}/reset-password?token=${token}&email=${email}`,
},
})
}
export const config: SubscriberConfig = {
event: "auth.password_reset",
}
Learn more in this documentation.
To execute a workflow in a subscriber:
import {
type SubscriberConfig,
type SubscriberArgs,
} from "@medusajs/framework"
import myWorkflow from "../workflows/hello-world"
import { Modules } from "@medusajs/framework/utils"
export default async function handleCustomerCreate({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const userId = data.id
const userModuleService = container.resolve(
Modules.USER
)
const user = await userModuleService.retrieveUser(userId)
const { result } = await myWorkflow(container)
.run({
input: {
name: user.first_name,
},
})
console.log(result)
}
export const config: SubscriberConfig = {
event: "user.created",
}
Learn more in this documentation
A scheduled job is a function executed at a specified interval of time in the background of your Medusa application.
To create a scheduled job, create the file src/jobs/hello-world.ts with the following content:
// the scheduled-job function
export default function () {
console.log("Time to say hello world!")
}
// the job's configurations
export const config = {
name: "every-minute-message",
// execute every minute
schedule: "* * * * *",
}
Learn more in this documentation.
To resolve resources in a scheduled job, use the container accepted as a first parameter:
import { MedusaContainer } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
export default async function myCustomJob(
container: MedusaContainer
) {
const productModuleService = container.resolve(Modules.PRODUCT)
const [, count] = await productModuleService.listAndCountProducts()
console.log(
`Time to check products! You have ${count} product(s)`
)
}
export const config = {
name: "every-minute-message",
// execute every minute
schedule: "* * * * *",
}
Learn more in this documentation
To limit the scheduled job's execution to a number of times during the Medusa application's runtime, use the numberOfExecutions configuration:
export default async function myCustomJob() {
console.log("I'll be executed three times only.")
}
export const config = {
name: "hello-world",
// execute every minute
schedule: "* * * * *",
numberOfExecutions: 3,
}
Learn more in this documentation.
To execute a workflow in a scheduled job:
import { MedusaContainer } from "@medusajs/framework/types"
import myWorkflow from "../workflows/hello-world"
export default async function myCustomJob(
container: MedusaContainer
) {
const { result } = await myWorkflow(container)
.run({
input: {
name: "John",
},
})
console.log(result.message)
}
export const config = {
name: "run-once-a-day",
schedule: `0 0 * * *`,
}
Learn more in this documentation
A loader is a function defined in a module that's executed when the Medusa application starts.
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:
export default async function helloWorldLoader() {
console.log(
"[HELLO MODULE] Just started the Medusa application!"
)
}
Learn more in this documentation.
To resolve resources in a loader, use the container property of its first parameter:
import {
LoaderOptions,
} from "@medusajs/framework/types"
import {
ContainerRegistrationKeys,
} from "@medusajs/framework/utils"
export default async function helloWorldLoader({
container,
}: LoaderOptions) {
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
logger.info("[helloWorldLoader]: Hello, World!")
}
Learn more in this documentation.
To access a module's options in its loader, use the options property of its first parameter:
import {
LoaderOptions,
} from "@medusajs/framework/types"
// recommended to define type in another file
type ModuleOptions = {
apiKey?: boolean
}
export default async function helloWorldLoader({
options,
}: LoaderOptions<ModuleOptions>) {
console.log(
"[HELLO MODULE] Just started the Medusa application!",
options
)
}
Learn more in this documentation.
To register a resource in the Module's container using a loader, use the container's registerAdd method:
import {
LoaderOptions,
} from "@medusajs/framework/types"
import { asValue } from "@medusajs/framework/awilix"
export default async function helloWorldLoader({
container,
}: LoaderOptions) {
container.registerAdd(
"custom_data",
asValue({
test: true,
})
)
}
Where the first parameter of registerAdd is the name to register the resource under, and the second parameter is the resource to register.
You can customize the Medusa Admin to inject widgets in existing pages, or create new pages using UI routes.
<Note>For a list of components to use in the admin dashboard, refer to this documentation.
</Note>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:
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"
const ProductWidget = () => {
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">Product Widget</Heading>
</div>
</Container>
)
}
export const config = defineWidgetConfig({
zone: "product.list.before",
})
export default ProductWidget
Learn more about widgets in this documentation.
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):
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"
import {
DetailWidgetProps,
AdminProduct,
} from "@medusajs/framework/types"
// The widget
const ProductWidget = ({
data,
}: DetailWidgetProps<AdminProduct>) => {
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">
Product Widget {data.title}
</Heading>
</div>
</Container>
)
}
// The widget's configurations
export const config = defineWidgetConfig({
zone: "product.details.before",
})
export default ProductWidget
Learn more in this documentation.
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:
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"
const CustomPage = () => {
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">This is my custom route</Heading>
</div>
</Container>
)
}
export const config = defineRouteConfig({
label: "Custom Route",
icon: ChatBubbleLeftRight,
})
export default CustomPage
This adds a new page at localhost:9000/app/custom.
Learn more in this documentation.
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:
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"
const CustomSettingPage = () => {
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h1">Custom Setting Page</Heading>
</div>
</Container>
)
}
export const config = defineRouteConfig({
label: "Custom",
})
export default CustomSettingPage
This adds a setting page at localhost:9000/app/settings/custom.
Learn more in this documentation
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:
import { useParams } from "react-router-dom"
import { Container } from "@medusajs/ui"
const CustomPage = () => {
const { id } = useParams()
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h1">Passed ID: {id}</Heading>
</div>
</Container>
)
}
export default CustomPage
This creates a UI route at localhost:9000/app/custom/:id, where :id is a path parameter.
Learn more in this documentation
To send a request to custom API routes from the admin dashboard, use the Fetch API.
For example:
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "@medusajs/ui"
import { useEffect, useState } from "react"
const ProductWidget = () => {
const [productsCount, setProductsCount] = useState(0)
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!loading) {
return
}
fetch(`/admin/products`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ count }) => {
setProductsCount(count)
setLoading(false)
})
}, [loading])
return (
<Container className="divide-y p-0">
{loading && <span>Loading...</span>}
{!loading && <span>You have {productsCount} Product(s).</span>}
</Container>
)
}
export const config = defineWidgetConfig({
zone: "product.list.before",
})
export default ProductWidget
Learn more in this documentation
To add a link to another page in a UI route or a widget, use react-router-dom's Link component:
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "@medusajs/ui"
import { Link } from "react-router-dom"
// The widget
const ProductWidget = () => {
return (
<Container className="divide-y p-0">
<Link to={"/orders"}>View Orders</Link>
</Container>
)
}
// The widget's configurations
export const config = defineWidgetConfig({
zone: "product.details.before",
})
export default ProductWidget
Learn more in this documentation.
Medusa provides a @medusajs/test-utils package with utility tools to create integration tests for your custom API routes, modules, or other Medusa customizations.
For details on setting up your project for integration tests, refer to this documentation.
</Note>To create a test for a custom API route, create the file integration-tests/http/custom-routes.spec.ts with the following content:
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
medusaIntegrationTestRunner({
testSuite: ({ api, getContainer }) => {
describe("Custom endpoints", () => {
describe("GET /custom", () => {
it("returns correct message", async () => {
const response = await api.get(
`/custom`
)
expect(response.status).toEqual(200)
expect(response.data).toHaveProperty("message")
expect(response.data.message).toEqual("Hello, World!")
})
})
})
},
})
Then, run the test with the following command:
npm run test:integration
Learn more in this documentation.
To create a test for a workflow, create the file integration-tests/http/workflow.spec.ts with the following content:
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { helloWorldWorkflow } from "../../src/workflows/hello-world"
medusaIntegrationTestRunner({
testSuite: ({ getContainer }) => {
describe("Test hello-world workflow", () => {
it("returns message", async () => {
const { result } = await helloWorldWorkflow(getContainer())
.run()
expect(result).toEqual("Hello, World!")
})
})
},
})
Then, run the test with the following command:
npm run test:integration
Learn more in this documentation.
To create a test for a module's service, create the test under the __tests__ directory of the module.
For example, create the file src/modules/blog/__tests__/service.spec.ts with the following content:
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { BLOG_MODULE } from ".."
import BlogModuleService from "../service"
import Post from "../models/post"
moduleIntegrationTestRunner<BlogModuleService>({
moduleName: BLOG_MODULE,
moduleModels: [Post],
resolve: "./modules/blog",
testSuite: ({ service }) => {
describe("BlogModuleService", () => {
it("says hello world", () => {
const message = service.getMessage()
expect(message).toEqual("Hello, World!")
})
})
},
})
Then, run the test with the following command:
npm run test:modules
Medusa provides all its commerce features as separate Commerce Modules, such as the Product or Order modules.
<Note>Refer to the Commerce Modules documentation for concepts and reference of every module's main service.
</Note>To create an actor type that can authenticate to the Medusa application, such as a manager:
import { model } from "@medusajs/framework/utils"
const Manager = model.define("manager", {
id: model.id().primaryKey(),
firstName: model.text(),
lastName: model.text(),
email: model.text(),
})
export default Manager
setAuthAppMetadataStep as a step in a workflow that creates a manager:import {
createWorkflow,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
setAuthAppMetadataStep,
} from "@medusajs/medusa/core-flows"
// other imports...
const createManagerWorkflow = createWorkflow(
"create-manager",
function (input: CreateManagerWorkflowInput) {
const manager = createManagerStep({
manager: input.manager,
})
setAuthAppMetadataStep({
authIdentityId: input.authIdentityId,
actorType: "manager",
value: manager.id,
})
return new WorkflowResponse(manager)
}
)
import type {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import createManagerWorkflow from "../../workflows/create-manager"
type RequestBody = {
first_name: string
last_name: string
email: string
}
export async function POST(
req: AuthenticatedMedusaRequest<RequestBody>,
res: MedusaResponse
) {
// If `actor_id` is present, the request carries
// authentication for an existing manager
if (req.auth_context.actor_id) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Request already authenticated as a manager."
)
}
const { result } = await createManagerWorkflow(req.scope)
.run({
input: {
manager: req.body,
authIdentityId: req.auth_context.auth_identity_id,
},
})
res.status(200).json({ manager: result })
}
authenticate middleware on the new route in src/api/middlewares.ts:import {
defineMiddlewares,
authenticate,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/manager",
method: "POST",
middlewares: [
authenticate("manager", ["session", "bearer"], {
allowUnregistered: true,
}),
],
},
{
matcher: "/manager/me*",
middlewares: [
authenticate("manager", ["session", "bearer"]),
],
},
],
})
Now, manager users can use the /manager API route to register, and all routes starting with /manager/me are only accessible by authenticated managers.
Find an elaborate example and learn more in this documentation.
To apply a promotion on a cart's items and shipping methods using the Cart and Promotion modules:
import {
ComputeActionAdjustmentLine,
ComputeActionItemLine,
ComputeActionShippingLine,
AddItemAdjustmentAction,
AddShippingMethodAdjustment,
// ...
} from "@medusajs/framework/types"
// retrieve the cart
const cart = await cartModuleService.retrieveCart("cart_123", {
relations: [
"items.adjustments",
"shipping_methods.adjustments",
],
})
// retrieve line item adjustments
const lineItemAdjustments: ComputeActionItemLine[] = []
cart.items.forEach((item) => {
const filteredAdjustments = item.adjustments?.filter(
(adjustment) => adjustment.code !== undefined
) as unknown as ComputeActionAdjustmentLine[]
if (filteredAdjustments.length) {
lineItemAdjustments.push({
...item,
adjustments: filteredAdjustments,
})
}
})
// retrieve shipping method adjustments
const shippingMethodAdjustments: ComputeActionShippingLine[] =
[]
cart.shipping_methods.forEach((shippingMethod) => {
const filteredAdjustments =
shippingMethod.adjustments?.filter(
(adjustment) => adjustment.code !== undefined
) as unknown as ComputeActionAdjustmentLine[]
if (filteredAdjustments.length) {
shippingMethodAdjustments.push({
...shippingMethod,
adjustments: filteredAdjustments,
})
}
})
// compute actions
const actions = await promotionModuleService.computeActions(
["promo_123"],
{
items: lineItemAdjustments,
shipping_methods: shippingMethodAdjustments,
}
)
// set the adjustments on the line item
await cartModuleService.setLineItemAdjustments(
cart.id,
actions.filter(
(action) => action.action === "addItemAdjustment"
) as AddItemAdjustmentAction[]
)
// set the adjustments on the shipping method
await cartModuleService.setShippingMethodAdjustments(
cart.id,
actions.filter(
(action) =>
action.action === "addShippingMethodAdjustment"
) as AddShippingMethodAdjustment[]
)
Learn more in this documentation.
To retrieve the tax lines of a cart's items and shipping methods using the Cart and Tax modules:
// retrieve the cart
const cart = await cartModuleService.retrieveCart("cart_123", {
relations: [
"items.tax_lines",
"shipping_methods.tax_lines",
"shipping_address",
],
})
// retrieve the tax lines
const taxLines = await taxModuleService.getTaxLines(
[
...(cart.items as TaxableItemDTO[]),
...(cart.shipping_methods as TaxableShippingDTO[]),
],
{
address: {
...cart.shipping_address,
country_code:
cart.shipping_address.country_code || "us",
},
}
)
// set line item tax lines
await cartModuleService.setLineItemTaxLines(
cart.id,
taxLines.filter((line) => "line_item_id" in line)
)
// set shipping method tax lines
await cartModuleService.setLineItemTaxLines(
cart.id,
taxLines.filter((line) => "shipping_line_id" in line)
)
Learn more in this documentation
To apply a promotion on an order's items and shipping methods using the Order and Promotion modules:
import {
ComputeActionAdjustmentLine,
ComputeActionItemLine,
ComputeActionShippingLine,
AddItemAdjustmentAction,
AddShippingMethodAdjustment,
// ...
} from "@medusajs/framework/types"
// ...
// retrieve the order
const order = await orderModuleService.retrieveOrder("ord_123", {
relations: [
"items.item.adjustments",
"shipping_methods.shipping_method.adjustments",
],
})
// retrieve the line item adjustments
const lineItemAdjustments: ComputeActionItemLine[] = []
order.items.forEach((item) => {
const filteredAdjustments = item.adjustments?.filter(
(adjustment) => adjustment.code !== undefined
) as unknown as ComputeActionAdjustmentLine[]
if (filteredAdjustments.length) {
lineItemAdjustments.push({
...item,
...item.detail,
adjustments: filteredAdjustments,
})
}
})
//retrieve shipping method adjustments
const shippingMethodAdjustments: ComputeActionShippingLine[] =
[]
order.shipping_methods.forEach((shippingMethod) => {
const filteredAdjustments =
shippingMethod.adjustments?.filter(
(adjustment) => adjustment.code !== undefined
) as unknown as ComputeActionAdjustmentLine[]
if (filteredAdjustments.length) {
shippingMethodAdjustments.push({
...shippingMethod,
adjustments: filteredAdjustments,
})
}
})
// compute actions
const actions = await promotionModuleService.computeActions(
["promo_123"],
{
items: lineItemAdjustments,
shipping_methods: shippingMethodAdjustments,
// TODO infer from cart or region
currency_code: "usd",
}
)
// set the adjustments on the line items
await orderModuleService.setOrderLineItemAdjustments(
order.id,
actions.filter(
(action) => action.action === "addItemAdjustment"
) as AddItemAdjustmentAction[]
)
// set the adjustments on the shipping methods
await orderModuleService.setOrderShippingMethodAdjustments(
order.id,
actions.filter(
(action) =>
action.action === "addShippingMethodAdjustment"
) as AddShippingMethodAdjustment[]
)
Learn more in this documentation
To accept payment using the Payment Module's main service:
import {
ContainerRegistrationKeys,
Modules,
} from "@medusajs/framework/utils"
// ...
const paymentCollection =
await paymentModuleService.createPaymentCollections({
region_id: "reg_123",
currency_code: "usd",
amount: 5000,
})
// resolve Link
const link = container.resolve(
ContainerRegistrationKeys.LINK
)
// create a link between the cart and payment collection
link.create({
[Modules.CART]: {
cart_id: "cart_123",
},
[Modules.PAYMENT]: {
payment_collection_id: paymentCollection.id,
},
})
const paymentSession =
await paymentModuleService.createPaymentSession(
paymentCollection.id,
{
provider_id: "stripe",
currency_code: "usd",
amount: 5000,
data: {
// any necessary data for the
// payment provider
},
}
)
const payment =
await paymentModuleService.authorizePaymentSession(
paymentSession.id,
{}
)
Learn more in this documentation.
To get prices of a product variant for a region and currency using Query:
import { QueryContext } from "@medusajs/framework/utils"
// ...
const { data: products } = await query.graph({
entity: "product",
fields: [
"*",
"variants.*",
"variants.calculated_price.*",
],
filters: {
id: "prod_123",
},
context: {
variants: {
calculated_price: QueryContext({
region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS",
currency_code: "eur",
}),
},
},
})
Learn more in this documentation.
To get all prices of a product variant using Query:
const { data: products } = await query.graph({
entity: "product",
fields: [
"*",
"variants.*",
"variants.prices.*",
],
filters: {
id: [
"prod_123",
],
},
})
Learn more in this documentation.
To get a variant's prices with taxes using Query and the Tax Module
import {
HttpTypes,
TaxableItemDTO,
ItemTaxLineDTO,
} from "@medusajs/framework/types"
import {
QueryContext,
calculateAmountsWithTax,
} from "@medusajs/framework/utils"
// other imports...
// ...
const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => {
return product.variants
?.map((variant) => {
if (!variant.calculated_price) {
return
}
return {
id: variant.id,
product_id: product.id,
product_name: product.title,
product_categories: product.categories?.map((c) => c.name),
product_category_id: product.categories?.[0]?.id,
product_sku: variant.sku,
product_type: product.type,
product_type_id: product.type_id,
quantity: 1,
unit_price: variant.calculated_price.calculated_amount,
currency_code: variant.calculated_price.currency_code,
}
})
.filter((v) => !!v) as unknown as TaxableItemDTO[]
}
const { data: products } = await query.graph({
entity: "product",
fields: [
"*",
"variants.*",
"variants.calculated_price.*",
],
filters: {
id: "prod_123",
},
context: {
variants: {
calculated_price: QueryContext({
region_id: "region_123",
currency_code: "usd",
}),
},
},
})
const taxLines = (await taxModuleService.getTaxLines(
products.map(asTaxItem).flat(),
{
// example of context properties. You can pass other ones.
address: {
country_code,
},
}
)) as unknown as ItemTaxLineDTO[]
const taxLinesMap = new Map<string, ItemTaxLineDTO[]>()
taxLines.forEach((taxLine) => {
const variantId = taxLine.line_item_id
if (!taxLinesMap.has(variantId)) {
taxLinesMap.set(variantId, [])
}
taxLinesMap.get(variantId)?.push(taxLine)
})
products.forEach((product) => {
product.variants?.forEach((variant) => {
if (!variant.calculated_price) {
return
}
const taxLinesForVariant = taxLinesMap.get(variant.id) || []
const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({
taxLines: taxLinesForVariant,
amount: variant.calculated_price!.calculated_amount!,
includesTax:
variant.calculated_price!.is_calculated_price_tax_inclusive!,
})
// do something with prices...
})
})
Learn more in this documentation.
To invite a user using the User Module:
const invite = await userModuleService.createInvites({
email: "[email protected]",
})
To accept an invite and create a user using the User Module:
const invite =
await userModuleService.validateInviteToken(inviteToken)
await userModuleService.updateInvites({
id: invite.id,
accepted: true,
})
const user = await userModuleService.createUsers({
email: invite.email,
})