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:

  • GET for retrieval operations
  • POST for resource creation
  • PUT and PATCH for updates
  • DELETE for 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 payloads
  • query-config.ts: Default pagination limits, selectable fields, and searchable field definitions
  • middlewares/: 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.ts files located under packages/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 MedusaRequest while admin routes require AuthenticatedMedusaRequest, both paired with MedusaResponse<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, and middlewares/ 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:

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 →