Skip to main content
Skip to main content

How to Create an API Route

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

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 run 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 [<PARAMETER_NAME>], where <PARAMETER_NAME> 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

CORS configurations are automatically added to custom API Routes defined under the /store or /admin path prefixes based on the store_cors and admin_cors configurations respectively.

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,
}),
],
},
],
}

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.


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>(
"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

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>(
"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

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<MyConfigModule>(
"configModule"
)
res.json({
message: configModule.projectConfig.custom_config,
})
}

Handle Errors

Medusa provides an errorHandler middleware that you can use on your custom API Routes so that your error handling is consistent with the Medusa backend. You can also create custom middlewares to handle errors.

To handle errors using Medusa's middlewares, first, import the errorHandler middleware from @medusajs/medusa and apply it on your routes.

For example:

src/api/middlewares.ts
import { 
errorHandler,
type MiddlewaresConfig,
} from "@medusajs/medusa"

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

Make sure it's applied after all other middlewares.

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

src/api/store/custom/route.ts
import { 
MedusaRequest,
MedusaResponse,
wrapHandler,
} from "@medusajs/medusa"

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

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

Now all errors thrown in your custom API Routes, or in resources you use within your API Route such as services, are caught and returned to the user.

Using MedusaError

If you throw errors 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, it's recommended to use MedusaError imported from @medusajs/utils as the thrown error 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:

  1. 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.
  2. The second parameter is the message of the error.
  3. 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"
}

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

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>("postRepository")
const manager = req.scope.resolve<EntityManager>("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

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:

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.

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

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,
})
}

See Also

Was this section helpful?