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

> Learn to implement custom logic within Medusa API routes by exporting HTTP verb functions and resolving services. Master custom TypeScript processing for typed responses.

- Repository: [Medusa/medusa](https://github.com/medusajs/medusa)
- Tags: how-to-guide
- Published: 2026-05-19

---

**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`](https://github.com/medusajs/medusa/blob/main/packages/medusa/src/api/admin/orders/route.ts), a standard GET handler looks like this:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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()`:

```typescript
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.