How to Handle GET, POST, PUT, and DELETE Request Methods in Medusa API Routes

Medusa API routes handle HTTP verbs by exporting named asynchronous functions (GET, POST, PUT, DELETE) from a route.ts file, which the framework's router loader in packages/core/framework/src/http/router.ts automatically registers on the Express router using typed MedusaRequest and MedusaResponse wrappers.

The medusajs/medusa repository implements a convention-based routing system where each API endpoint lives in a route.ts file under packages/medusa/src/api/.... Inside each file, you export functions matching the HTTP methods you want to support, enabling a clean separation between routing infrastructure and business logic workflows.

Understanding the Named Export Handler Pattern

When the Medusa server initializes, the router loader (packages/core/framework/src/http/router.ts) scans the API directory for route.ts files. It inspects the exports and registers any functions named GET, POST, PUT, or DELETE as handlers for their respective HTTP verbs on the corresponding route path.

Every handler follows a strict TypeScript signature:

export const <METHOD> = async (
  req: MedusaRequest<Params, Query>,
  res: MedusaResponse<ResponseBody>
) => { … }
  • MedusaRequest – Wraps Express' Request to inject Medusa-specific helpers including req.scope for dependency injection, req.filterableFields for parsed query parameters, and req.queryConfig for field selection.
  • MedusaResponse – Wraps Express' Response providing typed json(), status(), and other response methods.

Inside a handler, the standard flow involves reading input from the request object, resolving a workflow or service via req.scope, executing the workflow, and returning a typed JSON response.

Implementing GET Handlers for Read Operations

Use the GET export for read-only endpoints that retrieve data. The handler typically extracts filterable fields from req.filterableFields and passes them to a listing workflow.

The following example from the store shipping options endpoint demonstrates retrieving cart-specific shipping options:

// packages/medusa/src/api/store/shipping-options/route.ts
export const GET = async (
  req: MedusaRequest<{}, HttpTypes.StoreGetShippingOptionList>,
  res: MedusaResponse<HttpTypes.StoreShippingOptionListResponse>
) => {
  const { cart_id, is_return } = req.filterableFields

  const workflow = listShippingOptionsForCartWorkflow(req.scope)
  const { result: shipping_options } = await workflow.run({
    input: {
      cart_id,
      is_return: !!is_return,
      fields: req.queryConfig.fields,
    },
  })

  res.json({ shipping_options })
}

The handler resolves listShippingOptionsForCartWorkflow from the request scope, passes the cart ID and return flag from the parsed query parameters, and returns the result as JSON.

Creating Resources with POST Handlers

The POST export handles resource creation and action endpoints such as creating carts, adding line items, or processing payments. These handlers read data from req.body and typically return a 201 Created status.

Example from the cart creation endpoint:

// packages/medusa/src/api/store/carts/route.ts
export const POST = async (
  req: MedusaRequest<{}, HttpTypes.StorePostCartReq>,
  res: MedusaResponse<HttpTypes.StoreCartRes>
) => {
  const workflow = createCartWorkflow(req.scope)
  const { result: cart } = await workflow.run({
    input: {
      ...req.body,
      fields: req.queryConfig.fields,
    },
  })

  res.status(201).json({ cart })
}

Notice how the handler spreads req.body into the workflow input and explicitly sets the HTTP status to 201 to indicate successful resource creation.

Removing Data with DELETE Handlers

Use the DELETE export to remove resources or revoke actions. These handlers extract resource identifiers from req.params and return a 200 OK or 204 No Content response upon successful deletion.

Example from the cart line item deletion endpoint:

// packages/medusa/src/api/store/carts/[id]/line-items/[line_id]/route.ts
export const DELETE = async (
  req: MedusaRequest<{}, HttpTypes.StoreDeleteCartLineItemReq>,
  res: MedusaResponse<void>
) => {
  const workflow = deleteCartLineItemWorkflow(req.scope)
  await workflow.run({
    input: {
      cart_id: req.params.id,
      line_id: req.params.line_id,
    },
  })

  res.status(200).send()
}

The handler captures the cart ID and line item ID from the URL parameters, executes deleteCartLineItemWorkflow, and sends an empty response with a 200 status code.

Handling PUT Requests

While the same named-export pattern applies to PUT handlers, Medusa rarely uses them. According to the source code conventions, updates are generally performed via POST requests to specific "update" endpoints (e.g., /admin/products/:id) rather than using PUT. When you do need a PUT handler, implement it using the same async (req, res) signature as GET or POST.

Summary

  • Named exports drive routing – Export GET, POST, PUT, or DELETE functions from route.ts files to define supported HTTP methods.
  • Typed request/response objects – Use MedusaRequest and MedusaResponse for type safety and access to Medusa-specific features like req.scope and req.filterableFields.
  • Workflow delegation – Resolve workflows via req.scope and execute them with typed input rather than writing business logic directly in route handlers.
  • Status codes matter – Return 201 for successful POST requests, 200 for successful DELETE requests, and use res.json() for structured responses.

Frequently Asked Questions

What is the difference between MedusaRequest and Express's standard Request object?

MedusaRequest is a thin wrapper around Express' Request that injects Medusa-specific properties including req.scope for accessing the dependency injection container, req.filterableFields for parsed query parameters, and req.queryConfig for field selection configuration. This allows handlers to interact with Medusa's modular architecture while maintaining Express compatibility.

Can I define multiple HTTP methods in the same route.ts file?

Yes, you can export multiple handlers from a single file. For example, exporting both GET and POST from packages/medusa/src/api/store/products/route.ts registers both methods on the same route path, with each function handling its respective verb using the shared MedusaRequest and MedusaResponse types.

Why does Medusa use POST instead of PUT for updates?

Medusa's API design favors POST requests to specific action endpoints (like /admin/products/:id) for updates rather than using PUT. This aligns with the framework's workflow-based architecture where updates often trigger complex business processes. While PUT handlers are technically supported via the named export pattern, they are rarely implemented in the core codebase.

How do I access services or workflows inside a route handler?

Access the dependency injection container through req.scope. For workflows, import the workflow function (e.g., createCartWorkflow from @medusajs/core-flows) and invoke it with req.scope as the argument: const workflow = createCartWorkflow(req.scope). Then execute the workflow using await workflow.run({ input: ... }) to trigger the business logic.

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 →