On admin home page, create two simple pages, first one is "Videos" page or widget which is a list of urls, and it should contain an add button to add new url, and remove button to remove a url.
The second page is carsoual slides which should contain slides each slide have asset, description and buttonText. there should be a button to add a new slide and a button to remove a slide.
Workflows Introduction

A workflow is a series of queries and actions that complete a task. Workflows are made up of a series of steps that interact with Medusa’s commerce modules, custom services, or external systems.

You construct a Workflow similar to how you create a JavaScript function, but unlike regular functions, a Medusa Workflow creates an internal representation of your steps. This makes it possible to keep track of your Workflow’s progress, automatically retry failing steps, and, if necessary, roll back steps.

Workflows can be used to define a flow with interactions across multiple systems, integrate third-party services into your commerce application, or automate actions within your application. Any flow with a series of steps can be implemented as a workflow.
Example: Your First Workflow

The tools to build Workflows are installed by default in Medusa projects. For other Node.js projects, you can install the @medusajs/workflows-sdk package from npm.
Create a Step

A workflow is made of a series of steps. A step is created using the createStep utility function.

Create the file src/workflows/hello-world.ts with the following content:
src/workflows/hello-world.ts

import {
createStep,
StepResponse,
} from "@medusajs/workflows-sdk"

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

This creates one step that returns a hello message.
Create a Workflow

Next, you can create a workflow using the createWorkflow function:
src/workflows/hello-world.ts

import {
createStep,
StepResponse,
createWorkflow,
} from "@medusajs/workflows-sdk"

type WorkflowOutput = {
message: string
}

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

const myWorkflow = createWorkflow<unknown, WorkflowOutput>(
"hello-world",
function () {
const str1 = step1()

return {
  message: str1,
}

}
)

export default myWorkflow

This creates a hello-world workflow. When you create a workflow, it’s constructed but not executed yet.
Execute the Workflow

You can execute a workflow from different places within Medusa.

Use API Routes if you want the workflow to execute in response to an API request or a webhook.
Use Subscribers if you want to execute a workflow when an event is triggered.
Use Scheduled Jobs if you want your workflow to execute on a regular schedule.

To execute the workflow, invoke it passing the Medusa container as a parameter, then use its run method:

API Route
Subscribers
Scheduled Job

src/api/store/custom/route.ts

import type {
MedusaRequest,
MedusaResponse
} from "@medusajs/medusa";
import myWorkflow from "../../../workflows/hello-world";

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

res.send(result)
}

If you run your backend and trigger the execution of the workflow (based on where you’re executing it), you should see the message Hello from step one!.
Pass Inputs to Steps

Steps in a workflow can accept parameters.

For example, create a new step that accepts an input and returns a message with that input:
src/workflows/hello-world.ts

type WorkflowInput = {
name: string;
};

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

Then, update the workflow to accept input and pass it to the new step:
src/workflows/hello-world.ts

import {
// previous imports
transform,
} from "@medusajs/workflows-sdk"

// ...

const myWorkflow = createWorkflow<
WorkflowInput,
WorkflowOutput

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

const result = transform(
{
str1,
str2,
},
(input) => ({
message: ${input.str1}\n${input.str2},
})
)

return result
})

export default myWorkflow

Notice that to use the results of the steps, you must use the transform utility function. It gives you access to the real-time results of the steps once the workflow is executed.

Then, pass the necessary input to the workflow when you execute it. For example, in the API Route executing the workflow:
src/api/store/custom/route.ts

import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
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)
}

If you execute the workflow again, you’ll see:

A Hello from step one! message, indicating that step one ran first.
A Hello {name} from step two message, indicating that step two ran after.

Add Error Handling

Errors can occur in a workflow. To avoid data inconsistency, you can pass a compensation function as a third parameter to the createStep function.

The compensation function only runs if an error occurs throughout the Workflow. It’s useful to undo or roll back actions you’ve performed in a step.

For example, change step one to add a compensation function and step two to throw an error:
src/workflows/hello-world.ts

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...")
}
)

const step2 = createStep(
"step-2",
async ({ name }: WorkflowInput) => {
throw new Error("Throwing an error...")
}
)

If you execute the Workflow, you should see:

Hello from step one! logged, indicating that the first step ran successfully.
Oops! Rolling back my changes... logged, indicating that the second step failed and the compensation function of the first step ran consequently.

Try it Out

You can try out this guide on Stackblitz.
More Advanced Example

Let’s cover a more realistic example.

For example, you can build a workflow that updates a product’s CMS details both in Medusa and an external CMS service:

import { createWorkflow } from "@medusajs/workflows-sdk"
import { Product } from "@medusajs/medusa"
import { updateProduct, sendProductDataToCms } from "./steps"

type WorkflowInput = {
id: string
title: string,
description: string,
images: string[]
}

const updateProductCmsWorkflow = createWorkflow<
WorkflowInput,
Product

    ("update-product-cms", function (input) {
    const product = updateProduct(input)
    sendProductDataToCms(product)

return product

})

As these steps are making changes to data in the Medusa backend and a third-party service, it’s useful to provide a compensation function for each step that rolls back the changes.

For example, you can pass a compensation function to the updateProduct step that reverts the product update in case an error occurs:

const updateProduct = createStep(
"update-product",
async (input: WorkflowInput, context) => {
const productService: ProductService =
context.container.resolve("productService")

const { id, ...updateData } = input
const previousProductData = await productService.retrieve(
  id, {
    select: ["title", "description", "images"],
  }
)
const product = await productService.update(id, updateData)

return new StepResponse(product, {
  id,
  previousProductData,
})

}, async ({ id, previousProductData }, context) => {
const productService: ProductService =
context.container.resolve("productService")

await productService.update(id, previousProductData)

}
)

Your steps may interact with external systems. For example, the sendProductDataToCms step communicates with an external CMS service. With the error handling and roll-back features that workflows provide, developers can ensure data delivery between multiple systems in their stack.
Constraints on Workflow Constructor Function

The Workflow Builder, createWorkflow, comes with a set of constraints:

The function passed to the createWorkflow can’t be an arrow function:

// Don't
const myWorkflow = createWorkflow<
WorkflowInput,
WorkflowOutput

    ("hello-world", (input) => {
    // ...
    }
    )

// Do
const myWorkflow = createWorkflow<WorkflowInput, WorkflowOutput>
("hello-world", function (input) {
// ...
}
)

The function passed to the createWorkflow can’t be an asynchronous function.
Since the constructor function only defines how the workflow works, you can’t directly manipulate data within the function. To do that, you must use the transform function:

// Don't
const myWorkflow = createWorkflow<
WorkflowInput,
WorkflowOutput

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

return {
  message: `${input.str1}${input.str2}`,
}

}
)

// Do
const myWorkflow = createWorkflow<
WorkflowInput,
WorkflowOutput

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

const result = transform({
  str1,
  str2,
}, (input) => ({
  message: `${input.str1}${input.str2}`,
}))

return result

}
)

Medusa Architecture Overview

In this document, you'll get an overview of Medusa's architecture to better understand how all resources and tools work together.
Architecture Overview

Medusa's core package @medusajs/medusa is a Node.js headless server. It combines all the Commerce Modules that Medusa provides. Commerce Modules are ecommerce features that can be used as building blocks in an ecommerce ecosystem. Product is an example of a Commerce Module.

Medusa Core Architecture

The backend connects to a database, such as PostgreSQL, to store the ecommerce store’s data. The tables in that database are represented by Entities, built on top of Typeorm. Entities can also be reflected in the database using Migrations.

The retrieval, manipulation, and other utility methods related to that entity are created inside a Service. Services are TypeScript or JavaScript classes that, along with other resources, can be accessed throughout the Medusa backend through dependency injection.

The backend doesn't have any tightly-coupled frontend. Instead, it exposes API Routes which are REST APIs that frontends such as an admin or a storefront can use to communicate with the backend.

Medusa also uses an Events Architecture to trigger and handle events. Events are triggered when a specific action occurs, such as when an order is placed. To manage this events system, Medusa connects to a service that implements a pub/sub model, such as Redis.

Events can be handled using Subscribers. Subscribers are TypeScript or JavaScript files that export a handler function that's executed asynchronously when an event is triggered, and a configuration object defining what events the subscriber listens to.

You can create any of the resources in the backend’s architecture, such as entities, API Routes, services, and more, as part of your custom development without directly modifying the backend itself. The Medusa backend uses loaders to load the backend’s resources, as well as your custom resources and resources in Plugins.

You can package your customizations into Plugins to reuse them in different Medusa backends or publish them for others to use. You can also install existing plugins into your Medusa backend.

Dependency Container and Injection

In this document, you’ll learn what the dependency container is and how you can use it in Medusa with dependency injection.
Introduction
What is Dependency Injection

Dependency Injection is the act of delivering the required resources to a class. These resources are the class’s dependencies. This is usually done by passing (or injecting) the dependencies in the constructor of the class.

Generally, all resources are registered in a container. Then, whenever a class depends on one of these resources, the system retrieves the resources from the container and injects them into the class’s constructor.
Medusa’s Dependency Container

Medusa uses a dependency container to register essential resources of the backend. You can then access these resources in classes and API Routes using the dependency container.

For example, if you create a custom service, you can access any other service registered in Medusa in your service’s constructor. That includes Medusa’s core services, services defined in plugins, or other services that you create on your backend.

You can load more than services in your Medusa backend. You can load the Entity Manager, logger instance, and much more.
MedusaContainer

To manage dependency injections, Medusa uses Awilix. Awilix is an NPM package that implements dependency injection in Node.js projects.

When you run the Medusa backend, a container of the type MedusaContainer is created. This type extends the AwilixContainer object.

The backend then registers all important resources in the container, which makes them accessible in classes and API Routes.
Registered Resources

The Medusa backend scans the core Medusa package, plugins, and your files in the dist directory and registers the following resources:
Tip

The Lifetime column indicates the lifetime of a service. Other resources that aren't services don't have a lifetime, which is indicated with the - in the column. You can learn about what a lifetime is in the Create a Service documentation.

Resource

Description

Registration Name

Lifetime

Configurations

The configurations that are exported from medusa-config.js.

configModule

Services

Services that extend the TransactionBaseService class.

Each service is registered under its camel-case name. For example, the ProductService is registered as productService.

Core services by default have the SINGLETON lifetime. However, some have a different lifetime which is indicated in this table. Custom services, including services in plugins, by default have the SCOPED lifetime, unless defined differently within the custom service.

Entity Manager

An instance of Typeorm’s Entity Manager.

manager

Logger

An instance of Medusa CLI’s logger. You can use it to log messages to the terminal.

logger

Single Payment Processor

An instance of every payment processor that extends the AbstractPaymentService or the AbstractPaymentProcessor classes.

Every payment processor is registered under two names:

Its camel-case name of the processor. For example, the StripeProviderService is registered as stripeProviderService.
pp_ followed by its identifier. For example, the StripeProviderService is registered as pp_stripe.

    

By default, it's SINGLETON unless defined differently within the payment processor service.

All Payment Processors

An array of all payment processor that extend the AbstractPaymentService or AbstractPaymentProcessor class.

paymentProviders

paymentProviders is TRANSIENT, and each item in it is SINGLETON.

Single Fulfillment Provider

An instance of every fulfillment provider that extends the FulfillmentService class.

Every fulfillment provider is registered under two names:

Its camel-case name. For example, the WebshipperFulfillmentService is registered as webshipperFulfillmentService.
fp_ followed by its identifier. For example, the WebshipperFulfillmentService is registered as fp_webshipper.

    

By default, it's SINGLETON unless defined differently within the fulfillment provider service.

All Fulfillment Providers

An array of all fulfillment providers that extend the FulfillmentService class.

fulfillmentProviders

fulfillmentProviders is TRANSIENT, and each item in it is SINGLETON.

Single Notification Provider

An instance of every notification provider that extends the AbstractNotificationService or the BaseNotificationService classes.

Every notification provider is registered under two names:

Its camel-case name. For example, the SendGridService is registered as sendGridService.
noti_ followed by its identifier. For example, the SendGridService is registered as noti_sendgrid.

    

By default, it's SINGLETON unless defined differently within the notification provider service.

All Notification Providers

An array of all notification providers that extend the AbstractNotificationService or the BaseNotificationService classes.

notificationProviders

notificationProviders is TRANSIENT, and each item in it is SINGLETON.

File Service

An instance of the class that extends the FileService class, if any.

The file service is registered under two names:

Its camel-case name. For example, the MinioService is registered as minioService.
fileService

    

By default, it's SINGLETON unless defined differently within the file service.

Search Service

An instance of the class that extends the AbstractSearchService or the SearchService classes, if any.

The search service is registered under two names:

Its camel-case name. For example, the AlgoliaService is registered as algoliaService.
searchService

    

By default, it's SINGLETON unless defined differently within the search service.

Single Tax Provider

An instance of every tax provider that extends the AbstractTaxService class.

The tax provider is registered under two names:

Its camel-case name.
tp_ followed by its identifier.

    

By default, it's SINGLETON unless defined differently within the tax provider service.

All Tax Providers

An array of every tax provider that extends the AbstractTaxService class.

taxProviders

taxProviders is TRANSIENT, and each item in it is SINGLETON.

Oauth Services

An instance of every service that extends the OauthService class.

Each Oauth Service is registered under its camel-case name followed by Oauth.

By default, it's SINGLETON unless defined differently within the Oauth service.

Feature Flag Router

An instance of the FlagRouter. This can be used to list feature flags, set a feature flag’s value, or check if they’re enabled.

featureFlagRouter

Redis

An instance of the Redis client. If Redis is not configured, a fake Redis client is registered.

redisClient

Single Entity

An instance of every entity.

Each entity is registered under its camel-case name followed by Model. For example, the CustomerGroup entity is stored under customerGroupModel.

All Entities

An array of all database entities that is passed to Typeorm when connecting to the database.

db_entities

Repositories

An instance of each repository.

Each repository is registered under its camel-case name. For example, CustomerGroupRepository is stored under customerGroupRepository.

Single Batch Job Strategy

An instance of every class extending the AbstractBatchJobStrategy class.

Each batch job strategy is registered under three names:

Its camel-case name. For example, ProductImportStrategy is registered as productImportStrategy.
batch_ followed by its identifier. For example, the ProductImportStrategy is registered under batch_product-import-strategy.
batchType_ followed by its batch job type. For example, the ProductImportStrategy is registered under batchType_product-import.

    

All Batch Job Strategies

An array of all classes extending the AbstractBatchJobStrategy abstract class.

batchJobStrategies

Tax Calculation Strategy

An instance of the class implementing the ITaxCalculationStrategy interface.

taxCalculationStrategy

Cart Completion Strategy

An instance of the class extending the AbstractCartCompletionStrategy class.

cartCompletionStrategy

Price Selection Strategy

An instance of the class implementing the IPriceSelectionStrategy interface.

priceSelectionStrategy

Strategies

An instance of strategies that aren’t of the specific types mentioned above and that are under the strategies directory.

Its camel-case name.

    Resolve Resources

This section covers how to resolve resources from the dependency container to use them in API Routes and classes in general.
In API Routes

To resolve resources, such as services, in API Routes, use the MedusaRequest object's scope.resolve method. The method receives the registration name of the resource as a parameter.

For example:

const productService: ProductService = req.scope.resolve(
"productService"
)

Please note that in API Routes some resources, such as repositories, aren't available. Refer to the repositories documentation to learn how you can load them.
In Subscribers

To resolve resources, such as services, in subscriber handler functions, use the container property of the handler function's parameter. The container has a method resolve which accepts the registration name of a resource as a parameter.

For example:

import {
ProductService,
type SubscriberConfig,
type SubscriberArgs,
} from "@medusajs/medusa"

export default async function productUpdateHandler({
data, eventName, container, pluginOptions,
}: SubscriberArgs<Record<string, string>>) {
const productService: ProductService = container.resolve(
"productService"
)

// ...
}

// ...

In Classes

In classes such as services or strategies, you can load resources in the constructor function using dependency injection. The constructor receives an object of dependencies as a first parameter. Each dependency in the object should use the registration name of the resource that should be injected to the class.

For example:

import { OrderService } from "@medusajs/medusa"

class OrderSubscriber {
protected orderService: OrderService

constructor({ orderService }) {
this.orderService = orderService
}
}

Entities

In this document, you'll learn what Entities are in Medusa.
What are Entities

Entities in medusa represent tables in the database as classes. An example of this would be the Order entity which represents the order table in the database. Entities provide a uniform way of defining and interacting with data retrieved from the database.

Aside from the entities in the Medusa core package, you can also create custom entities to use in your Medusa backend. Custom entities are TypeScript or JavaScript files located in the src/models directory of your Medusa backend. You then transpile these entities to be used during the backend's runtime using the build command, which moves them to the dist/models directory.

Entities are based on Typeorm’s Entities and use Typeorm decorators. Each entity also require a repository to be created. A repository provides basic methods to access and manipulate the entity's data.
Create Entity and Expose it with API RoutesLearn how to create a new table in your database, then create API Routes to expose and manipulate its data.
Create entity
Create service
Create API Routes
Base Entities

All entities must extend either the BaseEntity or SoftDeletableEntity classes. The BaseEntity class holds common columns including the id, created_at, and updated_at columns.

The SoftDeletableEntity class extends the BaseEntity class and adds another column deleted_at. If an entity can be soft deleted, meaning that a row in it can appear to the user as deleted but still be available in the database, it should extend SoftDeletableEntity.
metadata Attribute

Most entities in Medusa have a metadata attribute. This attribute is an object that can be used to store custom data related to that entity. In the database, this attribute is stored as a JSON Binary (JSONB) column. On retrieval, the attribute is parsed into an object.

Some example use cases for the metadata attribute include:

Store an external ID of an entity related to a third-party integration.
Store product customization such as personalization options.

Add and Update Metadata

You can add or update metadata entities either through the REST APIs or through create and update methods in the entity's respective service.

In the admin REST APIs, you'll find that in create or update requests of some entities you can also set the metadata.

In services, there are typically create or update methods that allow you to set or update the metadata.

If you want to add a property to the metadata object or update a property in the metadata object, you can pass the metadata object with the properties you want to add or update in it. For example:

{
// other data
"metadata": {
"is_b2b": true
}
}

If you want to remove a property from the metadata object, you can pass the metadata object with the property you want to delete. The property should have an empty string value. For example:

{
// other data
"metadata": {
"is_b2b": "" // this deletes the is_b2b property from metadata
}
}

Migrations

In this document, you'll learn what Migrations are in Medusa.
What are Migrations

Migrations are scripts that are used to make additions or changes to your database schema. In Medusa, they are essential for both when you first install your backend and for subsequent backend upgrades later on.

When you first create your Medusa backend, the database schema used must have all the tables necessary for the backend to run.

When a new Medusa version introduces changes to the database schema, you'll have to run migrations to apply them to your own database.
Tip

Migrations are used to apply changes to the database schema. However, there are some version updates of Medusa that require updating the data in your database to fit the new schema. Those are specific to each version and you should check out the version under Upgrade Guides for details on the steps.
Migration Commands
Migrate Command

Using the Medusa CLI tool, you can run migrations with the following command:

npx medusa migrations run

This will check for any migrations that contain changes to your database schema that aren't applied yet and run them on your backend.
Seed Command

Seeding is the process of filling your database with data that is either essential or for testing and demo purposes. In Medusa, the seed command will run the migrations to your database if necessary before it seeds your database with dummy data.

You can use the following command to seed your database:

npx @medusajs/medusa-cli@latest seed -f ./data/seed.json

This assumes that you have the file data/seed.json in your Medusa backend, which is available by default. It includes the demo data to seed into your database.
Revert Migrations

To revert the last migration you ran, run the following command:

npx @medusajs/medusa-cli@latest migrations revert

API Routes

In this document, you’ll learn what API Routes are in Medusa.
Introduction

The Medusa Backend is a Node.js headless server. It exposes commerce functionalities implemented by Medusa's commerce module through REST APIs that external or frontend applications can access to process or retrieve data.

The backend exposes two types of API routes: Store APIs and Admin APIs. The Store APIs are typically accessed from the storefront. For example, you can use the Store APIs to show customers available products or implement a cart and checkout flow.

The Admin APIs are typically accessed from an admin dashboard. For example, you can use the Admin APIs to allow admins to manage the store’s data such as products, orders, and so on.

How to Create an API Route

In this document, you’ll learn how to create API Routes in Medusa.
Tip

v1.17.2 of @medusajs/medusa introduced API Routes to replace Express endpoints. You can still use the Express endpoints approach, however, it's highly recommended that you start using API Routes.
Basic Implementation
src/api/store/custom/route.ts

import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"

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

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

API Route Path

Custom API Routes must be created in a file named route.ts or route.js under the src/api directory of your Medusa backend or plugin. The API Route's path will be the same as the path of its corresponding route.ts file relative to src/api.

For example, if you're creating the API route store/custom, you must create the file src/api/store/custom/route.ts.
API Route Method

route.ts can export at least one of the following method handler functions: GET, POST, DELETE, PUT, PATCH, OPTIONS, and HEAD. Defining these method handlers adds a new API Route for the corresponding HTTP method at the same path.

Each of these method handler functions receives two parameters: the MedusaRequest which extends Express's Request, and the MedusaResponse which extends Response. Both are imported from @medusajs/medusa.

In the example above, GET and POST API Routes will be added at the store/custom path.
Building Files

Custom API Routes must be transpiled and moved to the dist directory before you can start consuming them. When you run your backend using either the medusa develop or npx medusa develop commands, it watches the files under src for any changes, then triggers the build command and restarts the server.

However, the build isn't triggered when the backend first starts running, and it's never triggered when the medusa start or npx medusa start commands are used.

So, make sure to run the build command before starting the backend and testing out your API Routes:

npm
Yarn
pnpm

yarn build

Medusa API Routes Path Convention

Although your API Route can be under any path you wish, the Medusa backend uses the following conventions:

All storefront REST APIs are prefixed by /store. For example, the /store/products API Route lets you retrieve the products to display them on your storefront.
All admin REST APIs are prefixed by /admin. For example, the /admin/products API Route lets you retrieve the products to display them on your admin.

Path Parameters

If your API Route accepts a path parameter, you can place its route file inside a directory with the name [], where is the name of your parameter.

For example, to add an API Route at the path store/custom/[id], create the route file at src/api/store/custom/[id]/route.ts.

You can access a path parameter's value in method handlers using the MedusaRequest object's params property, which is an object. Each of the params keys is a path parameter's name, and its value is the supplied value when sending the request to the API route.

For example:
src/api/store/custom/[id]/route.ts

import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"

export function GET(
req: MedusaRequest,
res: MedusaResponse
) {
const id = req.params.id

// do something with the ID.
}

An API Route can have more than one path parameter, but each path parameter's nam is unique. If the same path parameter name is used more than once in the same route path, it results in an error and the Medusa Backend won't register the API Route.

For example, if your API route accepts an author ID and a post ID, the path to your route file can be src/api/author/[id]/posts/[post_id]/route.ts. You can then use the MedusaRequest object's params.id and params.post_id to access the values of the path parameters.
CORS Configuration

The cors middleware, which enables Cross-Origin Resource Sharing (CORS), is automatically applied on custom API Routes defined under the /store or /admin path prefixes based on the respective store_cors and admin_cors configurations.

To add CORS configurations to custom API routes under other path prefixes, or override the CORS configurations added by default, define a middleware on your API routes and pass it the cors middleware. For example:
src/api/middlewares.ts

import type {
MiddlewaresConfig,
} from "@medusajs/medusa"
import cors from "cors"

export const config: MiddlewaresConfig = {
routes: [
{
matcher: "/custom*",
middlewares: [
cors({
origin: "*",
credentials: true,
}),
],
},
],
}

CORS Opt-Out

To disable the cors middleware for an API Route, export a CORS variable in the route file with its value set to false.

For example:
src/api/store/custom/route.ts

import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"

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

export const CORS = false

Parse Request Body Parameters

By default, the Medusa backend parses the body of all requests sent to your API Routes with the Content-Type header set to application/json to a JavaScript object. Then, the parsed data is attached to the MedusaRequest object's body property, which is an object.

Each of the body's keys are a name of the request body parameters, and its value is the passed value in the request body.

For example:
src/api/store/custom/route.ts

import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"

export const POST = (
req: MedusaRequest,
res: MedusaResponse
) => {
const name = req.body.name

// do something with the data...
}

If you want to parse other content types, such as application/x-www-form-urlencoded, you have to add a middleware to your API routes that parses that body type.

For example:
src/api/middlewares.ts

import type {
MiddlewaresConfig,
} from "@medusajs/medusa"
import {
urlencoded,
} from "body-parser"

export const config: MiddlewaresConfig = {
routes: [
{
matcher: "/store/*",
middlewares: [
urlencoded({ extended: true }),
],
},
],
}

Note that the urlencoded middleware imported from the body-parser package attaches the parsed data to the MedusaRequest object's body property as well.
Parse Webhook Body Parameters

For webhook API Routes, you may need to use the raw body parser middleware rather than the default json.

You can opt out of the default body parser by setting the bodyParser property of a middleware route object to false, and passing the preferred body-parser middleware in the middlewares property.

For example:
src/api/middlewares.ts

import { MiddlewaresConfig } from "@medusajs/medusa"
import { raw } from "body-parser"

export const config: MiddlewaresConfig = {
routes: [
{
matcher: "/webhooks/*",
bodyParser: false,
middlewares: [raw({ type: "application/json" })],
},
],
}

You can also disable the default json body parser for specific HTTP methods using the method property for a middleware route object. Its value can either be a string or an array of strings, each being an HTTP method name.

For example:
src/api/middlewares.ts

import { MiddlewaresConfig } from "@medusajs/medusa"
import { raw } from "body-parser"

export const config: MiddlewaresConfig = {
routes: [
{
method: ["POST", "PUT"],
matcher: "/webhooks/*",
bodyParser: false,
middlewares: [raw({ type: "application/json" })],
},
],
}

This disables the default JSON body parser for the POST and PUT HTTP methods on API Routes matching the /webhooks/* matcher, and applies the raw body-parser middleware on them instead.
Configure Request Body Size Limit

By default, the maximum request body size allowed is 1000 bytes. If a request body's size is greater than that, an error is thrown.

If you expect the request body of an API Route to be larger than the default, you can set the bodyParser property of a middleware route object to a configuration object with the property sizeLimit. Its value is a number indicating the maximum allowed size limit in bytes.

For example:
src/api/middlewares.ts

import { MiddlewaresConfig } from "@medusajs/medusa"

export const config: MiddlewaresConfig = {
routes: [
{
bodyParser: { sizeLimit: 2000 }, // in bytes
// ...
},
],
}

Protected API Routes

Protected API routes are routes that should only be accessible by logged-in customers or users.
Protect Store API Routes

By default, API routes prefixed by /store don't require customer authentication to access the API route. However, you can still access the logged-in customer's ID in the API Route method handler using the MedusaRequest object's user.customer_id, which will be undefined if the customer isn't logged in.

For example:
src/api/store/custom/route.ts

import { CustomerService } from "@medusajs/medusa"
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"

export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const id = req.user.customer_id

if (!id) {
// TODO handle not logged in
// customers based on the custom
// API route's functionality
}

const customerService = req.scope.resolve(
"customerService"
)

const customer = await customerService.retrieve(id)

// ...
}

API Routes prefixed by /store/me, on the other hand, require customer authentication to access the API Route. You can access the logged-in customer's ID in the API Route method handler using the MedusaRequest object's user.customer_id.

If you want to disable authentication requirement on your custom API Route prefixed with /store/me, export an AUTHENTICATE variable in the route file with its value set to false. For example:
src/api/store/me/custom/route.ts

import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"

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

export const AUTHENTICATE = false

Note

This disables authentication requirement on all API Route methods defined in the same file.
Protect Admin API Routes

By default, all API Routes prefixed by /admin require admin user authentication to access the API Route. You can access the logged-in user's ID in the API Route method handler using the MedusaRequest object's user.userId.

For example:
src/api/admin/custom/route.ts

import type {
MedusaRequest,
MedusaResponse,
UserService,
} from "@medusajs/medusa"

export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const id = req.user.userId

const userService = req.scope.resolve(
"userService"
)

const user = await userService.retrieve(id)

// ...
}

To disable authentication requirement on an admin API Route, export an AUTHENTICATE variable in your route file with its value set to false.

For example:
src/api/admin/custom/route.ts

import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"

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

export const AUTHENTICATE = false

Note

This disables authentication requirement on all API Route methods defined in the same file.
Protect Other API Routes

To protect API routes that aren't prefixed with /store or /admin, you can use one of the following middlewares exported by @medusajs/medusa for authenticating customers or users:

authenticate: this middleware ensures that only authenticated admin users can access an API Route. You can access the user's ID in the API Route method handler using the MedusaRequest object's user.userId.
authenticateCustomer: this middleware doesn't require a customer to be authenticated, but if a customer is logged in, it attaches their ID to the MedusaRequest object's user.customer_id.
requireCustomerAuthentication: this middleware ensures that only authenticated customers can access an API Route. You can access the customer's ID in the API Route method handler using the MedusaRequest object's user.customer_id.

For example:
src/api/middlewares.ts

import {
authenticate,
requireCustomerAuthentication,
type MiddlewaresConfig,
} from "@medusajs/medusa"

export const config: MiddlewaresConfig = {
routes: [
{
matcher: "/custom/admin*",
middlewares: [authenticate()],
},
{
matcher: "/custom/customer*",
middlewares: [requireCustomerAuthentication()],
},
],
}

Retrieve Medusa Config

You can access the configurations exported in medusa-config.js, including your custom configurations, by resolving the configModule resource using dependency injection.

For example:
src/api/store/custom/route.ts

import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import { ConfigModule } from "@medusajs/medusa"

// This is only helpful if you're
// accessing custom configurations
// otherwise it's fine to just use ConfigModule
type MyConfigModule = ConfigModule & {
projectConfig: {
custom_config?: string
}
}

export const GET = (
req: MedusaRequest,
res: MedusaResponse
) => {
const configModule = req.scope.resolve(
"configModule"
)
res.json({
message: configModule.projectConfig.custom_config,
})
}

Handle Errors

Medusa automatically applies an error-handler middleware on your custom API routes, which returns errors as a JSON response whose format is consistent with the Medusa backend.
Tip

When an error is thrown, the response status code is set to 500 by default. However, that changes based on the MedusaError type thrown as explained in the next section.
Medusa Error Types

You must throw errors of type MedusaError to ensure the error message is returned in the response. Otherwise, the returned error message will be Unknown Error.

For example, if you throw an error like this:

throw new Error ("Post was not found")

The API Route returns the following object error in the response:

{
"code": "unknown_error",
"type": "unknown_error",
"message": "An unknown error occurred."
}

To ensure your error message is relayed in the response, use MedusaError imported from @medusajs/utils as the thrown error type instead.

For example:

import { MedusaError } from "@medusajs/utils"

// ...

throw new MedusaError(
MedusaError.Types.NOT_FOUND,
"Post was not found"
)

The constructor of MedusaError accepts the following parameters:

The first parameter is the error's type. You can use one of the predefined errors under MedusaError.Types, such as MedusaError.Types.NOT_FOUND which sets the response status code to 404 automatically.
The second parameter is the message of the error.
The third parameter is an optional code, which is a string, that's returned in the error object.

After using MedusaError, the returned error in the response provides a clearer message:

{
"type": "not_found",
"message": "Post was not found"
}

Available MedusaError Types and their respective status codes
Override Error Handler

To override the default error handler, pass the errorHandler property to the exported middleware configurations with the custom error-handler middleware as its value.

For example:
src/api/middlewares.ts

import { MiddlewaresConfig } from "@medusajs/medusa"

export const config: MiddlewaresConfig = {
errorHandler: (err, req, res, next) => {
// custom error handling logic...
},
}

Disable Default Error Handler

To disable the default error handler, set the errorHandler property of the exported middleware configurations to false.

For example:
src/api/middlewares.ts

import { MiddlewaresConfig } from "@medusajs/medusa"

export const config: MiddlewaresConfig = {
errorHandler: false,
}

However, when you disable the default error handler, errors thrown in an async function or method are not handled and requests goes on indefinitely with no response.

To ensure that errors are still returned in the response when the default error handler is disabled, either create a custom error handler or wrap your API Route with the wrapHandler imported from @medusajs/medusa.

For example:
src/api/middlewares.ts

import {
MedusaRequest,
MedusaResponse,
wrapHandler,
} from "@medusajs/medusa"

export const GET = wrapHandler(async (
req: MedusaRequest,
res: MedusaResponse
): Promise => {
throw new Error("An error occurred")
})

Use Other Resources

Resources, such as services, that are registered in the dependency container can be retrieved in an API Route's handler method using the MedusaRequest object's scope.resolve method.

The scope method accepts as a parameter the resource's registration name in the dependency container.
Example: Retrieve Repository
Tip

Posts are represented by a custom entity not covered in this guide. You can refer to the entities for more details on how to create a custom entity.
src/api/store/posts/route.ts

import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import {
PostRepository,
} from "../../../repositories/post"
import { EntityManager } from "typeorm"

export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const postRepository =
req.scope.resolve("postRepository")
const manager = req.scope.resolve("manager")
const postRepo = manager.withRepository(postRepository)

res.json({
posts: await postRepo.find(),
})
}

Notice that to retrieve an instance of the repository, you need to retrieve first Typeorm's Entity Manager from the dependency container, then use its withRepository method.
Example: Retrieve Service
Note

PostService is a custom service that is not covered in this guide. You can refer to the services documentation for more details on how to create a custom service, and find an example of PostService
src/api/store/posts/route.ts

import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import { PostService } from "../../../services/post"

export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const postService: PostService = req.scope.resolve(
"postService"
)

res.json({
posts: await postService.list(),
})
}

Ignored Files and Directories

Files and directories prefixed with _ are ignored. This can be helpful if you want to implement API Route method handlers in different files, then reference them in your route.ts file.

For example:

src/api/custom/route.ts
src/api/_methods/get-product.ts

import getProducts from "../_methods/get-products"

export const GET = getProducts

Example: CRUD API Routes

This section provides an example of creating API Routes that perform Create, Read, Update, and Delete (CRUD) operations.
Note

You can refer to the Entities and Services documentation to learn how to create the custom entities and services used in this example.

src/api/admin/posts/route.ts
src/api/admin/posts/[id]/route.ts

import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import { PostService } from "../../../services/post"

// list posts
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const postService: PostService = req.scope.resolve(
"postService"
)

res.json({
posts: await postService.list(),
})
}

// create a post
export const POST = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const postService: PostService = req.scope.resolve(
"postService"
)

// basic validation of request body
if (!req.body.title || !req.body.author_id) {
throw new Error("title and author_id are required.")
}

const post = await postService.create(req.body)

res.json({
post,
})
}

Middlewares

In this document, you’ll learn how to add a middleware to existing or custom API Routes in Medusa.
Tip

v1.17.2 of @medusajs/medusa introduced a new approach to creating middlewares using a single middlewares.ts file. You can still use the Express Router Approach, however, it's highly recommended that you start using this new approach.
Basic Implementation
src/api/middlewares.ts

import type { MiddlewaresConfig } from "@medusajs/medusa"
import type {
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"

const storeMiddleware = (
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) => {
// do something
next()
}

export const config: MiddlewaresConfig = {
routes: [
{
matcher: "/store/*",
middlewares: [storeMiddleware],
},
],
}

A middleware is a function that has access to the MedusaRequest and MedusaResponse objects that are passed to API Route method handlers.

Middlewares are used to perform an action when a request is sent to an API Route, or modify the response of an API route, among other usages.
middlewares.ts File

Middlewares are defined in the src/api/middlewares.ts file. This file must expose a config object of type MiddlewaresConfig imported from @medusajs/medusa.

This object accepts two parameters:

routes: an array of middleware route objects. Each middleware route object accepts the following properties:
    matcher: a string or a regular expression that will be used to check whether the middlewares should be applied on a API Routes.
    middlewares: an array of middlewares that should be applied on API Routes matching the pattern specified in matcher.
    method: a string or an array of strings, where each value is the name of an HTTP method. This allows you to apply the middlewares only on the specified HTTP methods in this property. If not specified, the middlewares are applied on all HTTP methods.
    bodyParser: This property can have two types of values:
        false: If this value is used, the default json body parser middleware is disabled. This allows you to override the body parser with your own middleware. For example, it's useful when changing the default parser for webhooks.
        object: You can pass an object with configurations for the default json body parser middleware. It accepts the following configurations:
            sizeLimit: A number indicating the maximum body size allowed in bytes. By default, it's 1000 bytes. If a request body is greater than that, an error will be thrown. So, you can set this configuration if you expect the request body size of an API route to be greater than 1000 bytes.
errorHandler: This property can have two types of values:
    false: If this value is used, the default error handler is disabled. Learn more about its behavior here.
    function: A middleware function used to define a new error handler. Learn more here.

Build Files

Similar to custom API Routes, you must transpile the files under src into the dist directory for the backend to load them.

To do that, run the following command before running the Medusa backend:

npm
Yarn
pnpm

yarn build

You can then test that the middleware is working by running the backend.
Register New Resources in Dependency Container

In some cases, you may need to register a resource to use within your commerce application. For example, you may want to register the logged-in user to access it in other resources, such as services. You can do that in your middleware.
Tip

If you want to register a logged-in user and access it in your resources, you can check out this example guide.

To register a new resource in the dependency container, use the MedusaRequest object's scope.register method. It accepts an object, where each key is the name to be registered in the dependency container, and its value is an object that has a resolve property.

The resolve's value is a function that returns the resource to be registered in the dependency container.

For example:
src/api/middlewares.ts

import type { MiddlewaresConfig } from "@medusajs/medusa"
import type {
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"

const customResource = (
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction) => {
req.scope.register({
customResource: {
resolve: () => "my custom resource",
},
})

next()
}

export const config: MiddlewaresConfig = {
routes: [
{
matcher: "/store/*",
middlewares: [customResource],
},
],
}

You can then load this new resource within other resources. For example, to load it in a service:
src/services/custom-service.ts

import { TransactionBaseService } from "@medusajs/medusa"

class CustomService extends TransactionBaseService {

constructor(container, options) {
super(...arguments)

// use the registered resource.
try {
  container.customResource
} catch (e) {
  // avoid errors when the backend first loads
}

}
}

export default CustomService

Note

Make sure to wrap your usage of the new resource in a try-catch block when you use it in a constructor. This is to avoid errors that can arise when the backend first loads, as the resource isn't registered yet.
Note About Services Lifetime

If you want to access new registrations in the dependency container within a service, you must set the lifetime of the service either to Lifetime.SCOPED or Lifetime.TRANSIENT. Services that have a Lifetime.SINGLETON lifetime can't access new registrations since they're resolved and cached in the root dependency container beforehand. You can learn more in the Create Services documentation.

For custom services, no additional action is required as the default lifetime is Lifetime.SCOPED. However, if you extend a core service, you must change the lifetime since the default lifetime for core services is Lifetime.SINGLETON.

For example:

import { Lifetime } from "awilix"
import {
ProductService as MedusaProductService,
} from "@medusajs/medusa"

// extending ProductService from the core
class ProductService extends MedusaProductService {
// The default life time for a core service is SINGLETON
static LIFE_TIME = Lifetime.SCOPED

constructor(container, options) {
super(...arguments)

// use the registered resource.
try {
  container.customResource
} catch (e) {
  // avoid errors when the backend first loads
}

}

// ...
}

export default ProductService

Services

In this document, you'll learn about what Services are in Medusa.
What are Services

Services in Medusa represent bundled helper methods that you want to use across your commerce application. By convention, they represent a certain entity or functionality in Medusa.

For example, you can use Medusa’s productService to get the list of products, as well as perform other functionalities related to products. There’s also an authService that provides functionalities like authenticating customers and users.

In the Medusa backend, custom services are TypeScript or JavaScript files located in the src/services directory. Each service should be a class that extends the TransactionBaseService class from the core Medusa package @medusajs/medusa. Each file you create in src/services should hold one service and export it.

The file name is important as it determines the name of the service when you need to use it elsewhere. The name of the service will be registered in the dependency container as the camel-case version of the file name with Service appended to the end of the name. Other resources, such as other services or API Routes, will use that name when resolving the service from the dependency container.

For example, if the file name is hello.ts, the service will be registered as helloService in the dependency container. If the file name is hello-world.ts, the service name will be registered as helloWorldService.
Note

How to Create a Service

In this document, you’ll learn how you can create a Service and use it across your Medusa backend just like any of the core services.
Basic Service Implementation

To create a service, create a TypeScript or JavaScript file in src/services to hold the service. The name of the file should be the name of the service without Service. This is essential as the file name is used when registering the service in the dependency container, and Service is appended to the camel-case version of the file name automatically.

For example, if you want to create a service PostService, eventually registered as postService, create the file post.ts in src/services with the following content:
src/services/post.ts

import { TransactionBaseService } from "@medusajs/medusa"

class PostService extends TransactionBaseService {
getMessage() {
return Welcome to My Store!
}
}

export default PostService

This service will be registered in the dependency container as postService. It contains a single sample method getMessage.
Build Files

Custom services must be transpiled and moved to the dist directory before you can start consuming them. When you run your backend using either the medusa develop or npx medusa develop commands, it watches the files under src for any changes, then triggers the build command and restarts the server.

However, the build isn't triggered when the backend first starts running, and it's never triggered when the medusa start or npx medusa start commands are used.

So, make sure to run the build command before starting the backend:

npm
Yarn
pnpm

yarn build

Service Constructor

As the service extends the TransactionBaseService class, all resources registered in the dependency container can be accessed through dependency injection. This includes services, repositories, and other resources in the Medusa core, as well as your custom services and resources.

So, if you want your service to use another service, add it as part of your constructor’s dependencies and set it to a field inside your service’s class:
src/services/post.ts

import { ProductService } from "@medusajs/medusa"
import { PostRepository } from "../repositories/post"

class PostService extends TransactionBaseService {
private productService: ProductService

constructor(container) {
super(container)
this.productService = container.productService
}
// ...
}

Then, you can use that service anywhere in your custom service. For example:
src/services/post.ts

class PostService extends TransactionBaseService {
// ...
async getProductCount() {
return await this.productService.count()
}
}

Use Repositories

As your service provides helper methods related to one or more entities in your backend, you'll need to perform operations on that entity. To do that, you need to use the entity's repository.

Repositories, just like services, are registered in the dependency container and can be accessed with dependency injection.

However, to actually get an instance of the repository within the service's methods, you need to use the service's activeManager_, which is declared in the parent class TransactionBaseService. activeManager_ is an instance of Typeorm's EntityManager and has a method withRepository which allows you to retrieve an instance of the repository.

For example:
src/services/post.ts

import { PostRepository } from "../repositories/post"

class PostService extends TransactionBaseService {
// ...
protected postRepository_: typeof PostRepository

constructor(container) {
super(container)
// ...
this.postRepository_ = container.postRepository
}

async list(): Promise<Post[]> {
const postRepo = this.activeManager_.withRepository(
this.postRepository_
)
return await postRepo.find()
}

// ...
}

Refer to the repositories documentation to learn about its different methods.
Transactions

Transactions ensure that when an error occurs, all data manipulation within the transaction is reverted. As services are likely to include methods that manipulate data, such as create or update a post, it's very useful to wrap that logic within a transaction block.

Since services extend the TransactionBaseService class, you can use its atomicPhase_ method. The atomicPhase_ method allows you to wrap code within a transactional block.

It accepts as a parameter a function, which includes the logic to be performed inside the transactional block. The function accepts as a parameter a transaction manager, which is Typeorm's EntityManager. You can use it within the function to retrieve repositories, among other functionalities.

The data returned by the function passed as a parameter to the atomicPhase_ method will be return by the atomicPhase_ method as well.

For example, the PostService's create method with the atomicPhase_ method:
src/services/post.ts

class PostService extends TransactionBaseService {
protected postRepository_: typeof PostRepository
// ...
async create(
data: Pick<Post, "title" | "author_id">
): Promise {
return this.atomicPhase_(async (manager) => {
const postRepo = manager.withRepository(
this.postRepository_
)
const post = postRepo.create()

  post.title = data.title
  post.author_id = data.author_id

  const result = await postRepo.save(post)

  return result
})

}
}

Service Life Time

As the dependency container in Medusa is built on top of awilix, you can specify the Lifetime of a service. The lifetime is added as a static property to the service.

There are three lifetime types:

Lifetime.TRANSIENT: when used, a new instance of the service is created every time it is resolved in other resources from the dependency container.
Lifetime.SCOPED: (default for custom services) when used, an instance of the service is created and reused in the scope of the dependency container. So, when the service is resolved in other resources that share that dependency container, the same instance of the service will be returned.
Lifetime.SINGLETON: (default for core services) when used, the service is always reused, regardless of the scope. An instance of the service is cached in the root container.

You can set the lifetime of your service by setting the LIFE_TIME static property:
src/services/post.ts

import { TransactionBaseService } from "@medusajs/medusa"
import { Lifetime } from "awilix"

class PostService extends TransactionBaseService {
static LIFE_TIME = Lifetime.SCOPED

// ...
}

Retrieve Medusa Configurations

Within your service, you may need to access the Medusa configuration exported from medusa-config.js. To do that, you can access configModule using dependency injection.

For example:
src/services/post.ts

import {
ConfigModule,
TransactionBaseService,
} from "@medusajs/medusa"

class PostService extends TransactionBaseService {
protected readonly configModule_: ConfigModule

constructor(container) {
super(container)
// ...
this.configModule_ = container.configModule
}

getConfigurations() {
return this.configModule_
}

// ...
}

export default PostService

Pagination, Filtering, and Relations

Often, your service will provide methods that retrieve a list of items, which can be used by API Routes. In these methods, it can be helpful to provide filtering and pagination utilities that can be used by API Routes or any other resources utilizing this service.

The @medusajs/medusa package provides the following generic types that you can use to create the signature of your method that accepts filtering and pagination parameters:

Selector: Can be used to accepts the attributes of an entity as possible filtering parameters, based on each attribute's type.
FindConfig: Can be used to provide pagination parameters such as skip, take, and relations. skip indicates how many items to skip before retrieving the results, take indicates how many results to return, and relations, indicate which relations to expand and include in the returned objects.

The @medusajs/medusa package also provides a buildQuery method that allows you to pass two parameter, the first of type Selector and the second of type FindConfig, to build the object that should be passed to the repository.

So, for example, to create a method that retrieves a list of posts and the total number of posts available:
src/services/post.ts

import {
FindConfig,
Selector,
TransactionBaseService,
buildQuery,
} from "@medusajs/medusa"

class PostService extends TransactionBaseService {
// ...
async listAndCount(
selector?: Selector,
config: FindConfig = {
skip: 0,
take: 20,
relations: [],
}): Promise<[Post[], number]> {
const postRepo = this.activeManager_.withRepository(
this.postRepository_
)

const query = buildQuery(selector, config)

return postRepo.findAndCount(query)

}
}

In addition, you can expand relations when retrieving a single item with the help of FindConfig and buildQuery.

For example, to create a method that retrieves a single post:
src/services/post.ts

import {
FindConfig,
TransactionBaseService,
buildQuery,
} from "@medusajs/medusa"
import { MedusaError } from "@medusajs/utils"

class PostService extends TransactionBaseService {
// ...

async retrieve(
id: string,
config?: FindConfig
): Promise {
const postRepo = this.activeManager_.withRepository(
this.postRepository_
)

const query = buildQuery({
  id,
}, config)

const post = await postRepo.findOne(query)

if (!post) {
  throw new MedusaError(
    MedusaError.Types.NOT_FOUND,
    "Post was not found"
  )
}

return post

}
}

Then, any other resources such as API Route or services that use this method can pass what relations to expand in the next parameter:

await postService.retrieve(id, {
relations: ["authors"],
})

Throwing Errors

When you need to throw errors in your service methods, it's recommended to use MedusaError imported from @medusajs/util. That way, when an error is thrown within a request, the error will be returned in the response in a consistent format as Medusa's errors.
Note

This assumes you're handling errors in your custom API Route as explained here.

For example:
src/services/post.ts

import { MedusaError } from "@medusajs/utils"

class PostService extends TransactionBaseService {
// ...

async retrieve(
id: string,
config?: FindConfig
): Promise {
const postRepo = this.activeManager_.withRepository(
this.postRepository_
)

const query = buildQuery({
  id,
}, config)

const post = await postRepo.findOne(query)

if (!post) {
  throw new MedusaError(
    MedusaError.Types.NOT_FOUND,
    "Post was not found"
  )
}

return post

}
}

Use a Service

In this section, you'll learn how to use services throughout your Medusa backend. This includes both Medusa's services and your custom services.
Note

Before using your service, make sure you run the build command.
In a Service

To use your custom service in another custom service, you can have easy access to it in the dependencies injected to the constructor of your service:

class MyService extends TransactionBaseService {
constructor(container) {
super(container)
this.postService = container.postService
}
// ...
}

In an API Route

To use your custom service in an API Route, you can use MedusaRequest object's scope.resolve method passing it the service’s registration name:

const postService = req.scope.resolve("postService")

res.json({
posts: postService.list(),
})

In a Subscriber

To resolve your custom service in subscriber handler functions, use the container property of the handler function's parameter. The container has a method resolve which accepts the registration name of the service as a parameter.

For example:

import {
type SubscriberConfig,
type SubscriberArgs,
} from "@medusajs/medusa"

import { PostService } from "../services/post.ts"

export default async function postHandler({
data, eventName, container, pluginOptions,
}: SubscriberArgs<Record<string, string>>) {
const postService: PostService = container.resolve(
"postService"
)

// ...
}

// ...

Troubleshooting
AwilixResolutionError: Could Not Resolve X
Example: Services with CRUD Operations

In this section, you'll find a full example of the PostService and AuthorService that implement Create, Read, Update, and Delete (CRUD) operations.

You can refer to the Entities documentation to learn how to create the custom entities used in this example.

src/services/post.ts
src/services/author.ts

import {
FindConfig,
Selector,
TransactionBaseService,
buildQuery,
} from "@medusajs/medusa"
import { PostRepository } from "../repositories/post"
import { Post } from "../models/post"
import { MedusaError } from "@medusajs/utils"

class PostService extends TransactionBaseService {
protected postRepository_: typeof PostRepository

constructor(container) {
super(container)
this.postRepository_ = container.postRepository
}

async listAndCount(
selector?: Selector,
config: FindConfig = {
skip: 0,
take: 20,
relations: [],
}): Promise<[Post[], number]> {
const postRepo = this.activeManager_.withRepository(
this.postRepository_
)

const query = buildQuery(selector, config)

return postRepo.findAndCount(query)

}

async list(
selector?: Selector,
config: FindConfig = {
skip: 0,
take: 20,
relations: [],
}): Promise<Post[]> {
const [posts] = await this.listAndCount(selector, config)

return posts

}

async retrieve(
id: string,
config?: FindConfig
): Promise {
const postRepo = this.activeManager_.withRepository(
this.postRepository_
)

const query = buildQuery({
  id,
}, config)

const post = await postRepo.findOne(query)

if (!post) {
  throw new MedusaError(
    MedusaError.Types.NOT_FOUND,
    "Post was not found"
  )
}

return post

}

async create(
data: Pick<Post, "title" | "author_id">
): Promise {
return this.atomicPhase_(async (manager) => {
const postRepo = manager.withRepository(
this.postRepository_
)
const post = postRepo.create()
post.title = data.title
post.author_id = data.author_id
const result = await postRepo.save(post)

  return result
})

}

async update(
id: string,
data: Omit<Partial, "id">
): Promise {
return await this.atomicPhase_(async (manager) => {
const postRepo = manager.withRepository(
this.postRepository_
)
const post = await this.retrieve(id)

  Object.assign(post, data)

  return await postRepo.save(post)
})

}

async delete(id: string): Promise {
return await this.atomicPhase_(async (manager) => {
const postRepo = manager.withRepository(
this.postRepository_
)
const post = await this.retrieve(id)

  await postRepo.remove([post])
})

}
}

export default PostService
How to Extend a Service

In this document, you’ll learn how to extend a core service in Medusa.
Overview

Medusa’s core services cover a wide range of functionalities related to each domain or entity. You can extend these services to add custom methods or override existing methods.
Word of Caution about Overriding

Extending services to add new methods shouldn't cause any issues within your commerce application. However, if you extend them to override their existing methods, you should be aware that this could have negative implications, such as unanticipated bugs, especially when you try to upgrade the core Medusa package to a newer version.
Step 1: Create the Service File

In your Medusa backend, create the file src/services/product.ts. This file will hold your extended service.

Note that the name of the file must be the same as the name of the original service in the core package. So, if you’re extending the ProductService, the file’s name should be product.ts. On the other hand, if you’re extending the CustomerService, the file’s name should be customer.ts.
Step 2: Implementing the Service

In the file, you can import the original service from the Medusa core, then create your service that extends the core service.

For example, to extend the Product service:
src/services/product.ts

import {
ProductService as MedusaProductService,
} from "@medusajs/medusa"

class ProductService extends MedusaProductService {
// TODO add customizations
}

export default ProductService

Notice that you alias the ProductService of the core to avoid naming conflicts.

Within the service, you can add new methods or extend existing ones.

You can also change the lifetime of the service:
src/services/product.ts

import { Lifetime } from "awilix"
import {
ProductService as MedusaProductService,
} from "@medusajs/medusa"

class ProductService extends MedusaProductService {
// The default life time for a core service is SINGLETON
static LIFE_TIME = Lifetime.SCOPED

// ...
}

export default ProductService

You can learn more details about the service lifetime and other considerations when creating a service in the Create Service documentation.

Loaders

In this document, you’ll learn what loaders are in Medusa.
Overview

A loader is a script that runs when the Medusa backend starts. The Medusa backend uses loaders to initialize the database connection, load plugins, register resources in the dependency container, and more.

Loaders can be created within the Medusa backend codebase, in a plugin, or in a module to perform custom actions when the backend starts. The loader is created in a TypeScript or JavaScript file located in the src/loaders directory, then transpiled using the build command into the dist/loaders directory.
Loader Examples

For example, the Redis Event Bus module uses a loader to establish a connection with Redis and log a message in the console.

Another example is the Algolia plugin, which uses a loader to update the index settings when the Medusa backend starts based on the plugin’s options.

Loaders can be used to access the dependency container and register custom resources in it.

Loaders can also be used to create scheduled jobs that run at a specified interval of time.
How to Create a Loader

In this document, you’ll learn how to create a loader in Medusa. A loader can be created in the Medusa backend codebase, in a plugin, or in a module.
Step 1: Create Loader File

Create a TypeScript or JavaScript file in the src/loaders directory that will hold your custom script. There are no restrictions on the name of the file.

For example, create the file src/loaders/my-loader.ts that will hold the loader.
Step 2: Define the Loader

The loader file must export a function.
Parameters of Loaders in Medusa Backend and Plugins

When the loader is defined in the Medusa backend or a plugin, the function receives the following parameters:

container: the first parameter, which is a MedusaContainer object. You can use the container to register custom resources into the dependency container or resolve resources from the dependency container.
config: the second parameter, which is an object that holds the loader’s plugin options. If the loader is not created inside a plugin, the config object will be empty.

Parameters of Loaders in Modules

When the loader is defined in a module, it receives the following parameters:

container: the first parameter, which is a MedusaContainer object. You can use the container to register custom resources into the dependency container or resolve resources from the dependency container.
logger: the second parameter, which is a Logger object. The logger can be used to log messages in the console.
config: the third parameter, which is an object that holds the loader’s module options.

Example Implementation

For example, this loader function resolves the ProductService and logs in the console the count of products in the Medusa backend:
src/loaders/my-loader.ts

import {
ProductService,
ConfigModule,
MedusaContainer,
} from "@medusajs/medusa"

export default async (
container: MedusaContainer,
config: ConfigModule
): Promise => {
console.info("Starting loader...")
const productService = container.resolve(
"productService"
)
console.info(Products count: ${ await productService.count() })
console.info("Ending loader...")
}

Admin Configuration

In this document, you'll learn about the different ways you can configure the admin dashboard.
Plugin Options

The plugin accepts the following options:
medusa-config.js

const plugins = [
// ...
{
resolve: "@medusajs/admin",
/** @type {import('@medusajs/admin').PluginOptions} */
options: {
serve: true,
autoRebuild: true,
backend: "https://example.com",
path: "/app",
outDir: "build",
develop: {
open: true,
port: 7001,
host: "example.com",
logLevel: "error",
stats: "normal",
allowedHosts: "auto",
webSocketURL: undefined,
},
},
},
]

serveboolean
Whether to serve the admin dashboard when the Medusa backend starts. If set to false, you can serve the admin dashboard using the dev command.

Default: true
autoRebuildboolean
Whether the admin UI should be rebuilt if there are any changes or if a missing build is detected when the backend starts. If not set, you must manually build the admin dashboard.

Default: false
backendstring
The URL of the Medusa backend. Its value is only used if the serve option is set to false.
pathstring
The path the admin server should run on when running the Medusa backend in production. It must be prefixed with a slash /, but it can't end with a /, which throws an error. It also can't be one of the reserved paths: "admin" and "store".

Default: /app
outDirstring
The directory to output the admin build to. By default, the plugin builds the admin to the build directory in the root of the Medusa backend directory.
developobject
Options for the admin development server.
Admin CLI
Build Command Options

The build command in the admin CLI allows you to manually build the admin dashboard. If you intend to use it, you should typically add it to the package.json of the Medusa backend:
package.json

{
"scripts": {
// other scripts...
"build:admin": "medusa-admin build"
}
}

You can add the following option to the medusa-admin build command:

--deployment: a boolean value indicating that the build should be ready for deployment. When this option is added, options are not loaded from medusa-config.js anymore, and it means the admin will be built to be hosted on an external host. This also means that the backend URL is loaded from the MEDUSA_ADMIN_BACKEND_URL environment variable. For example, medusa-admin build --deployment.

Develop Command Options

The develop command in the admin CLI allows you to run the admin dashboard in development separately from the Medusa backend. If you intend to use it, you should typically add it to the package.json of the Medusa backend:
package.json

{
"scripts": {
// other scripts...
"dev:admin": "medusa-admin develop"
}
}

You can add the following options to the medusa-admin develop command:

--backend or -b: a string specifying the URL of the Medusa backend. By default, it's the value of the environment variable MEDUSA_ADMIN_BACKEND_URL. For example, medusa-admin develop --backend example.com.
--port or -p: the port to run the admin on. By default, it's 7001. For example, medusa-admin develop --port 8000.

Change Backend URL
In Development - Disabled Serve

To change the backend's URL that the admin sends request to in development when the serve option is disabled, set the backend option to the URL of your Medusa backend.

For example:
medusa-config.js

const plugins = [
// ...
{
resolve: "@medusajs/admin",
/** @type {import('@medusajs/admin').PluginOptions} */
options: {
serve: false,
backend: "http://localhost:9001",
// other options...
},
},
]

In Development - Enabled Serve

To change the backend's URL when the serve option is enabled, set the host and port properties of the develop option to those of the Medusa backend. This is useful when running Medusa remotely in development, such as using GitHub Codespace.

For example:
medusa-config.js

const plugins = [
// ...
{
resolve: "@medusajs/admin",
/** @type {import('@medusajs/admin').PluginOptions} */
options: {
// other options...
develop: {
port: 7000,
host: "example.com",
},
},
},
]

In Production
Note

This assumes that you've deployed the admin separately and you're passing the --deployment option to the build command.

To change the backend's URL that the admin sends request to in production, set the environment variable MEDUSA_ADMIN_BACKEND_URL to the backend's URL.

For example:

MEDUSA_ADMIN_BACKEND_URL=https://example.com

Custom Environment Variables

If you want to set environment variables that you want to access in your admin dashboard's customizations (such as in widgets or UI routes), your environment variables must be prefixed with MEDUSA_ADMIN_. Otherwise, it won't be loaded within the admin.

For example:

MEDUSA_ADMIN_CUSTOM_API_KEY=123...

Custom Webpack Configurations
Note

Plugins cannot include webpack customizations.

The admin dashboard uses Webpack to define the necessary configurations for both the core admin plugin and your extensions. So, for example, everything works out of the box with Tailwind CSS, the admin dependencies, and more.

However, you may have some advanced case where you need to tweak the webpack configurations. For example, you want to support styling your extensions with CSS Modules.

For such use cases, you can extend the default webpack configurations defined in the admin plugin to add your custom configurations.

To do that, create the file src/admin/webpack.config.js that uses the withCustomWebpackConfig method imported from @medusajs/admin to export the extended configurations. The method accepts a callback function that must return an object of webpack configuration. The callback function accepts two parameters:

config: the first parameter is an object that holds the default webpack configuration. You should add your configurations to this object, then return it. Not returning the default configurations will lead to the application breaking.
webpack: the second parameter is the webpack instance.

Warning

This is an advanced feature and requires knowledge of configuring webpack. If configured wrongly, it may lead to the admin application breaking.

For example:
src/admin/webpack.config.js

const withCustomWebpackConfig = require(
"@medusajs/admin"
).withCustomWebpackConfig

module.exports = withCustomWebpackConfig((config, webpack) => {
config.plugins.push(
new webpack.DefinePlugin({
"process.env": {
NODE_ENV: JSON.stringify("production"),
API_URL:
JSON.stringify("https://api.medusa-commerce.com"),
},
})
)

return config
})

How to Create an Admin Widget

In this document, you will learn about what Admin widgets are and how you can create your own.
Overview

Admin Widgets are custom React components that developers create to be injected into predetermined injection zones across the Medusa Admin dashboard.

Widgets allow you to customize the admin dashboard by providing merchants with new features. For example, you can add a widget on the order details page that shows payment details retrieved from Stripe.

This guide explains the available injection zones and how to create an admin widget.
Injection Zones

Injection zones are areas in the admin that you can add widgets into. Widgets can only be added into these areas.

There are different types of injection zones, and the type affects where the Widget is injected. For the different domains such as product, order, and customer there are list and details zones.

Below is a full list of injection zones:
Note

You can learn more about the additional props in the Props section.

Injection Zone Name

Description

Additional Props

order.list.before

Added at the top of the orders list page

order.list.after

Added at the bottom of the order list page

order.details.before

Added at the top of the order details page

Type OrderDetailsWidgetProps imported from @medusajs/admin

{
order, // Order object
}

order.details.after

Added at the end of the order details page

Type OrderDetailsWidgetProps imported from @medusajs/admin

{
order, // Order object
}

draft_order.list.before

Added at the top of the draft orders list page

draft_order.list.after

Added at the bottom of the draft orders list page

draft_order.details.before

Added at the top of the draft order details page

Type DraftOrderDetailsWidgetProps imported from @medusajs/admin

{
draftOrder, // DraftOrder object
}

draft_order.details.after

Added at the bottom of the draft order details page

Type DraftOrderDetailsWidgetProps imported from @medusajs/admin

{
draftOrder, // DraftOrder object
}

customer.list.before

Added at the top of the customers list page

customer.list.after

Added at the bottom of the customers list page

customer.details.before

Added at the top of the customer details page

Type CustomerDetailsWidgetProps imported from @medusajs/admin

{
customer, // Customer object
}

customer.details.after

Added at the bottom of the customer details page

Type CustomerDetailsWidgetProps imported from @medusajs/admin

{
customer, // Customer object
}

customer_group.list.before

Added at the top of the customer groups list page

customer_group.list.after

Added at the bottom of the customer groups list page

customer_group.details.before

Added at the top of the customer group details page

Type CustomerGroupDetailsWidgetProps imported from @medusajs/admin

{
customerGroup, // CustomerGroup object
}

customer_group.details.after

Added at the bottom of the customer group details page

Type CustomerGroupDetailsWidgetProps imported from @medusajs/admin

{
customerGroup, // CustomerGroup object
}

product.list.before

Added at the top of the product list page

product.list.after

Added at the bottom of the products list page

product.details.before

Added at the top of the product details page

Type ProductDetailsWidgetProps imported from @medusajs/admin

{
product, // Product object
}

product.details.after

Added at the bottom of the product details page

Type ProductDetailsWidgetProps imported from @medusajs/admin

{
product, // Product object
}

product_collection.list.before

Added at the top of the product collections list page

product_collection.list.after

Added at the bottom of the product collections list page

product_collection.details.before

Added at the top of the product collection details page

Type ProductCollectionDetailsWidgetProps imported from @medusajs/admin.

{
productCollection, // Collection object
}

product_collection.details.after

Added at the bottom of the product collections list page

Type ProductCollectionDetailsWidgetProps imported from @medusajs/admin.

{
productCollection, // Collection object
}

price_list.list.before

Added at the top of the “price list” list page

price_list.list.after

Added at the bottom of the “price list” list page

price_list.details.before

Added at the top of the “price list” details page

Type PriceListDetailsWidgetProps imported from @medusajs/admin

{
priceList, // PriceList object
}

price_list.details.after

Added at the bottom of the “price list” details page

Type PriceListDetailsWidgetProps imported from @medusajs/admin

{
priceList, // PriceList object
}

discount.list.before

Added at the top of the discounts list page

discount.list.after

Added at the bottom of the discounts list page

discount.details.before

Added at the top of the discounts details page

Type DiscountDetailsWidgetProps imported from @medusajs/admin

{
discount, // Discount object
}

discount.details.after

Added at the bottom of the discount details page

Type DiscountDetailsWidgetProps imported from @medusajs/admin

{
discount, // Discount object
}

gift_card.list.before

Added at the top of the gift cards list page

gift_card.list.after

Added at the bottom of the gift cards list page

gift_card.details.before

Added at the top of the gift card details page

Type GiftCardDetailsWidgetProps imported from @medusajs/admin

{
giftCard, // Product object
}

gift_card.details.after

Added at the bottom of the gift card details page

Type GiftCardDetailsWidgetProps imported from @medusajs/admin

{
giftCard, // Product object
}

custom_gift_card.before

Added at the top of the custom gift card page

Type GiftCardCustomWidgetProps imported from @medusajs/admin

{
giftCard, // GiftCard object
}

custom_gift_card.after

Added at the bottom of the custom gift card page

Type GiftCardCustomWidgetProps imported from @medusajs/admin

{
giftCard, // GiftCard object
}

login.before

Added before the login form

login.after

Added after the login form

    Widget Requirements

A Widget must adhere to a set of criteria that determines if it is valid for injection. These are:

All widget files must be placed in the folder /src/admin/widgets in your backend directory.
A widget file must have a valid React component as its default export.
A widget file must export a config object of type WidgetConfig imported from @medusajs/admin.

WidgetConfig

WidgetConfig is used to determine the configurations of the widget, mainly the injection zones. It’s an object that accepts the property zone, which can be a single or an array of injection zone strings. For example:

{
zone: "product.details.after"
}

How to Create a Widget

In this section, you’ll learn how to create an admin widget.
Prerequisites

It’s assumed you already have a Medusa backend with the admin plugin installed before you move forward with this guide. If not, you can follow this documentation page to install a Medusa project.
(Optional) TypeScript Preparations

Since Widgets are React components, they should be written in .tsx or .jsx files. If you’re using Typescript, you need to make some adjustments to avoid Typescript errors in your Admin files.

This section provides recommended configurations to avoid any TypeScript errors.
Note

These changes may already be available in your Medusa project. They're included here for reference purposes.

First, update your tsconfig.json with the following configurations:
tsconfig.json

{
"compilerOptions": {
"target": "es2019",
"module": "commonjs",
"allowJs": true,
"checkJs": false,
"jsx": "react-jsx",
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"noEmit": false,
"strict": false,
"moduleResolution": "node",
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/"],
"exclude": [
"dist",
"build",
".cache",
"tests",
"/*.spec.js",
"/*.spec.ts",
"node_modules",
".eslintrc.js"
]
}

The important changes to note here are the inclusion of the field "jsx": "react-jsx" and the addition of "build" and “.cache” to exclude.

The addition of "jsx": "react-jsx" specified how should TypeScript transform JSX, and excluding build and .cache ensures that TypeScript ignores build and development files.

Next, create the file tsconfig.server.json with the following content:
tsconfig.server.json

{
"extends": "./tsconfig.json",
"compilerOptions": {
/* Emit a single file with source maps instead of having a separate file. /
"inlineSourceMap": true
},
"exclude": ["src/admin", "**/.spec.js"]
}

This is the configuration that will be used to transpile your custom backend code, such as services or entities. The important part is that it excludes src/admin as that is where your Admin code will live.

Finally, create the file tsconfig.admin.json with the following content:
tsconfig.admin.json

{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "esnext"
},
"include": ["src/admin"],
"exclude": ["**/*.spec.js"]
}

This is the configuration that will be used when transpiling your admin code.
(Optional) Update Scripts in package.json

You can optionally update the following scripts in package.json to make your development process easier:
package.json

{
// ...
"scripts": {
"clean": "cross-env ./node_modules/.bin/rimraf dist",
"build": "cross-env npm run clean && npm run build:server && npm run build:admin",
"build:server": "cross-env npm run clean && tsc -p tsconfig.server.json",
"build:admin": "cross-env medusa-admin build",
"watch": "cross-env tsc --watch",
"test": "cross-env jest",
"seed": "cross-env medusa seed -f ./data/seed.json",
"start": "cross-env npm run build && medusa start",
"start:custom": "cross-env npm run build && node --preserve-symlinks index.js",
"dev": "cross-env npm run build:server && medusa develop"
},
// ...
}

Note

If you have autoRebuild enabled in the options of @medusajs/admin, you shouldn’t include npm run build:admin in the build script. It will lead to the admin being built twice during development.
Create the Admin Widget

To create a new admin widget, start by creating the folder src/admin/widgets. This is where your widgets must be located, as explained in the Widgets Requirements section.

Then, create the file src/admin/widgets/product-widget.tsx with the following content:
src/admin/widgets/product-widget.tsx

import type { WidgetConfig } from "@medusajs/admin"

const ProductWidget = () => {
return (

Product Widget


)
}

export const config: WidgetConfig = {
zone: "product.details.after",
}

export default ProductWidget

This file creates a React Component ProductWidget which renders an H1 header. This React Component is the default export in the file, which is one of the Widgets Requirements.

You also export the object config of type WidgetConfig, which is another widget requirement. It indicates that the widget must be injected in the product.details.after zone.

To test out your widget, run the following command in the root backend directory:

npx medusa develop

This command will build your backend and admin, then runs the backend.

Open localhost:7001 in your browser and log in. Then, go to the details page of any product. You should now see your widget at the bottom of the page.

Try making any changes to the component. The development server will hot-reload and your widget will be updated immediately.
Widget Props

Every widget receives props of the type WidgetProps, which includes the notify prop. The notify prop is an object that includes the following attributes:

success: a function that can be used to show a success message.
error: a function that can be used to show an error message.
warn: a function that can be used to show a warning message.
info: a function that can be used to show an info message.

In addition, some injection zones provide additional props specific to the context of the page. For example, widgets in the product.details.after zone will also receive a product prop, which is an object holding the data of the product being viewed.

You can learn about what additional props each injection zone may receive in the Injection Zone section.

For example, you can modify the widget you created to show the title of the product:
src/admin/widgets/product-widget.tsx

import type {
WidgetConfig,
ProductDetailsWidgetProps,
} from "@medusajs/admin"

const ProductWidget = ({
product,
notify,
}: ProductDetailsWidgetProps) => {
return (

Product Widget {product.title}

<button
className="bg-black rounded p-1 text-white"
onClick={() => notify.success("success", "You clicked the button!")}
>
Click me


)
}

export const config: WidgetConfig = {
zone: "product.details.after",
}

export default ProductWidget

Styling your Widget

Admin Widgets support Tailwind CSS out of the box.

For example, you can update the widget you created earlier to use Tailwind CSS classes:
src/admin/widgets/product-widget.tsx

import type {
WidgetConfig,
} from "@medusajs/admin"

const ProductWidget = () => {
return (

Product Widget


)
}

export const config: WidgetConfig = {
zone: "product.details.after",
}

export default ProductWidget

Routing Functionalities

If you want to navigate to other pages, link to other pages, or use other routing functionalities, you can use react-router-dom package.
Note

react-router-dom is available as one of the @medusajs/admin dependencies. You can also install it within your project using the following command:

npm
Yarn
pnpm

yarn add react-router-dom

If you're installing it in a plugin with admin customizations, make sure to include it in peerDependencies.

For example:
src/admin/widgets/product-widget.tsx

import type { WidgetConfig } from "@medusajs/admin"
import { Link } from "react-router-dom"

const ProductWidget = () => {
return (

Product Widget

<Link to={"/a/orders"}>
View Orders


)
}

export const config: WidgetConfig = {
zone: "product.details.after",
}

export default ProductWidget

View react-router-dom’s documentation for other available components and hooks.
Querying and Mutating Data

You will most likely need to interact with the Medusa backend from your Widgets. To do so, you can utilize the Medusa React package. It contains a collection of queries and mutation built on @tanstack/react-query that lets you interact with the Medusa backend.
Note

Make sure to also install the Medusa React package first if you’re intending to use it, as explained in the Medusa React guide.

For example, you can modify the widget you created to retrieve the tags of a product from the Medusa backend:
src/admin/widgets/product-widget.tsx

import type { ProductDetailsWidgetProps, WidgetConfig } from "@medusajs/admin"
import { useAdminProductTags } from "medusa-react"

const ProductWidget = ({ product }: ProductDetailsWidgetProps) => {
const { product_tags } = useAdminProductTags({
id: product.tags.map((tag) => tag.id),
limit: 10,
offset: 0,
})

return (

Product Tags


{product_tags?.map((tag) => (

{tag.value}

))}


)
}

export const config: WidgetConfig = {
zone: "product.details.after",
}

export default ProductWidget

Custom API Routes
You can also use medusa-react to interact with custom API Routes using the createCustomAdminHooks utility function.
How to Create an Admin UI Route

In this document, you’ll learn how to create a new route in the admin dashboard.
Overview

You can customize the admin dashboard that Medusa provides to add new routes. This is useful if you want to add new subpages to the admin dashboard, or you want to add new pages that appear in the sidebar as well.

An admin UI route is essentially a React Component created under the src/admin/routes directory.

This guide explains how to create a new route in the admin dashboard with some examples.
Note

If you want to create a page under the Settings page, please refer to this documentation instead.
Prerequisites

It’s assumed you already have a Medusa backend with the admin plugin installed before you move forward with this guide. If not, you can follow this documentation page to install a Medusa project.
(Optional) TypeScript Preparations

Since routes are React components, they should be written in .tsx or .jsx files. If you’re using Typescript, you need to make some adjustments to avoid Typescript errors in your Admin files.

This section provides recommended configurations to avoid any TypeScript errors.
Note

These changes may already be available in your Medusa project. They're included here for reference purposes.

First, update your tsconfig.json with the following configurations:
tsconfig.json

{
"compilerOptions": {
"target": "es2019",
"module": "commonjs",
"allowJs": true,
"checkJs": false,
"jsx": "react-jsx",
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"noEmit": false,
"strict": false,
"moduleResolution": "node",
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/"],
"exclude": [
"dist",
"build",
".cache",
"tests",
"/*.spec.js",
"/*.spec.ts",
"node_modules",
".eslintrc.js"
]
}

The important changes to note here are the inclusion of the field "jsx": "react-jsx" and the addition of "build" and “.cache” to exclude.

The addition of "jsx": "react-jsx" specified how should TypeScript transform JSX, and excluding build and .cache ensures that TypeScript ignores build and development files.

Next, create the file tsconfig.server.json with the following content:
tsconfig.server.json

{
"extends": "./tsconfig.json",
"compilerOptions": {
/* Emit a single file with source maps instead of having a separate file. /
"inlineSourceMap": true
},
"exclude": ["src/admin", "**/.spec.js"]
}

This is the configuration that will be used to transpile your custom backend code, such as services or entities. The important part is that it excludes src/admin as that is where your Admin code will live.

Finally, create the file tsconfig.admin.json with the following content:
tsconfig.admin.json

{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "esnext"
},
"include": ["src/admin"],
"exclude": ["**/*.spec.js"]
}

This is the configuration that will be used when transpiling your admin code.
Create the Admin UI Route

In this section, you’ll learn the basics of creating an admin UI route.
Step 1: Create File

Custom admin UI routes are added under the src/admin/routes directory of your Medusa project. The path of the file depends on the path you want the route to be available under. It is based on Next.js 13’s App Router.

For example, if you want the route to be available in the admin dashboard under the path /a/custom you should create your admin route under the path src/admin/routes/custom/page.tsx.
Tip

All admin routes are prefixed with /a by default.

You can also create dynamic routes. For example, you can create the route /a/custom/[id] by creating an admin router under the path src/admin/routes/custom/[id]/page.tsx.
Step 2: Create React Component in File

For an admin route to be valid, it must default export a React component. There are no restrictions on the content of the React component.

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

const CustomPage = () => {
return (

This is my custom route

)
}

export default CustomPage

This will create an admin UI route at the path /a/custom, with its content being the content of the React component.
Step 3: Test it Out

To test your admin UI route, run the following command in the root directory of the Medusa backend project:

npm
Yarn
pnpm

npx medusa develop

This will build your admin and opens a window in your default browser to localhost:7001. After you log in, if you go to localhost:7001/a/custom, you’ll find the page you just created.
Note

When using the develop command, the admin dashboard will run in development mode and will restart whenever you make changes to your admin customizations. This allows you to see changes in the dashboard instantly during your development.
Route Props

Every route receives props of the type RouteProps, which includes the notify prop. The notify prop is an object that includes the following attributes:

success: a function that can be used to show a success message.
error: a function that can be used to show an error message.
warn: a function that can be used to show a warning message.
info: a function that can be used to show an info message.

For example:

import { Post } from "../../../../../models/post"
import PostForm from "../../../../components/post/form"
import { RouteProps } from "@medusajs/admin"

const BlogPostCreatePage = ({
notify,
}: RouteProps) => {
const onSuccess = (post: Post) => {
notify.success(
"Success",
Post ${post.title} created successfully
)
}

return (

Create Post



)
}

export default BlogPostCreatePage

Show Route in Sidebar

You can add your routes into the admin dashboard sidebar by exporting an object of type RouteConfig import from @medusajs/admin in the same route file.

The object has one property link, which is an object having the following properties:

label: a string indicating the sidebar item’s label of your custom route.
icon: an optional React component that acts as an icon in the sidebar. If none provided, a default icon is used.

For example, you can change the content of the previous route you created to export a config object:
src/admin/routes/custom/page.tsx

import { RouteConfig } from "@medusajs/admin"
import { CustomIcon } from "../../icons/custom"

const CustomPage = () => {
return (

This is my custom route

)
}

export const config: RouteConfig = {
link: {
label: "Custom Route",
icon: CustomIcon,
},
}

export default CustomPage

Retrieve Path Parameters

As mentioned earlier, you can create dynamic routes like /a/custom/[id] by creating a route file at the path src/admin/routes/custom/[id]/page.tsx.

To retrieve the path parameter, you can use the useParams hook retrieved from the react-router-dom package.
Note

react-router-dom is available as one of the @medusajs/admin dependencies. You can also install it within your project using the following command:

npm
Yarn
pnpm

yarn add react-router-dom

If you're installing it in a plugin with admin customizations, make sure to include it in peerDependencies.

For example:
src/admin/routes/custom/[id]/page.tsx

import { useParams } from "react-router-dom"

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

return (

Passed ID: {id}

)
}

export default CustomPage

Routing Functionalities

If you want to use routing functionalities such as linking to another page or navigating between pages, you can use react-router-dom's utility hooks and functions.

For example, to add a link to another page:
src/admin/routes/custom/page.tsx

import { Link } from "react-router-dom"

const CustomPage = () => {

return (

<Link to={"/a/products"}>
View Products


)
}

export default CustomPage

View react-router-dom’s documentation for other available components and hooks.
Styling Route

Admin UI routes support Tailwind CSS by default.

For example, to customize your custom route:
src/admin/routes/custom/page.tsx

const CustomPage = () => {
return (

This is my custom route

)
}

export default CustomPage

Querying and Mutating Data

You might need to interact with the Medusa backend from your admin route. To do so, you can utilize the Medusa React package. It contains a collection of queries and mutation built on @tanstack/react-query@4.22 that lets you interact with the Medusa backend.
Note

Make sure to also install the Medusa React package first if you’re intending to use it, as explained in the Medusa React guide.

For example, you can retrieve available products and display them in your route:
src/admin/routes/custom/page.tsx

import { useAdminProducts } from "medusa-react"

const CustomPage = () => {
const { products } = useAdminProducts()
return (

{products?.map((product) => product.title)}

)
}

export default CustomPage

Custom API Routes
You can also use medusa-react to interact with custom API Routes using Custom Hooks utility functions.
How to Create an Admin Setting Page

In this document, you’ll learn how to create a setting page in the admin.
Overview

The admin UI routes allow you to add new pages to the admin dashboard. However, they can’t be used to add a new tab under the Setting page.

To do that, you need to create an Admin setting page. The page will automatically be shown as a tab under the Setting page in the admin. The tab leads to the content of your custom page.

A setting page is essentially a React Component created under the src/admin/settings directory.

This guide explains how to create a new setting page in the admin dashboard with some examples.
Prerequisites

It’s assumed you already have a Medusa backend with the admin plugin installed before you move forward with this guide. If not, you can follow this documentation page to install a Medusa project.
(Optional) TypeScript Preparations

Since setting pages are React components, they should be written in .tsx or .jsx files. If you’re using Typescript, you need to make some adjustments to avoid Typescript errors in your Admin files.

This section provides recommended configurations to avoid any TypeScript errors.
Note

These changes may already be available in your Medusa project. They're included here for reference purposes.

First, update your tsconfig.json with the following configurations:
tsconfig.json

{
"compilerOptions": {
"target": "es2019",
"module": "commonjs",
"allowJs": true,
"checkJs": false,
"jsx": "react-jsx",
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"noEmit": false,
"strict": false,
"moduleResolution": "node",
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/"],
"exclude": [
"dist",
"build",
".cache",
"tests",
"/*.spec.js",
"/*.spec.ts",
"node_modules",
".eslintrc.js"
]
}

The important changes to note here are the inclusion of the field "jsx": "react-jsx" and the addition of "build" and “.cache” to exclude.

The addition of "jsx": "react-jsx" specified how should TypeScript transform JSX, and excluding build and .cache ensures that TypeScript ignores build and development files.

Next, create the file tsconfig.server.json with the following content:
tsconfig.server.json

{
"extends": "./tsconfig.json",
"compilerOptions": {
/* Emit a single file with source maps instead of having a separate file. /
"inlineSourceMap": true
},
"exclude": ["src/admin", "**/.spec.js"]
}

This is the configuration that will be used to transpile your custom backend code, such as services or entities. The important part is that it excludes src/admin as that is where your Admin code will live.

Finally, create the file tsconfig.admin.json with the following content:
tsconfig.admin.json

{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "esnext"
},
"include": ["src/admin"],
"exclude": ["**/*.spec.js"]
}

This is the configuration that will be used when transpiling your admin code.
Create the Setting Page

In this section, you’ll learn the basics of creating an admin UI route.
Step 1: Create File

Custom admin setting pages are added under the src/admin/settings directory of your Medusa project. The path of the file depends on the path you want the setting page to be available under. It is based on Next.js 13’s App Router.

All setting page paths are prefixed with /a/settings.

For example, if you want the setting page to be available in the admin dashboard under the path /a/settings/custom you should create your setting page under the file path src/admin/settings/custom/page.tsx.
Note

Only one-level deep files are accepted. So, for example, you can create the page src/admin/settings/custom/page.tsx, but not src/admin/settings/custom/nested/page.tsx.
Step 2: Create React Component in File

For a setting page to be valid, it must default export a React component. There are no restrictions on the content of the React component. It must also export a configuration object that indicates how the tab is shown on the Setting page.

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

import type { SettingConfig } from "@medusajs/admin"
import { CustomIcon } from "../../icons/custom"

const CustomSettingPage = () => {
return (

Custom Setting Page


)
}

export const config: SettingConfig = {
card: {
label: "Custom",
description: "Manage your custom settings",
// optional
icon: CustomIcon,
},
}

export default CustomSettingPage

This creates a setting page that will be available at /a/settings/custom. The content of the page is defined in the exported React component.

The exported configuration also indicates the details of the tab that will be added to the Setting page. You must specify a label and description, and you can optionally specify an icon. The icon is a React component.
Step 3: Test it Out

To test your admin setting page, run the following command in the root directory of the Medusa backend project:

npx medusa develop

This will build your admin and opens a window in your default browser to localhost:7001. After you log in, go to localhost:7001/a/settings. You’ll find a new tab available under a new Extensions section.

If you click on the tab, a new page will open with the content as defined in your React component.
Setting Page Props

Every route receives props of the type RouteProps, which includes the notify prop. The notify prop is an object that includes the following attributes:

success: a function that can be used to show a success message.
error: a function that can be used to show an error message.
warn: a function that can be used to show a warning message.
info: a function that can be used to show an info message.

For example:
src/admin/settings/custom/page.tsx

import type { SettingConfig } from "@medusajs/admin"
import type { SettingProps } from "@medusajs/admin"

const CustomSettingPage = ({
notify,
}: SettingProps) => {

const handleClick = () => {
notify.success("Success", "You clicked the button")
}

return (

Custom Setting Page



)
}

export const config: SettingConfig = {
card: {
label: "Custom",
description: "Manage your custom settings",
},
}

export default CustomSettingPage

Styling Setting Page

Admin setting pages support Tailwind CSS by default.

For example, to customize the style of your custom setting page:
src/admin/settings/custom/page.tsx

import type { SettingConfig } from "@medusajs/admin"

const CustomSettingPage = () => {
return (

Custom Setting Page


)
}

export const config: SettingConfig = {
card: {
label: "Custom",
description: "Manage your custom settings",
},
}

export default CustomSettingPage

Use Routing Functionalities

If you want to use routing functionalities such as linking to another page or navigating between pages, you can use react-router-dom's utility hooks and functions.
Note

💡 react-router-dom is available as one of the @medusajs/admin dependencies. You can also install it within your project using the following command:

npm
Yarn
pnpm

yarn add react-router-dom

If you're installing it in a plugin with admin customizations, make sure to include it in peerDependencies.

For example, to add a link to another page:
src/admin/settings/custom/page.tsx

import type { SettingConfig } from "@medusajs/admin"
import { Link } from "react-router-dom"

const CustomSettingPage = () => {
return (

Custom Setting Page

<Link to={"/a/products"}>
View Products


)
}

export const config: SettingConfig = {
card: {
label: "Custom",
description: "Manage your custom settings",
},
}

export default CustomSettingPage

View react-router-dom’s documentation for other available components and hooks.
Querying and Mutating Data

You might need to interact with the Medusa backend from your admin setting page. To do so, you can utilize the Medusa React package. It contains a collection of queries and mutations built on @tanstack/react-query@4.22 that lets you interact with the Medusa backend.
Note

Make sure to also install the Medusa React package first if you’re intending to use it, as explained in the Medusa React guide.

For example, you can retrieve available products and display them in your route:
src/admin/settings/custom/page.tsx

import type { SettingConfig } from "@medusajs/admin"
import { useAdminProducts } from "medusa-react"

const CustomSettingPage = () => {
const { products } = useAdminProducts()

return (

Custom Setting Page


{products?.map((product) => product.title)}


)
}

export const config: SettingConfig = {
card: {
label: "Custom",
description: "Manage your custom settings",
},
}

export default CustomSettingPage

Custom API Routes
You can also use medusa-react to interact with custom API Routes using Custom Hooks utility functions.
Example: How to Create Onboarding Widget

In this guide, you’ll learn how to build the onboarding widget available in the admin dashboard the first time you install a Medusa project.
Note

The onboarding widget is already implemented within the codebase of your Medusa backend. This guide is helpful if you want to understand how it was implemented or you want an example of customizing the Medusa Admin and backend.
What you’ll be Building

By following this tutorial, you’ll:

Build an onboarding flow in the admin that takes the user through creating a sample product and order. This flow has four steps and navigates the user between four pages in the admin before completing the guide. This will be implemented using Admin Widgets.
Keep track of the current step the user has reached by creating a table in the database and an API Route that the admin widget uses to retrieve and update the current step. These customizations will be applied to the backend.

Onboarding Widget Demo
Prerequisites

Before you follow along this tutorial, you must have a Medusa backend installed. If not, you can use the following command to get started:

npx create-medusa-app@latest

Please refer to the create-medusa-app documentation for more details on this command, including prerequisites and troubleshooting.
Preparation Steps

The steps in this section are used to prepare for the custom functionalities you’ll be creating in this tutorial.
(Optional) TypeScript Configurations and package.json

If you're using TypeScript in your project, it's highly recommended to setup your TypeScript configurations and package.json as mentioned in this guide.
Install Medusa React

Medusa React is a React library that facilitates using Medusa’s API Routes within your React application. It also provides the utility to register and use custom API Routes.

To install Medusa React and its required dependencies, run the following command in the root directory of the Medusa backend:

npm
Yarn
pnpm

yarn add medusa-react @tanstack/react-query@4.22

Implement Helper Resources

The resources in this section are used for typing, layout, and design purposes, and they’re used in other essential components in this tutorial.

Each of the collapsible elements below hold the path to the file that you should create, and the content of that file.
src/admin/types/icon-type.ts
src/admin/components/shared/icons/get-started.tsx
src/admin/components/shared/icons/active-circle-dotted-line.tsx
src/admin/components/shared/accordion.tsx
src/admin/components/shared/card.tsx
Step 1: Customize Medusa Backend
Note

If you’re not interested in learning about backend customizations, you can skip to step 2.

In this step, you’ll customize the Medusa backend to:

Add a new table in the database that stores the current onboarding step. This requires creating a new entity, repository, and migration.
Add new API Routes that allow retrieving and updating the current onboarding step. This also requires creating a new service.

Create Entity

An entity represents a table in the database. It’s based on Typeorm, so it requires creating a repository and a migration to be used in the backend.

To create the entity, create the file src/models/onboarding.ts with the following content:
src/models/onboarding.ts

import { BaseEntity } from "@medusajs/medusa"
import { Column, Entity } from "typeorm"

@Entity()
export class OnboardingState extends BaseEntity {
@Column()
current_step: string

@Column()
is_complete: boolean

@Column()
product_id: string
}

Then, create the file src/repositories/onboarding.ts that holds the repository of the entity with the following content:
src/repositories/onboarding.ts

import {
dataSource,
} from "@medusajs/medusa/dist/loaders/database"
import { OnboardingState } from "../models/onboarding"

const OnboardingRepository = dataSource.getRepository(
OnboardingState
)

export default OnboardingRepository

You can learn more about entities and repositories in this documentation.
Create Migration

A migration is used to reflect database changes in your database schema.

To create a migration, run the following command in the root of your Medusa backend:

npx typeorm migration:create src/migrations/CreateOnboarding

This will create a file in the src/migrations directory with the name formatted as -CreateOnboarding.ts.

In that file, import the generateEntityId utility method at the top of the file:

import { generateEntityId } from "@medusajs/utils"

Then, replace the up and down methods in the migration class with the following content:

export class CreateOnboarding1685715079776 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise {
await queryRunner.query(
CREATE TABLE "onboarding_state" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "current_step" character varying NULL, "is_complete" boolean, "product_id" character varying NULL)
)

await queryRunner.query(
  `INSERT INTO "onboarding_state" ("id", "current_step", "is_complete") VALUES ('${generateEntityId(
    "",
    "onboarding"
  )}' , NULL, false)`
)

}

public async down(queryRunner: QueryRunner): Promise {
await queryRunner.query(DROP TABLE "onboarding_state")
}
}

Warning

Don’t copy the name of the class in the code snippet above. Keep the name you have in the file.

Finally, to reflect the migration in the database, run the build and migration commands:

npm
Yarn
pnpm

yarn build
npx medusa migrations run

You can learn more about migrations in this guide.
Create Service

A service is a class that holds helper methods related to an entity. For example, methods to create or retrieve a record of that entity. Services are used by other resources, such as API Routes, to perform functionalities related to an entity.

So, before you add the API Routes that allow retrieving and updating the onboarding state, you need to add the service that implements these helper functionalities.

Start by creating the file src/types/onboarding.ts with the following content:
src/types/onboarding.ts

import { OnboardingState } from "../models/onboarding"

export type UpdateOnboardingStateInput = {
current_step?: string;
is_complete?: boolean;
product_id?: string;
};

export interface AdminOnboardingUpdateStateReq {}

export type OnboardingStateRes = {
status: OnboardingState;
};

This file holds the necessary types that will be used within the service you’ll create, and later in your onboarding flow widget.

Then, create the file src/services/onboarding.ts with the following content:
src/services/onboarding.ts

import { TransactionBaseService } from "@medusajs/medusa"
import OnboardingRepository from "../repositories/onboarding"
import { OnboardingState } from "../models/onboarding"
import { EntityManager, IsNull, Not } from "typeorm"
import { UpdateOnboardingStateInput } from "../types/onboarding"

type InjectedDependencies = {
manager: EntityManager;
onboardingRepository: typeof OnboardingRepository;
};

class OnboardingService extends TransactionBaseService {
protected onboardingRepository_: typeof OnboardingRepository

constructor({ onboardingRepository }: InjectedDependencies) {
super(arguments[0])

this.onboardingRepository_ = onboardingRepository

}

async retrieve(): Promise<OnboardingState | undefined> {
const onboardingRepo = this.activeManager_.withRepository(
this.onboardingRepository_
)

const status = await onboardingRepo.findOne({
  where: { id: Not(IsNull()) },
})

return status

}

async update(
data: UpdateOnboardingStateInput
): Promise {
return await this.atomicPhase_(
async (transactionManager: EntityManager) => {
const onboardingRepository =
transactionManager.withRepository(
this.onboardingRepository_
)

    const status = await this.retrieve()

    for (const [key, value] of Object.entries(data)) {
      status[key] = value
    }

    return await onboardingRepository.save(status)
  }
)

}
}

export default OnboardingService

This service class implements two methods retrieve to retrieve the current onboarding state, and update to update the current onboarding state.

You can learn more about services in this documentation.
Create API Routes

The last part of this step is to create the API Routes that you’ll consume in the admin widget. There will be two API Routes: Get Onboarding State and Update Onboarding State.

To add these API Routes, create the file src/api/admin/onboarding/route.ts with the following content:
src/api/admin/onboarding/route.ts

import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import { EntityManager } from "typeorm"

import OnboardingService from "../../../services/onboarding"

export async function GET(
req: MedusaRequest,
res: MedusaResponse
) {
const onboardingService: OnboardingService =
req.scope.resolve("onboardingService")

const status = await onboardingService.retrieve()

res.status(200).json({ status })
}

export async function POST(
req: MedusaRequest,
res: MedusaResponse
) {
const onboardingService: OnboardingService =
req.scope.resolve("onboardingService")
const manager: EntityManager = req.scope.resolve("manager")

const status = await manager.transaction(
async (transactionManager) => {
return await onboardingService
.withTransaction(transactionManager)
.update(req.body)
}
)

res.status(200).json({ status })
}

Notice how these API Routes use the OnboardingService's retrieve and update methods to retrieve and update the current onboarding state. They resolve the OnboardingService using the Dependency Container.

You can learn more about API Routes in this documentation.
Step 2: Create Onboarding Widget

In this step, you’ll create the onboarding widget with a general implementation. Some implementation details will be added later in the tutorial.

Create the file src/admin/widgets/onboarding-flow/onboarding-flow.tsx with the following content:
src/admin/widgets/onboarding-flow/onboarding-flow.tsx

import { OrderDetailsWidgetProps, ProductDetailsWidgetProps, WidgetConfig, WidgetProps } from "@medusajs/admin"
import { useAdminCustomPost, useAdminCustomQuery, useMedusa } from "medusa-react"
import React, { useEffect, useState, useMemo, useCallback } from "react"
import { useNavigate, useSearchParams, useLocation } from "react-router-dom"
import { OnboardingState } from "../../../models/onboarding"
import {
AdminOnboardingUpdateStateReq,
OnboardingStateRes,
UpdateOnboardingStateInput,
} from "../../../types/onboarding"
import OrderDetailDefault from "../../components/onboarding-flow/default/orders/order-detail"
import OrdersListDefault from "../../components/onboarding-flow/default/orders/orders-list"
import ProductDetailDefault from "../../components/onboarding-flow/default/products/product-detail"
import ProductsListDefault from "../../components/onboarding-flow/default/products/products-list"
import { Button, Container, Heading, Text, clx } from "@medusajs/ui"
import Accordion from "../../components/shared/accordion"
import GetStarted from "../../components/shared/icons/get-started"
import { Order, Product } from "@medusajs/medusa"
import ProductsListNextjs from "../../components/onboarding-flow/nextjs/products/products-list"
import ProductDetailNextjs from "../../components/onboarding-flow/nextjs/products/product-detail"
import OrdersListNextjs from "../../components/onboarding-flow/nextjs/orders/orders-list"
import OrderDetailNextjs from "../../components/onboarding-flow/nextjs/orders/order-detail"

type STEP_ID =
| "create_product"
| "preview_product"
| "create_order"
| "setup_finished"
| "create_product_nextjs"
| "preview_product_nextjs"
| "create_order_nextjs"
| "setup_finished_nextjs"

type OnboardingWidgetProps = WidgetProps | ProductDetailsWidgetProps | OrderDetailsWidgetProps

export type StepContentProps = OnboardingWidgetProps & {
onNext?: Function;
isComplete?: boolean;
data?: OnboardingState;
};

type Step = {
id: STEP_ID;
title: string;
component: React.FC;
onNext?: Function;
};

const QUERY_KEY = ["onboarding_state"]

const OnboardingFlow = (props: OnboardingWidgetProps) => {
// create custom hooks for custom API Routes
const { data, isLoading } = useAdminCustomQuery<
undefined,
OnboardingStateRes

    ("/onboarding", QUERY_KEY)
    const { mutate } = useAdminCustomPost<
    AdminOnboardingUpdateStateReq,
    OnboardingStateRes
    ("/onboarding", QUERY_KEY)

const navigate = useNavigate()
const location = useLocation()
// will be used if onboarding step
// is passed as a path parameter
const { client } = useMedusa()

// get current step from custom API Route
const currentStep: STEP_ID | undefined = useMemo(() => {
return data?.status
?.current_step as STEP_ID
}, [data])

// initialize some state
const [openStep, setOpenStep] = useState(currentStep)
const [completed, setCompleted] = useState(false)

// this method is used to move from one step to the next
const setStepComplete = ({
step_id,
extraData,
onComplete,
}: {
step_id: STEP_ID;
extraData?: UpdateOnboardingStateInput;
onComplete?: () => void;
}) => {
const next = steps[findStepIndex(step_id) + 1]
mutate({ current_step: next.id, ...extraData }, {
onSuccess: onComplete,
})
}

// this is useful if you want to change the current step
// using a path parameter. It can only be changed if the passed
// step in the path parameter is the next step.
const [ searchParams ] = useSearchParams()

// the steps are set based on the
// onboarding type
const steps: Step[] = useMemo(() => {
{
switch(process.env.MEDUSA_ADMIN_ONBOARDING_TYPE) {
case "nextjs":
return [
{
id: "create_product_nextjs",
title: "Create Products",
component: ProductsListNextjs,
onNext: (product: Product) => {
setStepComplete({
step_id: "create_product_nextjs",
extraData: { product_id: product.id },
onComplete: () => {
if (!location.pathname.startsWith(/a/products/${product.id})) {
navigate(/a/products/${product.id})
}
},
})
},
},
{
id: "preview_product_nextjs",
title: "Preview Product in your Next.js Storefront",
component: ProductDetailNextjs,
onNext: () => {
setStepComplete({
step_id: "preview_product_nextjs",
onComplete: () => navigate(/a/orders),
})
},
},
{
id: "create_order_nextjs",
title: "Create an Order using your Next.js Storefront",
component: OrdersListNextjs,
onNext: (order: Order) => {
setStepComplete({
step_id: "create_order_nextjs",
onComplete: () => {
if (!location.pathname.startsWith(/a/orders/${order.id})) {
navigate(/a/orders/${order.id})
}
},
})
},
},
{
id: "setup_finished_nextjs",
title: "Setup Finished: Continue Building your Ecommerce Store",
component: OrderDetailNextjs,
},
]
default:
return [
{
id: "create_product",
title: "Create Product",
component: ProductsListDefault,
onNext: (product: Product) => {
setStepComplete({
step_id: "create_product",
extraData: { product_id: product.id },
onComplete: () => {
if (!location.pathname.startsWith(/a/products/${product.id})) {
navigate(/a/products/${product.id})
}
},
})
},
},
{
id: "preview_product",
title: "Preview Product",
component: ProductDetailDefault,
onNext: () => {
setStepComplete({
step_id: "preview_product",
onComplete: () => navigate(/a/orders),
})
},
},
{
id: "create_order",
title: "Create an Order",
component: OrdersListDefault,
onNext: (order: Order) => {
setStepComplete({
step_id: "create_order",
onComplete: () => {
if (!location.pathname.startsWith(/a/orders/${order.id})) {
navigate(/a/orders/${order.id})
}
},
})
},
},
{
id: "setup_finished",
title: "Setup Finished: Start developing with Medusa",
component: OrderDetailDefault,
},
]
}
}
}, [location.pathname])

// used to retrieve the index of a step by its ID
const findStepIndex = useCallback((step_id: STEP_ID) => {
return steps.findIndex((step) => step.id === step_id)
}, [steps])

// used to check if a step is completed
const isStepComplete = useCallback((step_id: STEP_ID) => {
return findStepIndex(currentStep) > findStepIndex(step_id)
}, [findStepIndex, currentStep])

// this is used to retrieve the data necessary
// to move to the next onboarding step
const getOnboardingParamStepData = useCallback(async (onboardingStep: string, data?: {
orderId?: string,
productId?: string,
}) => {
switch (onboardingStep) {
case "setup_finished_nextjs":
case "setup_finished":
if (!data?.orderId && "order" in props) {
return props.order
}
const orderId = data?.orderId || searchParams.get("order_id")
if (orderId) {
return (await client.admin.orders.retrieve(orderId)).order
}

    throw new Error ("Required `order_id` parameter was not passed as a parameter")
  case "preview_product_nextjs":
  case "preview_product":
    if (!data?.productId && "product" in props) {
      return props.product
    }
    const productId = data?.productId || searchParams.get("product_id")
    if (productId) {
      return (await client.admin.products.retrieve(productId)).product
    }

    throw new Error ("Required `product_id` parameter was not passed as a parameter")
  default:
    return undefined
}

}, [searchParams, props])

const isProductCreateStep = useMemo(() => {
return currentStep === "create_product" ||
currentStep === "create_product_nextjs"
}, [currentStep])

const isOrderCreateStep = useMemo(() => {
return currentStep === "create_order" ||
currentStep === "create_order_nextjs"
}, [currentStep])

// used to change the open step when the current
// step is retrieved from custom API Routes
useEffect(() => {
setOpenStep(currentStep)

if (findStepIndex(currentStep) === steps.length - 1) {setCompleted(true)}

}, [currentStep, findStepIndex])

// used to check if the user created a product and has entered its details page
// the step is changed to the next one
useEffect(() => {
if (location.pathname.startsWith("/a/products/prod_") && isProductCreateStep && "product" in props) {
// change to the preview product step
const currentStepIndex = findStepIndex(currentStep)
steps[currentStepIndex].onNext?.(props.product)
}
}, [location.pathname, isProductCreateStep])

// used to check if the user created an order and has entered its details page
// the step is changed to the next one.
useEffect(() => {
if (location.pathname.startsWith("/a/orders/order_") && isOrderCreateStep && "order" in props) {
// change to the preview product step
const currentStepIndex = findStepIndex(currentStep)
steps[currentStepIndex].onNext?.(props.order)
}
}, [location.pathname, isOrderCreateStep])

// used to check if the onboarding_step path
// parameter is passed and, if so, moves to that step
// only if it's the next step and its necessary data is passed
useEffect(() => {
const onboardingStep = searchParams.get("onboarding_step") as STEP_ID
const onboardingStepIndex = findStepIndex(onboardingStep)
if (onboardingStep && onboardingStepIndex !== -1 && onboardingStep !== openStep) {
// change current step to the onboarding step
const openStepIndex = findStepIndex(openStep)

  if (onboardingStepIndex !== openStepIndex + 1) {
    // can only go forward one step
    return
  }

  // retrieve necessary data and trigger the next function
  getOnboardingParamStepData(onboardingStep)
  .then((data) => {
    steps[openStepIndex].onNext?.(data)
  })
  .catch((e) => console.error(e))
}

}, [searchParams, openStep, getOnboardingParamStepData])

if (
!isLoading &&
data?.status?.is_complete &&
!localStorage.getItem("override_onboarding_finish")
)
{return null}

// a method that will be triggered when
// the setup is started
const onStart = () => {
mutate({ current_step: steps[0].id })
navigate(/a/products)
}

// a method that will be triggered when
// the setup is completed
const onComplete = () => {
setCompleted(true)
}

// a method that will be triggered when
// the setup is closed
const onHide = () => {
mutate({ is_complete: true })
}

// used to get text for get started header
const getStartedText = () => {
switch(process.env.MEDUSA_ADMIN_ONBOARDING_TYPE) {
case "nextjs":
return "Learn the basics of Medusa by creating your first order using the Next.js storefront."
default:
return "Learn the basics of Medusa by creating your first order."
}
}

return (
<>
<Container className={clx(
"text-ui-fg-subtle px-0 pt-0 pb-4",
{
"mb-4": completed,
}
)}>
<Accordion
type="single"
value={openStep}
onValueChange={(value) => setOpenStep(value as STEP_ID)}
>
<div className={clx(
"flex py-6 px-8",
{
"items-start": completed,
"items-center": !completed,
}
)}>



{!completed ? (
<>

Get started



{!!currentStep ? (
<>
{currentStep === steps[steps.length - 1].id ? (
<Button
variant="primary"
size="base"
onClick={() => onComplete()}
>
Complete Setup

) : (
<Button
variant="secondary"
size="base"
onClick={() => onHide()}
>
Cancel Setup

)}
</>
) : (
<>
<Button
variant="secondary"
size="base"
onClick={() => onHide()}
>
Close

<Button
variant="primary"
size="base"
onClick={() => onStart()}
>
Begin setup

</>
)}

</>
) : (
<>


Thank you for completing the setup guide!




<Button
variant="secondary"
size="base"
onClick={() => onHide()}
>
Close


</>
)}

{

{(!completed ? steps : steps.slice(-1)).map((step) => {
const isComplete = isStepComplete(step.id)
const isCurrent = currentStep === step.id
return (
<Accordion.Item
title="{step.title}"
value={step.id}
headingSize="medium"
active={isCurrent}
complete={isComplete}
disabled={!isComplete && !isCurrent}
key={step.id}
{...(!isComplete &&
!isCurrent && {
customTrigger: <></>,
})}
>

<step.component
onNext={step.onNext}
isComplete={isComplete}
data={data?.status}
{...props}
/>

</Accordion.Item>
)
})}

}


</>
)
}

export const config: WidgetConfig = {
zone: [
"product.list.before",
"product.details.before",
"order.list.before",
"order.details.before",
],
}

export default OnboardingFlow

Notice that you'll see errors related to components not being defined. You'll create these components in upcoming sections.

There are three important details to ensure that Medusa reads this file as a widget:

The file is placed under the src/admin/widget directory.
The file exports a config object of type WidgetConfig, which is imported from @medusajs/admin.
The file default exports a React component, which in this case is OnboardingFlow

The extension uses react-router-dom, which is available as a dependency of the @medusajs/admin package, to navigate to other pages in the dashboard.

The OnboardingFlow widget also implements functionalities related to handling the steps of the onboarding flow, including navigating between them and updating the current step in the backend.

To use the custom API Routes created in a previous step, you use the useAdminCustomQuery and useAdminCustomPost hooks from the medusa-react package. You can learn more about these hooks in the Medusa React documentation.

You can learn more about Admin Widgets in this documentation.