How to Create a Custom Medusa API Route: File-Based Routing Guide

You create a custom Medusa API route by placing a route.ts file under src/api/<path>/ and exporting HTTP method handlers (like GET or POST) named after the verbs they handle, which automatically registers the endpoint at the corresponding URL path.

Medusa uses a file-based routing system that scans src/api/**/route.ts files on startup and wires them to the appropriate HTTP methods without requiring manual route registration. This architecture lets you extend your commerce backend by simply adding TypeScript files that follow the framework's naming conventions. The system supports dynamic path parameters, dependency injection via the container, and custom middleware through a centralized configuration file.

Understanding File-Based Routing Structure

Medusa automatically discovers API endpoints by scanning the src/api directory of your project. The folder structure directly determines the final URL path, while the exported handler functions determine supported HTTP methods.

According to the official plugin documentation in packages/plugins/loyalty/src/api/README.md, the framework expects:

  • Route files: Must be named route.ts (or route.js) and live inside folders under src/api
  • URL mapping: The folder path becomes the endpoint path (e.g., src/api/store/hello-world/route.ts becomes GET /store/hello-world)
  • HTTP handlers: Export functions named exactly after HTTP verbs: GET, POST, PUT, PATCH, DELETE, OPTIONS, or HEAD

Creating a Basic GET Endpoint

To create your first custom route, add a route.ts file in a new folder under src/api.

// src/api/store/hello-world/route.ts
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
  res.json({ message: "Hello world!" })
}

When the server starts, Medusa registers this handler at GET /store/hello-world. The MedusaRequest and MedusaResponse types provide full access to the request context and response helpers from the @medusajs/framework/http package.

Handling Dynamic Path Parameters

You capture dynamic URL segments by using bracketed folder names like [param]. The parameter values become available on req.params.

// src/api/store/products/[productId]/route.ts
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
  const { productId } = req.params
  res.json({ message: `You requested product ${productId}` })
}

This pattern matches GET /store/products/123 and extracts "123" as req.params.productId. You can nest multiple parameter folders to create complex route hierarchies.

Accessing Services via Dependency Injection

Custom routes integrate with Medusa's service layer through the request-scoped container. Use req.scope.resolve("<serviceKey>") to access any registered service.

// src/api/store/product-count/route.ts
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
  const productService = req.scope.resolve("product")
  const [, count] = await productService.listAndCount()
  res.json({ count })
}

The product service key resolves to the core Product service, allowing you to reuse existing business logic and database queries. This dependency injection pattern ensures your custom routes remain consistent with Medusa's data access patterns.

Adding Custom Middleware

To execute code before your route handler runs, create a middlewares.ts file in src/api and export a configuration using defineMiddlewares.

// src/api/middlewares.ts
import { defineMiddlewares } from "@medusajs/framework/http"
import type { MedusaRequest, MedusaResponse, MedusaNextFunction } from "@medusajs/framework/http"

async function logger(req: MedusaRequest, res: MedusaResponse, next: MedusaNextFunction) {
  console.log("[Custom Logger]:", req.method, req.originalUrl)
  next()
}

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/hello-world",
      middlewares: [logger],
    },
  ],
})

The matcher property supports route patterns, and the middlewares array accepts functions with the standard Express-style signature (req, res, next). This configuration applies the middleware only to the specified paths while leaving other routes unaffected.

Real-World Implementation Reference

For complex implementations that query data, handle feature flags, and transform responses, examine the core product routes in the Medusa source. The packages/medusa/src/api/store/products/route.ts file demonstrates how production routes wire together query building, inventory checking, and tax price wrapping:

// packages/medusa/src/api/store/products/route.ts
import { MedusaResponse } from "@medusajs/framework/http"
import { HttpTypes, QueryContextType } from "@medusajs/framework/types"
import {
  ContainerRegistrationKeys,
  FeatureFlag,
  isPresent,
  QueryContext,
} from "@medusajs/framework/utils"
import IndexEngineFeatureFlag from "../../../feature-flags/index-engine"
import { wrapVariantsWithInventoryQuantityForSalesChannel } from "../../utils/middlewares"
import { RequestWithContext, wrapProductsWithTaxPrices } from "./helpers"

export const GET = async (
  req: RequestWithContext<HttpTypes.StoreProductListParams>,
  res: MedusaResponse<HttpTypes.StoreProductListResponse>
) => {
  // Implementation handles pagination, filtering, and response transformation
}

This implementation shows how to leverage ContainerRegistrationKeys for type-safe service resolution and integrate with Medusa's feature flag system.

Summary

  • File naming: Create route.ts files under src/api/<path>/ to automatically register endpoints at the corresponding URL paths
  • HTTP handlers: Export functions named exactly after HTTP verbs (GET, POST, PUT, DELETE, etc.) to handle specific methods
  • Dynamic segments: Use bracketed folder names like [productId] to capture URL parameters accessible via req.params
  • Service access: Resolve services through req.scope.resolve("<serviceKey>") to maintain clean dependency injection
  • Middleware: Configure route-specific middleware in src/api/middlewares.ts using the defineMiddlewares export

Frequently Asked Questions

What file extension should I use for Medusa API routes?

Medusa supports both TypeScript (.ts) and JavaScript (.js) files. Use route.ts for TypeScript projects to benefit from type safety with MedusaRequest and MedusaResponse types imported from @medusajs/framework/http.

How do I handle POST requests and request body validation?

Export a POST function from your route.ts file. Access the request body through req.body, which Medusa validates according to your defined schemas. The request object includes typed body data when you extend the base MedusaRequest type with your validation schema.

Can I register multiple HTTP methods on the same route?

Yes. Simply export multiple handler functions from the same route.ts file, such as GET, POST, and PUT. Medusa registers each exported verb as a handler for that specific HTTP method on the same URL path defined by the folder structure.

Where should I place global middleware that affects all routes?

Create src/api/middlewares.ts at the root of your API directory and export a defineMiddlewares configuration. Use wildcard matchers like /store/* or apply middleware to specific routes using exact path matchers. This file acts as the central registry for all custom middleware in your application.

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 →