Skip to main content
API routes in Medusa are file-based HTTP endpoints that follow Next.js-style conventions. They automatically integrate with authentication, validation, and the dependency injection container.

Route Basics

Routes are defined in the src/api directory with a specific file structure:
src/api/
├── admin/              # Admin API routes (requires authentication)
│   └── brands/
│       ├── route.ts    # /admin/brands
│       └── [id]/
│           └── route.ts # /admin/brands/:id
└── store/              # Store API routes (public or customer auth)
    └── brands/
        └── route.ts

Creating a Basic Route

1

Create the Route File

Create a route.ts file with named exports for HTTP methods:
src/api/admin/brands/route.ts
import {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export async function GET(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  res.json({
    brands: [],
  })
}

export async function POST(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  res.json({
    brand: { id: "brand_123" },
  })
}
Available HTTP methods: GET, POST, PUT, PATCH, DELETE
2

Use Dependency Injection

Access services and modules via req.scope:
import { BRAND_MODULE } from "../../../modules/brand"
import { IBrandModuleService } from "../../../modules/brand/types"

export async function GET(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  const brandService = req.scope.resolve<IBrandModuleService>(
    BRAND_MODULE
  )

  const brands = await brandService.listBrands()

  res.json({ brands })
}
3

Execute Workflows

Most routes should execute workflows instead of calling services directly:
import { createBrandWorkflow } from "../../../workflows/brand/create-brand"

export async function POST(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  const { result: brand } = await createBrandWorkflow(req.scope).run({
    input: req.body,
  })

  res.status(201).json({ brand })
}

Complete CRUD Example

Here’s a full example implementing CRUD operations:

List Brands

src/api/admin/brands/route.ts
import {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export async function GET(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: brands, metadata } = await query.graph({
    entity: "brand",
    fields: req.queryConfig.fields,
    filters: req.filterableFields,
    pagination: req.queryConfig.pagination,
  })

  res.json({
    brands,
    count: metadata.count,
    offset: metadata.skip,
    limit: metadata.take,
  })
}

Create Brand

src/api/admin/brands/route.ts
import { createBrandWorkflow } from "../../../workflows/brand/create-brand"

export async function POST(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  const { result: brand } = await createBrandWorkflow(req.scope).run({
    input: {
      name: req.validatedBody.name,
      description: req.validatedBody.description,
    },
  })

  res.status(201).json({ brand })
}

Get Single Brand

src/api/admin/brands/[id]/route.ts
import {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export async function GET(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: brands } = await query.graph({
    entity: "brand",
    filters: { id: req.params.id },
    fields: req.queryConfig.fields,
  })

  if (!brands.length) {
    return res.status(404).json({
      message: `Brand with id ${req.params.id} not found`,
    })
  }

  res.json({ brand: brands[0] })
}

Update Brand

src/api/admin/brands/[id]/route.ts
import { updateBrandWorkflow } from "../../../../workflows/brand/update-brand"

export async function POST(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  const { result: brand } = await updateBrandWorkflow(req.scope).run({
    input: {
      id: req.params.id,
      ...req.validatedBody,
    },
  })

  res.json({ brand })
}

Delete Brand

src/api/admin/brands/[id]/route.ts
import { deleteBrandWorkflow } from "../../../../workflows/brand/delete-brand"

export async function DELETE(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  await deleteBrandWorkflow(req.scope).run({
    input: { ids: [req.params.id] },
  })

  res.json({
    id: req.params.id,
    object: "brand",
    deleted: true,
  })
}

Request Types

AuthenticatedMedusaRequest

Use for admin routes that require authentication:
import { AuthenticatedMedusaRequest } from "@medusajs/framework/http"

export async function GET(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  // Access authenticated user
  const userId = req.auth_context.actor_id
  
  // User is guaranteed to be authenticated
}

MedusaRequest

Use for public routes that don’t require authentication:
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  // No authentication required
}

Type Parameters

Add type parameters for request body and query params:
import { AuthenticatedMedusaRequest } from "@medusajs/framework/http"
import type { HttpTypes } from "@medusajs/framework/types"

interface CreateBrandBody {
  name: string
  description?: string
}

interface BrandQueryParams {
  fields?: string
}

export async function POST(
  req: AuthenticatedMedusaRequest<CreateBrandBody, BrandQueryParams>,
  res: MedusaResponse<HttpTypes.AdminBrandResponse>
) {
  const { name, description } = req.validatedBody
  // Type-safe access to body
}

Request Properties

req.scope

Dependency injection container:
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const logger = req.scope.resolve(ContainerRegistrationKeys.LOGGER)
const brandService = req.scope.resolve(BRAND_MODULE)

req.params

URL path parameters:
// Route: /admin/brands/[id]/route.ts
// URL: /admin/brands/brand_123
const brandId = req.params.id // "brand_123"

req.validatedBody

Validated request body (when using validators):
const { name, description } = req.validatedBody

req.validatedQuery

Validated query parameters:
const { limit, offset } = req.validatedQuery

req.filterableFields

Filters for querying:
const brands = await query.graph({
  entity: "brand",
  filters: req.filterableFields,
})

req.queryConfig

Query configuration:
const { fields, pagination } = req.queryConfig

req.auth_context

Authentication context (on AuthenticatedMedusaRequest):
const userId = req.auth_context.actor_id
const authType = req.auth_context.auth_identity_id

Response Methods

res.json()

Send JSON response:
res.json({ brand: { id: "brand_123" } })

res.status()

Set status code:
res.status(201).json({ brand })
res.status(404).json({ message: "Not found" })
res.status(204).send()

Using Query for Data Fetching

The Query service provides a powerful way to fetch related data:
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export async function GET(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: brands } = await query.graph({
    entity: "brand",
    fields: [
      "id",
      "name",
      "description",
      "products.*",
      "products.variants.*",
    ],
    filters: {
      name: { $like: "%Nike%" },
    },
    pagination: {
      skip: 0,
      take: 20,
    },
  })

  res.json({ brands })
}

Error Handling

Use standard HTTP status codes and error responses:
import { MedusaError } from "@medusajs/framework/utils"

export async function GET(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: brands } = await query.graph({
    entity: "brand",
    filters: { id: req.params.id },
  })

  if (!brands.length) {
    throw new MedusaError(
      MedusaError.Types.NOT_FOUND,
      `Brand with id ${req.params.id} was not found`
    )
  }

  res.json({ brand: brands[0] })
}

Best Practices

  • Use AuthenticatedMedusaRequest for admin routes requiring authentication
  • Execute workflows instead of calling services directly
  • Use the Query service for complex data fetching
  • Type your request and response for type safety
  • Return appropriate HTTP status codes (200, 201, 204, 404, etc.)
  • Use MedusaError for consistent error handling
  • Keep route handlers thin - business logic belongs in workflows
  • Access services via req.scope.resolve()

Next Steps

Create Workflows

Build business logic for your routes

Create Services

Implement module services

Build docs developers (and LLMs) love