Medusa Examples

This documentation page has examples of customizations useful for your custom development in the Medusa application.

Each section links to the associated documentation page to learn more about it.

API Routes#

An API route is a REST API endpoint that exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems.

Create API Route#

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

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

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

Learn more in this documentation.

Resolve Resources in API Route#

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

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

This resolves the Product Module's main service.

Learn more in this documentation.

Use Path Parameters#

API routes can accept path parameters.

To do that, create the file src/api/hello-world/[id]/route.ts with the following content:

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

Learn more about path parameters in this documentation.

Use Query Parameters#

API routes can accept query parameters:

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

Learn more about query parameters in this documentation.

Use Body Parameters#

API routes can accept request body parameters:

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

Learn more about request body parameters in this documentation.

Set Response Code#

You can change the response code of an API route:

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

Learn more about setting the response code in this documentation.

Execute a Workflow in an API Route#

To execute a workflow in an API route:

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

Learn more in this documentation.

Change Response Content Type#

By default, an API route's response has the content type application/json.

To change it to another content type, use the writeHead method of MedusaResponse:

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

This changes the response type to return an event stream.

Learn more in this documentation.

Create Middleware#

A middleware is a function executed when a request is sent to an API Route.

Create the file src/api/middlewares.ts with the following content:

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

Learn more about middlewares in this documentation.

Restrict HTTP Methods in Middleware#

To restrict a middleware to an HTTP method:

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

Add Validation for Custom Routes#

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

Learn more about request body validation in this documentation.

Pass Additional Data to API Route#

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

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

Restrict an API Route to Admin Users#

You can protect API routes by restricting access to authenticated admin users only.

Add the following middleware in src/api/middlewares.ts:

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

Learn more in this documentation.

Restrict an API Route to Logged-In Customers#

You can protect API routes by restricting access to authenticated customers only.

Add the following middleware in src/api/middlewares.ts:

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

Learn more in this documentation.

Retrieve Logged-In Admin User#

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

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

Learn more in this documentation.

Retrieve Logged-In Customer#

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

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

Learn more in this documentation.

Throw Errors in API Route#

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

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

Learn more in this documentation.

Override Error Handler of API Routes#

To override the error handler of API routes, create the file src/api/middlewares.ts with the following content:

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

Learn more in this documentation,

Setting up CORS for Custom API Routes#

By default, Medusa configures CORS for all routes starting with /admin, /store, and /auth.

To configure CORS for routes under other prefixes, create the file src/api/middlewares.ts with the following content:

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

Parse Webhook Body#

By default, the Medusa application parses a request's body using JSON.

To parse a webhook's body, create the file src/api/middlewares.ts with the following content:

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

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

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

Modules#

A module is a package of reusable commerce or architectural functionalities. They handle business logic in a class called a service, and define and manage data models that represent tables in the database.

Create Module#

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

Module with Multiple Services#

To add services in your module other than the main one, create them in the services directory of the module.

For example, create the file src/modules/hello/services/custom.ts with the following content:

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

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

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

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

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

Learn more in this documentation.

Accept Module Options#

A module can accept options for configurations and secrets.

To accept options in your module:

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

Learn more in this documentation.

Integrate Third-Party System in Module#

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

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

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


Data Models#

A data model represents a table in the database. Medusa provides a data model language to intuitively create data models.

Create Data Model#

To create a data model in a module:

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

Learn more in this documentation.

Data Model Property Types#

A data model can have properties of the following types:

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

Learn more in this documentation.

Set Primary Key#

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

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

To set a text property as the primary key:

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

To set a number property as the primary key:

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

Learn more in this documentation.

Default Property Value#

To set the default value of a property:

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

Learn more in this documentation.

Nullable Property#

To allow null values for a property:

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

Learn more in this documentation.

Unique Property#

To create a unique index on a property:

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

Learn more in this documentation.

Define Database Index on Property#

To define a database index on a property:

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

Learn more in this documentation.

Define Composite Index on Data Model#

To define a composite index on a data model:

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

Learn more in this documentation.

Make a Property Searchable#

To make a property searchable using terms or keywords:

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

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

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

Learn more in this documentation.

Create One-to-One Relationship#

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

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

Learn more in this documentation.

Create One-to-Many Relationship#

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

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

Learn more in this documentation.

Create Many-to-Many Relationship#

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

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

Learn more in this documentation.

Configure Cascades of Data Model#

To configure cascade on a data model:

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

This configures the delete cascade on the Store data model so that, when a store is delete, its products are also deleted.

Learn more in this documentation.

Manage One-to-One Relationship#

Consider you have a one-to-one relationship between Email and User data models, where an email belongs to a user.

To set the ID of the user that an email belongs to:

NotehelloModuleService is the main service that the data models belong to.
Code