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 processesreq.scope.resolve(ContainerRegistrationKeys.QUERY)– Execute low-level graph queries against the data layerreq.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.tsexport HTTP verb functions (GET, POST, PUT, PATCH, DELETE) that receiveMedusaRequestandMedusaResponseobjects. - Resolve services via
req.scope.resolve()using keys likeModules.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
HttpTypesto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →