---
description: 'Learn how to create endpoints in Medusa. This guide also includes how to add CORS configurations, creating multiple endpoints, adding protected routes, and more.'
addHowToData: true
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# How to Create Endpoints

In this document, you’ll learn how to create endpoints in Medusa.

## Overview

Custom endpoints are created under the `src/api` directory in your Medusa Backend. They're defined in a TypeScript or JavaScript file named `index` (for example, `index.ts`). This file should export a function that returns an Express router or an array of routes and middlewares.

To consume the custom endpoints in your Medusa backend, you must transpile them with the `build` command before starting your backend.

---

## Basic Implementation

To create a new endpoint, start by creating a new file in `src/api` called `index.ts`. At its basic format, `index.ts` should look something like this:

```ts title=src/api/index.ts
import { Router } from "express"

export default (rootDirectory, options) => {
  const router = Router()

  router.get("/hello", (req, res) => {
    res.json({
      message: "Welcome to My Store!",
    })
  })

  return router
}
```

This exports a function that returns an Express router. The function receives two parameters:

- `rootDirectory` is the absolute path to the root directory that your backend is running from.
- `options` is an object that contains the configurations exported from `medusa-config.js`. If your API route is part of a plugin, then it will contain the plugin's options instead.

---

## Building Files

Custom endpoints 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:

```bash npm2yarn
npm run build
```

---

## Defining Multiple Routes or Middlewares

Instead of returning an Express router in the function, you can return an array of routes and [middlewares](./add-middleware.mdx).

For example:

```ts title=src/api/index.ts
import { Router } from "express"

export default (rootDirectory, options) => {
  const router = Router()

  router.get("/hello", (req, res) => {
    res.json({
      message: "Welcome to My Store!",
    })
  })

  // you can also define the middleware
  // in another file and import it
  const middleware = (res, req, next) => {
    // TODO define global middleware
    console.log("hello from middleware")
    next()
  }

  const anotherRouter = Router()
  anotherRouter.get("/store/*", (req, res, next) => {
    // TODO perform an actions for all store endpoints
    next()
  })

  return [middleware, router, anotherRouter]
}
```

This allows you to export multiple routers and middlewares from the same file. You can also import the routers, routes, and middlewares from other files, then import them in `src/api/index.ts` instead of defining them within the same file.

---

## Endpoint Path

Your endpoint can be under any path you wish.

By Medusa’s conventions:

- All Storefront REST APIs are prefixed by `/store`. For example, the `/store/products` endpoint lets you retrieve the products to display them on your storefront.
- All Admin REST APIs are prefixed by `/admin`. For example, the `/admin/products` endpoint lets you retrieve the products to display them on your Admin.

You can also create endpoints that don't reside under these two prefixes, similar to the `hello` endpoint in the previous example.

---

## CORS Configuration

If you’re adding a storefront or admin endpoint and you want to access these endpoints from the storefront or Medusa admin, you need to pass your endpoints Cross-Origin Resource Origin (CORS) options using the `cors` package.

First, import the necessary utility functions and types from Medusa's packages with the `cors` package:

```ts
import { 
  getConfigFile, 
  parseCorsOrigins,
} from "medusa-core-utils"
import { 
  ConfigModule, 
} from "@medusajs/medusa/dist/types/global"
import cors from "cors"
```

Next, in the exported function, retrieve the CORS configurations of your backend using the utility functions you imported:

```ts
export default (rootDirectory) => {
  // ...

  const { configModule } = 
    getConfigFile<ConfigModule>(rootDirectory, "medusa-config")
  const { projectConfig } = configModule

  // ....
}
```

Then, create an object that will hold the CORS configurations. Based on whether it's storefront or admin CORS options, you pass it the respective configuration from `projectConfig`:

<Tabs groupId="endpoint-type" isCodeTabs={true}>
<TabItem value="storefront" label="Storefront CORS" default>

```ts
const storeCorsOptions = {
  origin: projectConfig.store_cors.split(","),
  credentials: true,
}
```

</TabItem>
<TabItem value="admin" label="Admin CORS">

```ts
const adminCorsOptions = {
  origin: projectConfig.admin_cors.split(","),
  credentials: true,
}
```

</TabItem>
</Tabs>

Finally, you can either pass the `cors` middleware for a specific route, or pass it to the entire router:

<Tabs groupId="pass-type" isCodeTabs={true}>
<TabItem value="single" label="Pass to Endpoint" default>

```ts
adminRouter.options("/admin/hello", cors(adminCorsOptions))
adminRouter.get(
  "/admin/hello",
  cors(adminCorsOptions),
  (req, res) => {
    // ...
  }
)
```

</TabItem>
<TabItem value="router" label="Pass to Router">

```ts
adminRouter.use(cors(adminCorsOptions))
```

</TabItem>
</Tabs>

---

## Parse Request Body Parameters

If you want to accept request body parameters, you need to pass express middlewares that parse the payload type to your router.

For example:

```ts title=src/api/index.ts
import bodyParser from "body-parser"
import express, { Router } from "express"


export default (rootDirectory, pluginOptions) => {
  const router = Router()

  router.use(express.json())
  router.use(express.urlencoded({ extended: true }))

  router.post("/store/hello", (req, res) => {
    res.json({
      message: req.body.name,
    })
  })

  return router
}
```

In the code snippet above, you use the following middlewares:

- `express.json()`: parses requests with JSON payloads
- `express.urlencoded()`: parses requests with urlencoded payloads.

You can learn about other available middlewares in the [Express documentation](https://expressjs.com/en/api.html#express).

---

## Protected Routes

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

### Protect Store Routes

There are two approaches to make a storefront route protected:

- Using the `requireCustomerAuthentication` middleware, which disallows unauthenticated customers from accessing a route, and allows you to access the logged-in customer's ID.
- Using the `authenticateCustomer` middleware, which allows both authenticated and unauthenticated customers to access your route, but allows you to access the logged-in customer's ID as well.

To make a storefront route protected using either middlewares, first, import the middleware at the top of your file:

<!-- eslint-disable max-len -->

```ts
import { requireCustomerAuthentication } from "@medusajs/medusa"
// import { authenticateCustomer } from "@medusajs/medusa"
```

Then, pass the middleware to either a single route or an entire router:

<Tabs groupId="pass-type" isCodeTabs={true}>
<TabItem value="single" label="Pass to Endpoint" default>

```ts
// only necessary if you're passing cors options per route
router.options("/store/hello", cors(storeCorsOptions))
router.get(
  "/store/hello",
  cors(storeCorsOptions),
  requireCustomerAuthentication(), 
  // authenticateCustomer()
  async (req, res) => {
    // access current customer
    const id = req.user.customer_id
    // if you're using authenticateCustomer middleware
    // check if id is set first

    const customerService = req.scope.resolve("customerService")
      
    const customer = await customerService.retrieve(id)
    // ...
  }
)
```

</TabItem>
<TabItem value="router" label="Pass to Router">

```ts
storeRouter.use(requireCustomerAuthentication())
// all routes added to storeRouter are now protected
// the logged in customer can be accessed using:
// req.user.customer_id

// storeRouter.use(authenticateCustomer())
```

</TabItem>
</Tabs>

### Protect Admin Routes

To protect admin routes and only allow logged-in users from accessing them, first, import the `authenticate` middleware at the top of the file:

<!-- eslint-disable max-len -->

```ts
import { authenticate } from "@medusajs/medusa"
```

Then, pass the middleware to either a single route or an entire router:

<Tabs groupId="pass-type" isCodeTabs={true}>
<TabItem value="single" label="Pass to Endpoint" default>

```ts
// only necessary if you're passing cors options per route
adminRouter.options("/admin/hello", cors(adminCorsOptions))
adminRouter.get(
  "/admin/hello", 
  cors(adminCorsOptions), 
  authenticate(),
  async (req, res) => {
    // access current user
    const id = req.user.userId
    const userService = req.scope.resolve("userService")
      
    const user = await userService.retrieve(id)
    // ...
  }
)
```

</TabItem>
<TabItem value="router" label="Pass to Router">

```ts
adminRouter.use(authenticate())
// all routes added to adminRouter are now protected
// the logged in user can be accessed using:
// req.user.userId
```

</TabItem>
</Tabs>

---

## Retrieve Medusa Config

As mentioned, the second parameter `options` includes the configurations exported from `medusa-config.js`. However, in plugins it only includes the plugin's options.

If you need to access the Medusa configuration in your endpoint, you can use the `getConfigFile` method imported from `medusa-core-utils`. It accepts the following parameters:

1. `rootDirectory`: The first parameter is a string indicating root directory of your Medusa backend.
2. `config`: The second parameter is a string indicating the name of the config file, which should be `medusa-config` unless you change it.

The function returns an object with the following properties:

1. `configModule`: An object containing the configurations exported from `medusa-config.js`.
2. `configFilePath`: A string indicating absolute path to the configuration file.
3. `error`: if any errors occur, they'll be included as the value of this property. Otherwise, its value will be `undefined`.

Here's an example of retrieving the configurations within an endpoint using `getConfigFile`:

```ts title=src/api/index.ts
import { Router } from "express"
import { ConfigModule } from "@medusajs/medusa"
import { getConfigFile } from "medusa-core-utils"

export default (rootDirectory) => {
  const router = Router()
  const { configModule } = getConfigFile<ConfigModule>(
    rootDirectory,
    "medusa-config"
  )

  router.get("/store-cors", (req, res) => {
    res.json({
      store_cors: configModule.projectConfig.store_cors,
    })
  })

  return router
}
```

Notice that `getConfigFile` is a generic function. So, if you're using TypeScript, you should pass it the type `ConfigModule` imported from `@medusajs/medusa`.

If you're accessing custom configurations, you'll need to create a new type that defines these configurations. For example:

```ts title=src/api/index.ts
import { Router } from "express"
import { ConfigModule } from "@medusajs/medusa"
import { getConfigFile } from "medusa-core-utils"

type MyConfigModule = ConfigModule & {
  projectConfig: {
    custom_config?: string
  }
}

export default (rootDirectory) => {
  const router = Router()
  const { configModule } = getConfigFile<MyConfigModule>(
    rootDirectory,
    "medusa-config"
  )

  router.get("/hello", (req, res) => {
    res.json({
      custom_config: configModule.projectConfig.custom_config,
    })
  })

  return router
}
```

---

## Handle Errors

As Medusa uses v4 of Express, you need to manually handle errors thrown asynchronously as explained in [Express's documentation](https://expressjs.com/en/guide/error-handling.html).

You can use [middlewares](./add-middleware.mdx) to handle errors. You can also use middlewares defined by Medusa, which ensure that your error handling is consistent across your Medusa backend.

:::note

Code snippets are taken from the [full example available at the end of this document](#example-crud-endpoints).

:::

To handle errors using Medusa's middlewares, first, import the `errorHandler` middleware from `@medusajs/medusa` and apply it on your routers. Make sure it's applied after all other middlewares and routes:

```ts title=src/api/index.ts
import express, { Router } from "express"
import adminRoutes from "./admin"
import storeRoutes from "./store"
import { errorHandler } from "@medusajs/medusa"

export default (rootDirectory, options) => {
  const router = Router()

  router.use(express.json())
  router.use(express.urlencoded({ extended: true }))

  adminRoutes(router, options)
  storeRoutes(router, options)

  router.use(errorHandler())

  return router
}
```

Then, wrap the function handler of every route with the `wrapHandler` middleware imported from `@medusajs/medusa`. For example:

```ts title=src/api/admin.ts
import { wrapHandler } from "@medusajs/medusa"

// ...

export default function adminRoutes(
  router: Router,
  options: ConfigModule
) {
  // ...

  adminRouter.get("/posts", wrapHandler(async (req, res) => {
    const postService: PostService = 
      req.scope.resolve("postService")

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

Alternatively, you can define the endpoints in different files, and import and use them in your router:

<!-- eslint-disable @typescript-eslint/no-var-requires -->

```ts title=src/api/admin.ts
import { wrapHandler } from "@medusajs/medusa"

// ...

export default function adminRoutes(
  router: Router,
  options: ConfigModule
) {
  // ...
  
  adminRouter.get(
    "/posts",
    wrapHandler(require("./list-posts").default)
  )
  
  // ...
}
```

Now all errors thrown in your custom endpoints, including in their custom services, will be caught and returned to the user.

However, if you throw errors like this:

```ts
throw new Error ("Post was not found")
```

You'll notice that the endpoint returns the following object error in the response:

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

To ensure the error message is relayed in the response, it's recommended to use `MedusaError` imported from `@medusajs/utils` as the thrown error instead:

```ts
import { MedusaError } from "@medusajs/utils"

// ...

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

The constructor of `MedusaError` accepts the following parameters:

1. The first parameter is the type of the error. 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.
2. The second parameter is the message of the error.
3. The third parameter is an optional code, which is a string, that can be returned in the error object.

After using `MedusaError`, you'll notice that the returned error in the response provides a clearer message:

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

---

## Use Other Resources

### Entities and Repositories

Your endpoints likely perform an action on an entity. For example, you may create an endpoint to retrieve a list of posts.

You can perform actions on an entity either through its [Repository](../entities/overview.mdx#what-are-repositories) or through a [service](#services). This section covers how to retrieve a repository in an endpoint, but it's recommended to use services instead.

You can retrieve any registered resource, including repositories, in your endpoint using `req.scope.resolve` passing it the resource's registration name in the [dependency container](../fundamentals/dependency-injection.md). Repositories are registered as their camel-case name. So, for example, if you have a `PostRepository`, it's registered as `postRepository`.

Here’s an example of an endpoint that retrieves the list of posts in a store:

:::note

Posts are represented by a custom entity not covered in this guide. You can refer to the [entities](../entities/create.mdx#adding-relations) for more details on how to create a custom entity.

:::

```ts
import { PostRepository } from "../repositories/post"
import { EntityManager } from "typeorm"

// ...

export default () => {
  // ...

  storeRouter.get("/posts", async (req, res) => {
    const postRepository: typeof PostRepository = 
      req.scope.resolve("postRepository")
    const manager: EntityManager = req.scope.resolve("manager")
    const postRepo = manager.withRepository(postRepository)

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

  // ...
}
```

Notice that to retrieve an instance of the repository, you need to retrieve first Typeorm's Entity manager and use its `withRepository` method.

### Services

Services in Medusa bundle a set of functionalities into one class. Typically, these functionalities are associated with an entity, such as methods to retrieve, create, or update its records.

You can retrieve any registered resource, including services, in your endpoint using `req.scope.resolve` passing it the service’s registration name  in the [dependency container](../fundamentals/dependency-injection.md). Services are registered as their camel-case name. So, for example, if you have a `PostService`, it's registered as `postService`.

Here’s an example of an endpoint that retrieves the list of posts in a store:

:::note

`PostService` is a custom service that is not covered in this guide. You can refer to the [services](../services/create-service.mdx) documentation for more details on how to create a custom service, and find an [example of PostService](../services/create-service.mdx#example-services-with-crud-operations)

:::

```ts
storeRouter.get("/posts", async (req, res) => {
  const postService: PostService = req.scope.resolve(
    "postService"
  )

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

### Other Resources

Any resource that is registered in the dependency container, such as strategies or file services, can be accessed through `req.scope.resolve`. Refer to the [dependency injection](../fundamentals/dependency-injection.md) documentation for details on registered resources.

---

## Example: CRUD Endpoints

This section services as an example of creating endpoints that perform Create, Read, Update, and Delete (CRUD) operations. Note that all admin endpoints are placed in `src/api/admin.ts`, and store endpoints are placed in `src/api/store.ts`. You can also place each endpoint in a separate file, import it, and add it to its respective router.

You can refer to the [Entities](../entities/create.mdx#adding-relations) and [Services](../services/create-service.mdx#example-services-with-crud-operations) documentation to learn how to create the custom entities and services used in this example.

<Tabs groupId="files" isCodeTabs={true}>
<TabItem value="index" label="src/api/index.ts" default>

```ts
import express, { Router } from "express"
import adminRoutes from "./admin"
import storeRoutes from "./store"
import { errorHandler } from "@medusajs/medusa"

export default (rootDirectory, options) => {
  const router = Router()

  router.use(express.json())
  router.use(express.urlencoded({ extended: true }))

  adminRoutes(router, options)
  storeRoutes(router, options)

  router.use(errorHandler())

  return router
}
```

</TabItem>
<TabItem value="admin" label="src/api/admin.ts">

```ts
import { Router } from "express"
import PostService from "../services/post"
import { 
  ConfigModule, 
} from "@medusajs/medusa/dist/types/global"
import cors from "cors"
import { authenticate, wrapHandler } from "@medusajs/medusa"
import AuthorService from "../services/author"

export default function adminRoutes(
  router: Router,
  options: ConfigModule
) {
  const { projectConfig } = options

  const corsOptions = {
    origin: projectConfig.admin_cors.split(","),
    credentials: true,
  }

  const adminRouter = Router()

  router.use("/admin/blog", adminRouter)

  adminRouter.use(cors(corsOptions))
  adminRouter.use(authenticate())

  // it's recommended to define the routes
  // in separate files. They're done in
  // the same file here for simplicity


  // list all blog posts
  adminRouter.get(
    "/posts",
    wrapHandler(async (req, res) => {
      const postService: PostService = req.scope.resolve(
        "postService"
      )

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


  // retrieve a single blog post
  adminRouter.get(
    "/posts/:id",
    wrapHandler(async (req, res) => {
      const postService: PostService = req.scope.resolve(
        "postService"
      )

      const post = await postService.retrieve(req.params.id)

      res.json({
        post,
      })
  }))

  // create a blog post
  adminRouter.post(
    "/posts",
    wrapHandler(async (req, res) => {
      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,
      })
  }))

  // update a blog post
  adminRouter.post(
    "/posts/:id",
    wrapHandler(async (req, res) => {
      const postService: PostService = req.scope.resolve(
        "postService"
      )

      // basic validation of request body
      if (req.body.id) {
        throw new Error("Can't update post ID")
      }

      const post = await postService.update(
        req.params.id,
        req.body
      )

      res.json({
        post,
      })
  }))

  // delete a blog post
  adminRouter.delete(
    "/posts/:id",
    wrapHandler(async (req, res) => {
      const postService: PostService = req.scope.resolve(
        "postService"
      )

      await postService.delete(req.params.id)

      res.status(200).end()
  }))

  // list all blog authors
  adminRouter.get(
    "/authors", 
    wrapHandler(async (req, res) => {
      const authorService: AuthorService = req.scope.resolve(
        "authorService"
      )

      res.json({
        authors: await authorService.list(),
      })
  }))

  // retrieve a single blog author
  adminRouter.get(
    "/authors/:id",
    wrapHandler(async (req, res) => {
      const authorService: AuthorService = req.scope.resolve(
        "authorService"
      )

      res.json({
        post: await authorService.retrieve(req.params.id),
      })
  }))

  // create a blog author
  adminRouter.post(
    "/authors",
    wrapHandler(async (req, res) => {
      const authorService: AuthorService = req.scope.resolve(
        "authorService"
      )

      // basic validation of request body
      if (!req.body.name) {
        throw new Error("`name` is required.")
      }

      const author = await authorService.create(req.body)

      res.json({
        author,
      })
  }))

  // update a blog author
  adminRouter.post(
    "/authors/:id",
    wrapHandler(async (req, res) => {
      const authorService: AuthorService = req.scope.resolve(
        "authorService"
      )

      // basic validation of request body
      if (req.body.id) {
        throw new Error("Can't update author ID")
      }

      const author = await authorService.update(
        req.params.id,
        req.body
      )

      res.json({
        author,
      })
  }))

  // delete a blog author
  adminRouter.delete(
    "/authors/:id",
    wrapHandler(async (req, res) => {
      const authorService: AuthorService = req.scope.resolve(
        "authorService"
      )

      await authorService.delete(req.params.id)

      res.status(200).end()
  }))
}
```

</TabItem>
<TabItem value="store" label="src/api/store.ts">

```ts
import { Router } from "express"
import { 
  ConfigModule, 
} from "@medusajs/medusa/dist/types/global"
import PostService from "../services/post"
import cors from "cors"
import AuthorService from "../services/author"
import { wrapHandler } from "@medusajs/medusa"

export default function storeRoutes(
  router: Router,
  options: ConfigModule
) {
  const { projectConfig } = options

  const storeCorsOptions = {
    origin: projectConfig.store_cors.split(","),
    credentials: true,
  }

  const storeRouter = Router()
  router.use("/store/blog", storeRouter)

  storeRouter.use(cors(storeCorsOptions))
  
  // list all blog posts
  storeRouter.get(
    "/posts",
    wrapHandler(async (req, res) => {
      const postService: PostService = req.scope.resolve(
        "postService"
      )

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

  // retrieve a single blog post
  storeRouter.get(
    "/posts/:id",
    wrapHandler(async (req, res) => {
      const postService: PostService = req.scope.resolve(
        "postService"
      )

      res.json({
        post: await postService.retrieve(req.params.id),
      })
  }))

  // list all blog authors
  storeRouter.get(
    "/authors",
    wrapHandler(async (req, res) => {
      const authorService: AuthorService = req.scope.resolve(
        "authorService"
      )

      res.json({
        authors: await authorService.list(),
      })
  }))

  // retrieve a single blog author
  storeRouter.get(
    "/authors/:id",
    wrapHandler(async (req, res) => {
      const authorService: AuthorService = req.scope.resolve(
        "authorService"
      )

      res.json({
        post: await authorService.retrieve(req.params.id),
      })
  }))
}
```

</TabItem>
</Tabs>

---

## See Also

- [Storefront API Reference](https://docs.medusajs.com/api/store)
- [Admin API Reference](https://docs.medusajs.com/api/admin)
