Medusa API Route Conventions: File-Based Routing and HTTP Handler Patterns
Medusa API route conventions use a file-based system where each endpoint is defined in a route.ts file under packages/medusa/src/api/<scope>/<resource>/, exporting HTTP verbs as uppercase constants like GET and POST with typed AuthenticatedMedusaRequest and MedusaResponse objects, while delegating business logic to workflows via dependency injection.
The medusajs/medusa repository implements a strict, framework-level convention for building HTTP endpoints that separates routing concerns from business logic. Understanding these Medusa API route conventions is essential for extending the platform, as every store, admin, and plugin interface follows the same file-based structure and handler signature patterns established in the core codebase.
File-Based Route Structure and Naming Conventions
Medusa uses a filesystem-based router where the directory path determines the final URL. Routes are organized under packages/medusa/src/api/ into three primary scopes: store, admin, and plugin-specific namespaces.
Route Files and Dynamic Segments
Each resource endpoint resides in a route.ts file. For endpoints requiring identifiers, use the [id]/route.ts pattern, which maps to /:id in the URL structure.
- Store products list:
packages/medusa/src/api/store/products/route.ts→/store/products - Admin product by ID:
packages/medusa/src/api/admin/products/[id]/route.ts→/admin/products/:id - Nested calculations:
packages/medusa/src/api/store/shipping-options/[id]/calculate/route.ts→/store/shipping-options/:id/calculate
Exported HTTP Handlers
Within each route.ts file, HTTP verbs are exported as uppercase constants. The handler signature always follows (req, res) => Promise<void>.
Supported exports include:
GETfor retrieval operationsPOSTfor resource creationPUTandPATCHfor updatesDELETEfor removals
Typed Request and Response Objects
Type safety is enforced through generic request and response interfaces that vary by authentication scope.
Store vs Admin Authentication
Store routes use MedusaRequest<T>, while admin routes require AuthenticatedMedusaRequest<T> to ensure proper authorization checks. Responses are typed with MedusaResponse<T>, commonly using namespaces from @medusajs/framework/types such as HttpTypes.StoreProductListResponse or HttpTypes.AdminProductDeleteResponse.
Dependency Resolution and Workflow Integration
Rather than embedding business logic directly in route handlers, Medusa delegates to workflows through dependency injection.
Accessing Services and Modules
Dependencies are resolved via req.scope.resolve(<key>), typically using constants from the Modules enum:
const productService = req.scope.resolve(Modules.PRODUCT)
Workflow Execution
Business operations are handled by imported workflows from @medusajs/core-flows, which the route executes and returns:
const { result } = await listProductsWorkflow(req.scope).run({
input: { filterableFields, listConfig },
})
Validation, Query Config, and Middleware Patterns
Medusa API route conventions enforce co-location of supporting utilities with route files to maintain modularity.
Co-Located Utilities
Each route directory can contain:
validators.ts: Zod schemas or custom validation functions for request payloadsquery-config.ts: Default pagination limits, selectable fields, and searchable field definitionsmiddlewares/: Cross-cutting concerns like tax context, pricing calculations, and inventory checks
Middlewares are composed in the route file using router.use() before handler registration.
Practical Code Examples
Basic Store GET Handler
The following example from packages/medusa/src/api/store/products/route.ts demonstrates a standard list operation:
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { HttpTypes } from "@medusajs/framework/types"
import { listProductsWorkflow } from "@medusajs/core-flows"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse<HttpTypes.StoreProductListResponse>
) => {
const { filterableFields, listConfig } = req
const { result } = await listProductsWorkflow(req.scope).run({
input: {
filterableFields,
listConfig,
},
})
res.status(200).json(result)
}
Admin DELETE Handler
For admin operations requiring authentication, use AuthenticatedMedusaRequest as shown in packages/medusa/src/api/admin/products/[id]/route.ts:
export const DELETE = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse<HttpTypes.AdminProductDeleteResponse>
) => {
const { id } = req.params
await deleteProductWorkflow(req.scope).run({
input: { id },
})
res.status(200).json({
id,
object: "product",
deleted: true,
})
}
Nested Dynamic Route
Deeply nested endpoints follow the [param]/subresource pattern, as seen in packages/medusa/src/api/store/shipping-options/[id]/calculate/route.ts:
export const POST = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse<HttpTypes.StoreShippingOptionCalculateResponse>
) => {
const { id } = req.params
const { cart_id, address } = req.body
const { result } = await calculateShippingOptionWorkflow(req.scope).run({
input: { shipping_option_id: id, cart_id, address },
})
res.status(200).json(result)
}
Summary
- File-based routing: Routes are defined in
route.tsfiles located underpackages/medusa/src/api/<scope>/<resource>/, with dynamic segments using[id]folder syntax. - HTTP verb exports: Handlers are exported as uppercase constants (
GET,POST,PUT,PATCH,DELETE) with standardized(req, res) => Promise<void>signatures. - Type safety: Store routes use
MedusaRequestwhile admin routes requireAuthenticatedMedusaRequest, both paired withMedusaResponse<T>. - Dependency injection: Services and modules are accessed via
req.scope.resolve()with workflow delegation to@medusajs/core-flows. - Co-located utilities: Validation logic, query configurations, and middleware are stored alongside routes in
validators.ts,query-config.ts, andmiddlewares/directories.
Frequently Asked Questions
How do I create a custom admin API route in Medusa?
Create a route.ts file under packages/medusa/src/api/admin/<your-resource>/ and export your HTTP handlers using uppercase constants. Use AuthenticatedMedusaRequest for the request type to ensure proper authentication, and resolve your custom services via req.scope.resolve() before delegating to workflows.
What is the difference between store and admin route types in Medusa?
Store routes use the MedusaRequest<T> type for unauthenticated or session-based customer requests, while admin routes use AuthenticatedMedusaRequest<T> which enforces admin user authentication. Both use MedusaResponse<T> for responses, but admin routes typically include additional middleware for permissions and context initialization.
How does Medusa handle route parameters in nested URLs?
Medusa implements nested dynamic routes using filesystem brackets, where [id]/route.ts creates a /:id endpoint and deeper nesting like [id]/calculate/route.ts produces /:id/calculate URLs. Access these parameters via req.params inside your handler, with the parameter name matching the bracket notation used in the directory structure.
Where should validation logic be placed in Medusa API routes?
Validation logic should be extracted into a co-located validators.ts file within your route directory, imported and executed in the handler before workflow invocation. This pattern keeps route files clean while ensuring Zod schemas or custom validators enforce request payload shapes consistently across the API surface.
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 →