How to Validate Input Data for Medusa API Routes: A Complete Zod Implementation Guide

Medusa validates API requests using Zod schemas defined in *_validators.ts files, processed by the validateAndTransformBody middleware from @medusajs/framework/http, which attaches parsed, type-safe data to req.validatedBody before the route handler executes.

The medusajs/medusa repository implements a declarative validation architecture that ensures every API endpoint receives strictly typed, sanitized input. Rather than manual validation inside route handlers, Medusa separates schema definitions from business logic, leveraging Zod's strict parsing and transformation capabilities to reject malformed requests before they reach the service layer.

The Validation Architecture

Medusa's validation flow follows a three-step pipeline implemented consistently across both store and admin API routes. According to the source code in packages/medusa/src/api/store/carts, the system relies on Zod for schema definition and custom framework middleware for execution.

Step 1: Define Zod Schemas in *_validators.ts Files

Validation schemas reside in dedicated validator files alongside route definitions. In packages/medusa/src/api/store/carts/validators.ts, the CreateCart schema demonstrates the pattern using Zod's fluent API with strict mode enabled.

Key characteristics of Medusa validators include:

  • .strict() configuration – Rejects unknown keys to prevent payload bloat, as seen in line 28 of the cart validators file
  • WithAdditionalData wrappers – Allow dynamic field injection while maintaining base schema integrity
  • Type coercion – Automatically transforms string inputs to numbers, dates, or other types before validation completes
// packages/medusa/src/api/store/carts/validators.ts
import { z } from "@medusajs/framework/zod"
import { WithAdditionalData } from "@medusajs/framework/types"

export const CreateCart = z
  .object({
    email: z.string().email(),
    region_id: z.string(),
    shipping_address: z.object({
      address_1: z.string(),
      city: z.string(),
      country_code: z.string().length(2),
      postal_code: z.string(),
    }),
    items: z.array(
      z.object({
        variant_id: z.string(),
        quantity: z.number().gt(0),
      })
    ),
  })
  .strict()

Step 2: Attach Validation Middleware to Routes

The validateAndTransformBody middleware (or validateAndTransformQuery for GET requests) is registered in the route's middleware configuration array. In packages/medusa/src/api/store/carts/middlewares.ts, this middleware intercepts incoming requests, parses the JSON body against the Zod schema, and handles validation failures by returning a 400 error with detailed field-level messages.

// packages/medusa/src/api/store/carts/middlewares.ts
import {
  validateAndTransformBody,
  validateAndTransformQuery,
} from "@medusajs/framework/http"
import { CreateCart } from "./validators"

export const storeCartRoutesMiddlewares = [
  {
    method: ["POST"],
    matcher: "/store/carts",
    middlewares: [
      validateAndTransformBody(CreateCart),
    ],
  },
]

Step 3: Consume Validated Data in Route Handlers

Once validation passes, the middleware attaches the parsed and typed data to req.validatedBody. Route handlers in files like packages/medusa/src/api/store/carts/route.ts access this property, which is guaranteed to match the Zod schema's TypeScript type definition.

// packages/medusa/src/api/store/carts/route.ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const POST = async (
  req: MedusaRequest, 
  res: MedusaResponse
) => {
  // req.validatedBody is typed and validated according to CreateCart schema
  const cartData = req.validatedBody
  
  const cartService = req.scope.resolve("cartService")
  const cart = await cartService.create(cartData)
  
  return res.status(201).json({ cart })
}

Validating Query Parameters for GET Requests

For endpoints that accept query strings, Medusa uses validateAndTransformQuery combined with createSelectParams() to handle pagination, field selection, and filtering. This pattern appears in the same middleware configuration files but targets req.validatedQuery instead of req.validatedBody.

// packages/medusa/src/api/store/carts/validators.ts
import { createSelectParams } from "../../../utils/validators"

export const GetCartQueryParams = createSelectParams()
// packages/medusa/src/api/store/carts/middlewares.ts
{
  method: ["GET"],
  matcher: "/store/carts/:id",
  middlewares: [
    validateAndTransformQuery(GetCartQueryParams),
  ],
}

The createSelectParams() utility generates a standard schema supporting fields, limit, offset, and order parameters used consistently across Medusa's API surface.

Complete Implementation Example

Here is a complete workflow for validating input data in a custom orders endpoint:

1. Define the schema:

// src/api/store/orders/validators.ts
import { z } from "@medusajs/framework/zod"

export const CreateOrderSchema = z
  .object({
    email: z.string().email(),
    shipping_address: z.object({
      address_1: z.string().min(1),
      city: z.string(),
      country_code: z.string().length(2),
      postal_code: z.string(),
    }),
    items: z.array(
      z.object({
        variant_id: z.string(),
        quantity: z.number().gt(0),
      })
    ),
  })
  .strict()

2. Configure middleware:

// src/api/store/orders/middlewares.ts
import { validateAndTransformBody } from "@medusajs/framework/http"
import { CreateOrderSchema } from "./validators"

export const storeOrderRoutesMiddlewares = [
  {
    method: ["POST"],
    matcher: "/store/orders",
    middlewares: [
      validateAndTransformBody(CreateOrderSchema),
    ],
  },
]

3. Implement the handler:

// src/api/store/orders/route.ts
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const orderService = req.scope.resolve("orderService")
  
  // req.validatedBody is guaranteed to match CreateOrderSchema
  const order = await orderService.create(req.validatedBody)
  
  return res.status(201).json(order)
}

Summary

  • Zod schemas defined in *_validators.ts files describe the exact shape, types, and constraints of request payloads using .strict() mode to reject unknown properties.
  • validateAndTransformBody middleware from @medusajs/framework/http executes validation automatically and attaches parsed data to req.validatedBody.
  • validateAndTransformQuery handles GET request validation, typically using createSelectParams() for standardized pagination and field selection.
  • Source references: The cart implementation in packages/medusa/src/api/store/carts/validators.ts and packages/medusa/src/api/store/carts/middlewares.ts demonstrates the canonical pattern used throughout the Medusa codebase.

Frequently Asked Questions

What happens if validation fails in a Medusa API route?

When Zod validation fails, the validateAndTransformBody middleware automatically returns a 400 Bad Request response with detailed error messages indicating which fields failed validation and why. This occurs before the route handler executes, ensuring that business logic only receives valid, sanitized data.

Can I use custom validation logic beyond Zod schemas?

While Medusa's framework is optimized for Zod-based validation in *_validators.ts files, you can implement custom middleware functions that execute before or after the standard validation middleware. However, the recommended approach is to extend Zod schemas using .refine() or .transform() methods to keep validation declarative and type-safe.

How do I validate request headers or route parameters in Medusa?

For route parameters like :id in /store/orders/:id, Medusa automatically validates that the parameter exists as a string. For custom header validation, you would create a custom middleware function registered in the route's middlewares array, similar to how validateAndTransformBody is attached, but targeting req.headers instead of req.body.

Where does the validated data get stored in the request object?

After successful validation, the parsed data is attached to req.validatedBody for POST/PUT/PATCH requests or req.validatedQuery for GET requests. These properties are typed according to the Zod schema definition and are accessible within the route handler function without additional type casting.

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 →