# How to Build Responses in Medusa API Routes: Complete Guide with Code Examples

> Learn to build responses in Medusa API routes. Fetch paginated data, enrich results, and return uniform JSON using MedusaResponse. Full code examples provided for your integration.

- Repository: [Medusa/medusa](https://github.com/medusajs/medusa)
- Tags: how-to-guide
- Published: 2026-05-19

---

**Medusa API routes build responses by resolving the query service to fetch paginated data, optionally enriching results with tax prices or inventory quantities, and returning a uniform JSON object using the typed `MedusaResponse` wrapper.**

Every route in the MedusaJS framework follows a standardized pattern for constructing HTTP responses. When you build responses in Medusa API routes, you work with a typed wrapper around Express that enforces consistent JSON shapes across store and admin endpoints. This architecture leverages dependency injection, query builders, and utility helpers to transform raw database rows into clean, paginated API outputs.

## Core Components: MedusaResponse and Request Types

The foundation of response building starts with two typed objects imported from `@medusajs/framework/http`.

**`MedusaResponse`** is a thin wrapper around Express's `res` object that adds TypeScript typings for the JSON body. Every route handler uses this to ensure type-safe responses:

```typescript
import { MedusaResponse, RequestWithContext } from "@medusajs/framework/http"
import { HttpTypes } from "@medusajs/framework/types"

export const GET = async (
  req: RequestWithContext<HttpTypes.StoreProductListParams>,
  res: MedusaResponse<HttpTypes.StoreProductListResponse>
) => {
  // Handler implementation
  res.json({ products: [], count: 0, offset: 0, limit: 20 })
}

```

**Request types** extend the raw Express request with helpers for filtering, pagination, and context. Store routes typically use `RequestWithContext`, while admin routes use `AuthenticatedMedusaRequest` to access authenticated user data and extended filterable fields.

## The Query Service Pattern

The canonical way to fetch data uses the **query service** resolved from the container using `ContainerRegistrationKeys.QUERY`. This service abstracts database access and returns both data and pagination metadata.

In [`packages/medusa/src/api/store/products/route.ts`](https://github.com/medusajs/medusa/blob/main/packages/medusa/src/api/store/products/route.ts), the pattern looks like this:

```typescript
import { ContainerRegistrationKeys, QueryContext, isPresent } from "@medusajs/framework/utils"

export const GET = async (req, res) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
  
  const context: object = {}
  if (isPresent(req.pricingContext)) {
    context["variants"] = { calculated_price: QueryContext(req.pricingContext) }
  }

  const { data: products = [], metadata } = await query.graph({
    entity: "product",
    fields: req.queryConfig.fields,
    filters: req.filterableFields,
    pagination: req.queryConfig.pagination,
    context,
  })
  
  // Response building continues...
}

```

The `query.graph` method (or `query.index` when using the Index Engine feature flag) returns an object containing `data` and `metadata`, which drives the final response structure.

## Structuring Paginated Responses

Medusa enforces a uniform pagination format across all list endpoints. The **metadata** object from the query service contains `count` (or `estimate_count` for index queries), `skip`, and `take`, which map to `count`, `offset`, and `limit` in the JSON response.

As implemented in [`packages/medusa/src/api/admin/orders/route.ts`](https://github.com/medusajs/medusa/blob/main/packages/medusa/src/api/admin/orders/route.ts), the standard response shape combines the data array with these pagination fields:

```typescript
res.json({
  orders: rows as unknown as HttpTypes.AdminOrder[],
  count: metadata.count,
  offset: metadata.skip,
  limit: metadata.take,
})

```

This ensures every list endpoint returns a predictable shape for frontend consumers and third-party integrations.

## Data Transformation Utilities

Before sending the response, routes often post-process raw database results using utilities from `packages/medusa/src/utils/`.

**`cleanResponseData`** removes internal fields and trims the payload according to the `fields` query parameter. Located in [`packages/medusa/src/utils/clean-response-data.ts`](https://github.com/medusajs/medusa/blob/main/packages/medusa/src/utils/clean-response-data.ts), this utility filters out sensitive or internal properties like `raw_discount_total`:

```typescript
import { cleanResponseData } from "@medusajs/medusa/src/utils/clean-response-data"

const rawData = await someService.list()
const allowedFields = ["id", "email", "total"]
const filtered = cleanResponseData(rawData, allowedFields)

res.json({ customers: filtered })

```

Additional helpers like `wrapProductsWithTaxPrices` and `wrapVariantsWithInventoryQuantityForSalesChannel` attach computed data (tax calculations, inventory levels) that aren't stored directly in the database tables.

## Complete Implementation Examples

### Store Product Endpoint with Context

The store products route in [`packages/medusa/src/api/store/products/route.ts`](https://github.com/medusajs/medusa/blob/main/packages/medusa/src/api/store/products/route.ts) demonstrates the full pipeline including pricing context and tax wrapping:

```typescript
import { MedusaResponse, RequestWithContext } from "@medusajs/framework/http"
import { HttpTypes } from "@medusajs/framework/types"
import {
  ContainerRegistrationKeys,
  QueryContext,
  isPresent,
} from "@medusajs/framework/utils"

export const GET = async (
  req: RequestWithContext<HttpTypes.StoreProductListParams>,
  res: MedusaResponse<HttpTypes.StoreProductListResponse>
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const context: object = {}
  if (isPresent(req.pricingContext)) {
    context["variants"] = { calculated_price: QueryContext(req.pricingContext) }
  }

  const { data: products = [], metadata } = await query.graph({
    entity: "product",
    fields: req.queryConfig.fields,
    filters: req.filterableFields,
    pagination: req.queryConfig.pagination,
    context,
  })

  await wrapProductsWithTaxPrices(req, products)

  res.json({
    products,
    count: metadata!.count,
    offset: metadata!.skip,
    limit: metadata!.take,
  })
}

```

### Admin Orders with Workflow Integration

Admin routes often delegate data fetching to workflows. The orders route in [`packages/medusa/src/api/admin/orders/route.ts`](https://github.com/medusajs/medusa/blob/main/packages/medusa/src/api/admin/orders/route.ts) formats the workflow result into the standard response shape:

```typescript
import { getOrdersListWorkflow } from "@medusajs/core-flows"
import { HttpTypes, OrderDTO } from "@medusajs/framework/types"
import {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: AuthenticatedMedusaRequest<HttpTypes.AdminOrderFilters>,
  res: MedusaResponse<HttpTypes.AdminOrderListResponse>
) => {
  const variables = {
    filters: { ...req.filterableFields, is_draft_order: false },
    ...req.queryConfig.pagination,
  }

  const workflow = getOrdersListWorkflow(req.scope)
  const { result } = await workflow.run({
    input: { fields: req.queryConfig.fields, variables },
  })

  const { rows, metadata } = result as {
    rows: OrderDTO[]
    metadata: any
  }

  res.json({
    orders: rows as unknown as HttpTypes.AdminOrder[],
    count: metadata.count,
    offset: metadata.skip,
    limit: metadata.take,
  })
}

```

## Summary

- **Use `MedusaResponse`** from `@medusajs/framework/http` to ensure type-safe JSON outputs across all routes.
- **Resolve the query service** via `req.scope.resolve(ContainerRegistrationKeys.QUERY)` to fetch data with built-in pagination support.
- **Map metadata fields** consistently: `metadata.count` (or `metadata.estimate_count`), `metadata.skip`, and `metadata.take` become `count`, `offset`, and `limit` in your JSON response.
- **Apply post-processing utilities** like `cleanResponseData` (from [`packages/medusa/src/utils/clean-response-data.ts`](https://github.com/medusajs/medusa/blob/main/packages/medusa/src/utils/clean-response-data.ts)) to filter fields and attach computed values such as tax prices or inventory quantities.
- **Follow the standard pattern**: resolve query → execute with filters/pagination → transform data → return uniform JSON shape.

## Frequently Asked Questions

### How does Medusa handle pagination in API responses?

Medusa uses a metadata object returned by the query service that contains `count` (total records), `skip` (offset), and `take` (limit). These values are mapped directly to the response JSON fields `count`, `offset`, and `limit`, ensuring every list endpoint returns a consistent pagination structure that frontend applications can rely on for infinite scroll or table pagination.

### What is the difference between query.graph and query.index in Medusa?

`query.graph` is the default query method that uses Medusa's standard GraphQL-style query engine, while `query.index` is used when the Index Engine feature flag is enabled. Both return identical `{ data, metadata }` shapes, so you can build responses in Medusa API routes using the same pattern regardless of which engine is active under the hood.

### How do I restrict which fields are returned in a Medusa API response?

Use the `cleanResponseData` utility from [`packages/medusa/src/utils/clean-response-data.ts`](https://github.com/medusajs/medusa/blob/main/packages/medusa/src/utils/clean-response-data.ts). Pass your raw data array and an array of allowed field names (such as `["id", "email", "total"]`) to strip out internal database fields and sensitive data before calling `res.json()`. This respects the `fields` query parameter pattern used throughout the Medusa framework.

### Can I use workflows to build responses in custom API routes?

Yes, workflows are fully supported in custom routes. Resolve the workflow using `req.scope`, run it with your filter and pagination variables, then extract `rows` and `metadata` from the result. Format these into the standard response shape with `count`, `offset`, and `limit` fields before sending via `res.json()`, following the pattern established in [`packages/medusa/src/api/admin/orders/route.ts`](https://github.com/medusajs/medusa/blob/main/packages/medusa/src/api/admin/orders/route.ts).