Medusa v2 Custom API Routes
Before writing code
Fetch live docs:
- Fetch
https://docs.medusajs.com/learn/fundamentals/api-routesfor API route overview - Web-search
site:docs.medusajs.com custom API route file conventionfor file-based routing - Web-search
site:docs.medusajs.com API route middlewarefor middleware configuration - Web-search
site:docs.medusajs.com API route validation zodfor request validation - Web-search
site:docs.medusajs.com additional data API routesfor the additional-data pattern
File-Based Routing
Medusa v2 uses a file-system router under src/api/:
| File Path | Resulting Endpoint |
|---|---|
src/api/store/custom/route.ts | GET/POST /store/custom |
src/api/admin/custom/route.ts | GET/POST /admin/custom |
src/api/store/custom/[id]/route.ts | GET/POST /store/custom/:id |
src/api/custom/route.ts | GET/POST /custom (no auth prefix) |
Conventions
- File must be named
route.ts(notindex.ts) - Dynamic parameters use
[param]folder syntax - The
store/prefix applies storefront authentication scope - The
admin/prefix applies admin authentication scope - Routes outside these prefixes have no default authentication
HTTP Method Exports
Each route.ts exports named functions matching HTTP methods:
| Export Name | HTTP Method |
|---|---|
GET | GET |
POST | POST |
PUT | PUT |
PATCH | PATCH |
DELETE | DELETE |
Route Handler Skeleton
// src/api/store/custom/route.ts
// Fetch live docs for MedusaRequest/MedusaResponse types
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const service = req.scope.resolve("my-module")
res.json({ items: await service.listMyEntities() })
}
Route with Dynamic Parameter
// src/api/store/custom/[id]/route.ts
// Fetch live docs for path parameter access
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const item = await req.scope.resolve("my-module").retrieveMyEntity(req.params.id)
res.json({ item })
}
Request Validation with Zod
Medusa v2 uses Zod schemas for request body and query parameter validation:
Validation File Convention
| Validation Target | File |
|---|---|
| POST/PUT/PATCH body | validators.ts in same directory as route.ts |
| Query parameters | Same validators.ts file |
Validator Skeleton
// src/api/store/custom/validators.ts
// Fetch live docs for Zod schema integration
import { z } from "zod"
export const PostStoreCustom = z.object({
name: z.string(),
description: z.string().optional(),
})
Validators are linked to routes via middleware configuration.
Middleware Configuration
All middleware is configured in src/api/middlewares.ts:
// src/api/middlewares.ts — Fetch live docs for defineMiddlewares API
import { defineMiddlewares, validateAndTransformBody } from "@medusajs/framework/http"
import { PostStoreCustom } from "./store/custom/validators"
export default defineMiddlewares({
routes: [{ matcher: "/store/custom", method: "POST",
middlewares: [validateAndTransformBody(PostStoreCustom)] }],
})
Built-in Middleware Utilities
| Utility | Purpose |
|---|---|
validateAndTransformBody(schema) | Validate request body with Zod |
validateAndTransformQuery(schema) | Validate query parameters |
authenticate("customer", ["session", "bearer"]) | Require customer auth |
authenticate("user", ["session", "bearer"]) | Require admin auth |
Authentication Middleware
| Route Prefix | Default Auth | Custom Auth Override |
|---|---|---|
/admin/* | Admin user required | Can be loosened per route |
/store/* | Optional customer auth | Can require auth per route |
/custom/* | None | Must add explicitly |
Requiring Customer Authentication
// In middlewares.ts routes array
// Fetch live docs for authenticate() options
{
matcher: "/store/custom/me",
middlewares: [authenticate("customer", ["session", "bearer"])],
}
Additional Data Pattern
Medusa v2 supports passing extra data through built-in API routes to workflow hooks:
- Client sends extra fields in the request body under
additional_data - Validated by defining an
additionalDataValidatorin middleware - Received in workflow hooks as
additional_data
This allows extending core commerce flows (e.g., adding custom fields to product creation) without overriding API routes.
Error Handling
| Error Type | Recommended Approach |
|---|---|
| Validation error | Handled automatically by Zod middleware (400) |
| Not found | Throw MedusaError with NOT_FOUND type |
| Unauthorized | Handled by auth middleware (401) |
| Business logic | Throw MedusaError with appropriate type |
| Unexpected | Let Medusa error handler return 500 |
Best Practices
- Use file-based routing conventions -- do not register routes programmatically
- Always validate request bodies with Zod schemas via
validateAndTransformBody - Resolve services from
req.scope-- never import services directly - Use the
additional_datapattern to extend core routes instead of overriding them - Apply
authenticate()middleware explicitly for routes requiring auth outside default scopes - Return consistent response shapes:
{ item }for single,{ items, count, offset, limit }for lists - Keep route handlers thin -- delegate business logic to workflows or services
Fetch the Medusa API route documentation for exact file conventions, middleware utilities, and Zod integration patterns before implementing.