How to Implement Custom Logic Within Medusa API Routes: A Complete Guide

To implement custom logic within Medusa API routes, export HTTP verb functions (GET, POST, PUT, PATCH, DELETE) from route files under api/**/route.ts that accept MedusaRequest and MedusaResponse objects, then resolve services from req.scope, run workflows, or insert custom TypeScript processing before returning typed responses.

Medusa's HTTP API architecture follows a route-handler pattern that enables developers to implement custom logic within Medusa API routes while maintaining full integration with the framework's dependency injection and workflow systems. Each file under packages/medusa/src/api/**/route.ts exports functions named after standard HTTP verbs that receive type-safe request and response objects. This approach allows you to leverage the request scope, workflow engine, and query system to extend core functionality with custom business logic, validation, and response transformations.

Understanding the Route Handler Pattern

Medusa routes are defined by exporting named functions corresponding to HTTP methods from TypeScript files located in the api directory. When a request hits an endpoint, the framework instantiates a MedusaRequest (or AuthenticatedMedusaRequest for protected admin routes) and a MedusaResponse, passing both to your exported handler.

According to the source code in packages/medusa/src/api/admin/orders/route.ts, a standard GET handler looks like this:

export const GET = async (
  req: AuthenticatedMedusaRequest<HttpTypes.AdminOrderListParams>,
  res: MedusaResponse<HttpTypes.AdminOrderListResponse>
) => {
  // Custom logic implementation
}

The handler receives the DI container through req.scope, enabling access to any registered service, workflow, or utility within the Medusa framework.

Resolving Services from the Request Scope

The req.scope object provides access to Medusa's dependency injection container. You resolve services and utilities using string keys or class references to inject functionality into your route handlers.

Common resolution patterns include:

  • req.scope.resolve(Modules.WORKFLOW_ENGINE) – Access the workflow engine to run complex business processes
  • req.scope.resolve(ContainerRegistrationKeys.QUERY) – Execute low-level graph queries against the data layer
  • req.scope.resolve(CustomService) – Inject domain-specific services for custom operations

As implemented in packages/medusa/src/api/store/carts/[id]/complete/route.ts, you can resolve multiple utilities within a single handler:

const we = req.scope.resolve(Modules.WORKFLOW_ENGINE)
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

Leveraging Workflows for Core Operations

Most core business actions in Medusa are encapsulated in workflows that provide automatic transaction handling, compensation logic, and event emission. When you implement custom logic within Medusa API routes, you can invoke these workflows and extend their results.

The completeCartWorkflow, getOrdersListWorkflow, and createProductWorkflow are examples of reusable workflows available in @medusajs/core-flows.

To execute a workflow and handle its results:

const { result } = await getOrdersListWorkflow(req.scope).run({
  input: { 
    fields: req.queryConfig.fields, 
    variables 
  },
})

Workflows return a result object that you can validate, transform, or enrich with additional data before sending the response.

Implementing Custom Business Logic

After resolving dependencies and running workflows, you can insert arbitrary TypeScript code to handle validation, transformation, or conditional branching. The route handler acts as a standard Express-style function with full access to the request body, parameters, and query strings.

Pre-Validation and Data Inspection

Inspect req.body before workflow execution to enforce business rules. Throw MedusaError to return standardized error responses:

if (!req.body.title?.trim()) {
  throw new MedusaError(
    MedusaError.Types.INVALID_DATA,
    "Product title cannot be empty"
  )
}

Post-Processing Workflow Results

Transform workflow outputs or enrich them with additional queries. The example from packages/medusa/src/api/store/carts/[id]/complete/route.ts demonstrates handling workflow errors with custom logic:

if (errors?.[0]) {
  const error = errors[0].error
  // custom handling of payment-related errors
  if (!statusOKErrors.includes(error?.type)) {
    throw error
  }
  res.status(200).json({ type: "cart", cart, error: { ... } })
  return
}

Conditional Branching

Route handlers can decide which workflow to execute based on request parameters or custom service results, enabling dynamic business logic without modifying core framework files.

Querying Data with the Graph API

When existing workflows do not expose the exact data you need, resolve the query helper to execute direct reads against Medusa's graph API:

const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

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

This pattern is useful for fetching related entities or fetching data after workflow execution to construct complex response shapes.

Structuring Custom Admin Routes

For admin-side customization, use AuthenticatedMedusaRequest to ensure the route requires valid authentication. Combine validation, service resolution, and workflow execution to extend product creation or order management:

import {
  ContainerRegistrationKeys,
  MedusaError,
  Modules,
} from "@medusajs/framework/utils"
import {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { HttpTypes } from "@medusajs/framework/types"
import { createProductWorkflow } from "@medusajs/core-flows"

export const POST = async (
  req: AuthenticatedMedusaRequest<HttpTypes.AdminCreateProduct>,
  res: MedusaResponse<HttpTypes.AdminProductResponse>
) => {
  // Custom validation
  if (!req.body.title?.trim()) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Product title cannot be empty"
    )
  }

  // Custom service usage
  const taxService = req.scope.resolve(Modules.TAX)
  const taxRate = await taxService.calculateTaxRate(req.body)

  // Run standard workflow
  const { result } = await createProductWorkflow(req.scope).run({
    input: { ...req.body, tax_rate: taxRate },
  })

  // Custom response shaping
  res.status(201).json({
    product: result,
    message: "Product created with custom tax calculation",
  })
}

Extending Store Routes with Enrichment

Storefront routes use MedusaRequest and often benefit from enriching standard workflow results with additional contextual data:

import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { HttpTypes } from "@medusajs/framework/types"
import { getCartWorkflow } from "@medusajs/core-flows"

export const GET = async (
  req: MedusaRequest<HttpTypes.StoreCartParams>,
  res: MedusaResponse<HttpTypes.StoreCartResponse>
) => {
  const { result } = await getCartWorkflow(req.scope).run({
    input: { cart_id: req.params.id },
  })

  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  // Custom logic: fetch active promotions
  const { data: promotions } = await query.graph({
    entity: "promotion",
    fields: ["id", "code", "type"],
    filters: { cart_id: result.id },
  })

  res.json({
    cart: result,
    active_promotions: promotions,
  })
}

Adding Middleware for Reusable Concerns

Place cross-cutting concerns like authentication, inventory validation, or rate limiting in middleware files located in src/api/**/middlewares/*.ts. Reference these middleware in your route configuration to keep handlers focused on core business logic while maintaining clean separation of concerns.

Returning Typed Responses

Use HttpTypes definitions to guarantee JSON payload shapes and enable IDE autocomplete. Cast workflow results or query data to the appropriate response types before calling res.json():

res.json({
  orders: rows as unknown as HttpTypes.AdminOrder[],
  count: metadata.count,
  offset: metadata.skip,
  limit: metadata.take,
})

Summary

  • Route files under packages/medusa/src/api/**/route.ts export HTTP verb functions (GET, POST, PUT, PATCH, DELETE) that receive MedusaRequest and MedusaResponse objects.
  • Resolve services via req.scope.resolve() using keys like Modules.WORKFLOW_ENGINE, ContainerRegistrationKeys.QUERY, or custom service classes.
  • Execute workflows using the workflow engine for transaction-safe operations, then apply custom validation or transformation to the results.
  • Query data directly using the graph API helper when workflows do not provide sufficient data granularity.
  • Use type definitions from HttpTypes to ensure response consistency and catch errors at compile time.
  • Implement middleware in dedicated files for authentication and validation logic that applies across multiple routes.

Frequently Asked Questions

How do I access services in a Medusa API route?

Access services through the request scope's dependency injection container using req.scope.resolve(). Pass the service identifier, such as Modules.TAX, Modules.WORKFLOW_ENGINE, or ContainerRegistrationKeys.QUERY, to retrieve the instantiated service. This pattern is used throughout packages/medusa/src/api/store/carts/[id]/complete/route.ts to resolve the workflow engine and query helpers.

Can I modify workflow results before returning them to the client?

Yes. After executing a workflow with .run(), capture the result object and apply any custom transformations, filtering, or enrichment before passing the data to res.json(). You can also query additional data using req.scope.resolve(ContainerRegistrationKeys.QUERY) to supplement workflow outputs with related entities.

What is the difference between MedusaRequest and AuthenticatedMedusaRequest?

MedusaRequest is used for public storefront routes that do not require authentication, while AuthenticatedMedusaRequest extends the base interface with authentication context for admin routes. Use AuthenticatedMedusaRequest in admin API routes (located in api/admin/**) to ensure the user is logged in and has appropriate permissions.

How do I handle errors in custom route logic?

Throw MedusaError with specific error types (such as MedusaError.Types.INVALID_DATA or MedusaError.Types.NOT_FOUND) to return standardized HTTP error responses. The framework catches these errors and formats them appropriately. For workflow-specific errors, inspect the errors array returned by the workflow execution and implement conditional logic to either re-throw or handle gracefully as shown in the cart completion route.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →