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:
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.
Resolve Resources in API Route#
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.
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:
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.
Use Query Parameters#
API routes can accept query parameters:
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.
Use Body Parameters#
API routes can accept request body parameters:
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.
Set Response Code#
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.
Execute a Workflow in an API Route#
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.
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:
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.
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:
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.
Restrict HTTP Methods in Middleware#
To restrict a middleware to an HTTP method:
import type {
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
defineMiddlewares,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/custom*",
method: ["POST", "PUT"],
middlewares: [
// ...
],
},
],
})
Add Validation for Custom Routes#
- Create a Zod schema in the file
src/api/custom/validators.ts:
import { z } from "@medusajs/framework/zod"
export const PostStoreCustomSchema = z.object({
a: z.number(),
b: z.number(),
})
- Add a validation middleware to the custom route in
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),
],
},
],
})
- Use the validated body in the
/customAPI 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.
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.tswith 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(),
},
},
],
})
- Create the file
src/workflows/hooks/created-product.tswith 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
})
)
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:
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.
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:
import {
defineMiddlewares,
authenticate,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/custom/customer*",
middlewares: [
authenticate("customer", ["session", "bearer"]),
],
},
],
})
Learn more in this documentation.
Retrieve Logged-In Admin User#
To retrieve the currently logged-in user in an API route:
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.
Retrieve Logged-In Customer#
To retrieve the currently logged-in customer in an API route:
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.
Throw Errors in API Route#
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.
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:
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,
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:
import type {
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
defineMiddlewares,
} from "@medusajs/framework/http"
import { ConfigModule } from "@medusajs/framework/types"
import {
parseCorsOrigins,
ContainerRegistrationKeys,
} 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(
ContainerRegistrationKeys.CONFIG_MODULE
)
return cors({
origin: parseCorsOrigins(
configModule.projectConfig.http.storeCors
),
credentials: true,
})(req, res, next)
},
],
},
],
})
Parse Webhook Body#
By default, the Medusa application parses a request's body using JSON.
To parse a webhook's body, create the file src/api/middlewares.ts with the following content:
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)
}
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/blog. - Create the file
src/modules/blog/models/post.tswith 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
- Create the file
src/modules/blog/service.tswith the following service:
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
Post,
}){
}
export default BlogModuleService
- Create the file
src/modules/blog/index.tsthat 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,
})
- Add the module to the configurations in
medusa-config.ts:
module.exports = defineConfig({
projectConfig: {
// ...
},
modules: [
{
resolve: "./modules/blog",
},
],
})
- Generate and run migrations:
- Create a workflow that uses the module's main service in a step in the file
src/workflows/create-post.ts:
import {
createStep,
createWorkflow,
StepResponse,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import BlogModuleService from "../modules/blog/service"
import { BLOG_MODULE } from "../modules/blog"
type CreatePostInput = {
title: string
}
const createPostStep = createStep(
"create-post",
async (input: CreatePostInput, { container }) => {
const blogModuleService: BlogModuleService =
container.resolve(BLOG_MODULE)
const post = await blogModuleService.createPosts(input)
return new StepResponse(post, post.id)
},
async (postId, { container }) => {
if (!postId) {
return
}
const blogModuleService: BlogModuleService =
container.resolve(BLOG_MODULE)
await blogModuleService.deletePosts(postId)
}
)
const createPostWorkflow = createWorkflow(
"create-post",
(input: CreatePostInput) => {
const post = createPostStep(input)
return new WorkflowResponse(post)
}
)
export default createPostWorkflow
- Execute the workflow in an API route:
import {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import createPostWorkflow from "../../workflows/create-post"
export async function POST(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const { result: post } = await createPostWorkflow(req.scope)
.run({
input: {
title: "test",
},
})
res.json({
post,
})
}
Module with Multiple Services#
To add services in your module other than the main one, create them in the services directory of the module.
For example, create the file src/modules/blog/services/category.ts with the following content:
Then, export the service in the file src/modules/blog/services/index.ts:
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.
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:
module.exports = defineConfig({
// ...
modules: [
{
resolve: "./modules/blog",
options: {
apiKey: true,
},
},
],
})
- Access the options in the module's main 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: false,
}
}
// ...
}
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:
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.
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/blog/models/post.tswith 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
- 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:
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.
Default Property Value#
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.
Nullable Property#
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.
Unique Property#
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.
Define Database Index on Property#
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.
Define Composite Index on Data Model#
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.
Make a Property Searchable#
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.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:
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.
Create One-to-Many Relationship#
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.
Create Many-to-Many Relationship#
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.
Configure Cascades of Data Model#
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.
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:
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.
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:
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
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:
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.
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:
blogModuleService is the main service that manages the Product and Order data models.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:
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.
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:
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.
Run Database Query in Service#
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
Execute Database Operations in Transactions#
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.
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/blog-product.tswith 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
)
- 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:
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.
Set Delete Cascade on Link Definition#
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,
deleteCascade: true,
}
)
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:
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.
Create Link Between Records#
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.
Dismiss Link Between Records#
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.
Cascade Delete Linked Records#
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.
Restore Linked Records#
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#
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:
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.
Retrieve Linked Records of Data Model#
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.
Apply Filters to Retrieved Records#
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.
Apply Pagination and Sort Records#
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.
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.tswith 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!`)
})
- Create the second step at
src/workflows/hello-world/steps/step-2.tswith 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!`)
}
)
- Create the workflow at
src/workflows/hello-world/index.tswith 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.
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:
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.
Manipulate Variables in Workflow#
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
Using Conditions in Workflow#
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
)
}
)
Run Workflow in Another#
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",
(input) => {
const products = createProductsWorkflow.runAsStep({
input: {
products: [
// ...
],
},
})
// ...
}
)
Learn more in this documentation.
Consume a Workflow Hook#
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.
Expose a Hook#
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.
Retry Steps#
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.
Run Steps in Parallel#
If steps in a workflow don't depend on one another, run them in parallel using parallel from the Workflows SDK:
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.
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:
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.
Configure Step Timeout#
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.
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:
const step2 = createStep(
{
name: "step-2",
async: true,
},
async () => {
console.log("Waiting to be successful...")
}
)
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 Workflow Engine Module:
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,
},
})
- In an API route, workflow, or other resource, change a step's status to failure using the Worfklow Engine Module:
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.
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:
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.
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:
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.
Resolve Resources in Subscriber#
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.
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:
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.
Execute a Workflow in a Subscriber#
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
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:
// the scheduled-job function
export default async 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.
Resolve Resources in Scheduled Job#
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
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:
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.
Execute a Workflow in a Scheduled Job#
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
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:
export default async function helloWorldLoader() {
console.log(
"[HELLO MODULE] Just started the Medusa application!"
)
}
Learn more in this documentation.
Resolve Resources in Loader#
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.
Access Module Options#
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.
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:
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.
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:
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",
})
export 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 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",
})
export 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:
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.
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:
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
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:
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
Send Request to API Route#
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",
})
export 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:
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",
})
export 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:
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:
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:
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:
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/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:
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:
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
- Use the
setAuthAppMetadataStepas 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)
}
)
- Use the workflow in an API route that creates a user (manager) of the actor type:
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 })
}
- Apply the
authenticatemiddleware on the new route insrc/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.
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:
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.
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:
// 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
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:
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
Accept Payment using Module#
To accept payment using the Payment Module's main service:
- Create a payment collection and link it to the cart:
import {
ContainerRegistrationKeys,
Modules,
} from "@medusajs/framework/utils"
// ...
const paymentCollection =
await paymentModuleService.createPaymentCollections({
region_id: "reg_123",
currency_code: "usd",
amount: 50,
})
// 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,
},
})
- Create a payment session in the collection:
const paymentSession =
await paymentModuleService.createPaymentSession(
paymentCollection.id,
{
provider_id: "stripe",
currency_code: "usd",
amount: 50,
data: {
// any necessary data for the
// payment provider
},
}
)
- 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:
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.
Get All Variant's Prices#
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.
Get Variant Prices with Taxes#
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.
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:
const invite =
await userModuleService.validateInviteToken(inviteToken)
await userModuleService.updateInvites({
id: invite.id,
accepted: true,
})
const user = await userModuleService.createUsers({
email: invite.email,
})