874 lines
23 KiB
Markdown
874 lines
23 KiB
Markdown
# Custom API Routes
|
|
|
|
API routes (also called "endpoints") are the primary way to expose custom functionality to storefronts and admin dashboards.
|
|
|
|
## Contents
|
|
- [Path Conventions](#path-conventions)
|
|
- [Middleware Validation](#middleware-validation)
|
|
- [Query Parameter Validation](#query-parameter-validation)
|
|
- [Request Query Config for List Endpoints](#request-query-config-for-list-endpoints)
|
|
- [API Route Structure](#api-route-structure)
|
|
- [Error Handling](#error-handling)
|
|
- [Protected Routes](#protected-routes)
|
|
- [Using Workflows in API Routes](#using-workflows-in-api-routes)
|
|
|
|
## Path Conventions
|
|
|
|
### Store API Routes (Storefront)
|
|
- **Path prefix**: `/store/<rest-of-path>`
|
|
- **Examples**: `/store/newsletter-signup`, `/store/custom-search`
|
|
- **Authentication**: SDK automatically includes publishable API key
|
|
|
|
### Admin API Routes (Dashboard)
|
|
- **Path prefix**: `/admin/<rest-of-path>`
|
|
- **Examples**: `/admin/custom-reports`, `/admin/bulk-operations`
|
|
- **Authentication**: SDK automatically includes auth headers (bearer/session)
|
|
|
|
**Detailed authentication patterns**: See [authentication.md](authentication.md)
|
|
|
|
## Middleware Validation
|
|
|
|
**⚠️ CRITICAL**: Always validate request bodies using Zod schemas and the `validateAndTransformBody` middleware.
|
|
|
|
### Combining Multiple Middlewares
|
|
|
|
When you need both authentication AND validation, pass them as an array. **NEVER nest validation inside authenticate:**
|
|
|
|
```typescript
|
|
// ✅ CORRECT - Multiple middlewares in array
|
|
export default defineMiddlewares({
|
|
routes: [
|
|
{
|
|
matcher: "/store/products/:id/reviews",
|
|
method: "POST",
|
|
middlewares: [
|
|
authenticate("customer", ["session", "bearer"]),
|
|
validateAndTransformBody(CreateReviewSchema)
|
|
],
|
|
},
|
|
],
|
|
})
|
|
|
|
// ❌ WRONG - Don't nest validator inside authenticate
|
|
export default defineMiddlewares({
|
|
routes: [
|
|
{
|
|
matcher: "/store/products/:id/reviews",
|
|
method: "POST",
|
|
middlewares: [authenticate("customer", ["session", "bearer"], {
|
|
validator: CreateReviewSchema // This doesn't work!
|
|
})],
|
|
},
|
|
],
|
|
})
|
|
```
|
|
|
|
**Middleware order matters:** Put `authenticate` before `validateAndTransformBody` so authentication happens first.
|
|
|
|
### Step 1: Create Middleware File
|
|
|
|
```typescript
|
|
// api/store/[feature]/middlewares.ts
|
|
import { MiddlewareRoute, validateAndTransformBody } from "@medusajs/framework"
|
|
import { z } from "zod"
|
|
|
|
export const CreateMySchema = z.object({
|
|
email: z.string().email(),
|
|
name: z.string().min(2),
|
|
// other fields
|
|
})
|
|
|
|
// Export the inferred type for use in route handlers
|
|
export type CreateMySchema = z.infer<typeof CreateMySchema>
|
|
|
|
export const myMiddlewares: MiddlewareRoute[] = [
|
|
{
|
|
matcher: "/store/my-route",
|
|
method: "POST",
|
|
middlewares: [validateAndTransformBody(CreateMySchema)],
|
|
},
|
|
]
|
|
```
|
|
|
|
### Step 2: Register in api/middlewares.ts
|
|
|
|
```typescript
|
|
// api/middlewares.ts
|
|
import { defineMiddlewares } from "@medusajs/framework/http"
|
|
import { myMiddlewares } from "./store/[feature]/middlewares"
|
|
|
|
export default defineMiddlewares({
|
|
routes: [...myMiddlewares],
|
|
})
|
|
```
|
|
|
|
**⚠️ CRITICAL - Middleware Export Pattern:**
|
|
|
|
Middlewares are exported as **named arrays**, NOT default exports with config objects:
|
|
|
|
```typescript
|
|
// ✅ CORRECT - Named export of MiddlewareRoute array
|
|
// api/store/reviews/middlewares.ts
|
|
export const reviewMiddlewares: MiddlewareRoute[] = [
|
|
{
|
|
matcher: "/store/reviews",
|
|
method: "POST",
|
|
middlewares: [validateAndTransformBody(CreateReviewSchema)],
|
|
},
|
|
]
|
|
|
|
// ✅ CORRECT - Import and spread the named array
|
|
// api/middlewares.ts
|
|
import { reviewMiddlewares } from "./store/reviews/middlewares"
|
|
|
|
export default defineMiddlewares({
|
|
routes: [...reviewMiddlewares],
|
|
})
|
|
```
|
|
|
|
```typescript
|
|
// ❌ WRONG - Don't use default export with .config
|
|
// api/store/reviews/middlewares.ts
|
|
export default {
|
|
config: {
|
|
routes: [...], // This is NOT the middleware pattern!
|
|
},
|
|
}
|
|
|
|
// ❌ WRONG - Don't access .config.routes
|
|
// api/middlewares.ts
|
|
import reviewMiddlewares from "./store/reviews/middlewares"
|
|
export default defineMiddlewares({
|
|
routes: [...reviewMiddlewares.config.routes], // This doesn't work!
|
|
})
|
|
```
|
|
|
|
**Why this matters:**
|
|
- Middleware files export arrays directly, not config objects
|
|
- Route files (like `route.ts`) use `export const config = defineRouteConfig(...)`
|
|
- Don't confuse the two patterns - middlewares are simpler (just an array)
|
|
|
|
### Step 3: Use Typed req.validatedBody in Route
|
|
|
|
**⚠️ CRITICAL**: When using `req.validatedBody`, you MUST pass the inferred Zod schema type as a type argument to `MedusaRequest`. Otherwise, you'll get TypeScript errors when accessing `req.validatedBody`.
|
|
|
|
```typescript
|
|
// api/store/my-route/route.ts
|
|
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
import { CreateMySchema } from "./middlewares"
|
|
|
|
// ✅ CORRECT: Pass the Zod schema type as type argument
|
|
export async function POST(
|
|
req: MedusaRequest<CreateMySchema>,
|
|
res: MedusaResponse
|
|
) {
|
|
// Now req.validatedBody is properly typed
|
|
const { email, name } = req.validatedBody
|
|
|
|
// ... rest of implementation
|
|
}
|
|
|
|
// ❌ WRONG: Without type argument, req.validatedBody will have type errors
|
|
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
const { email, name } = req.validatedBody // Type error!
|
|
}
|
|
```
|
|
|
|
## Query Parameter Validation
|
|
|
|
For API routes that accept query parameters, use the `validateAndTransformQuery` middleware to validate them.
|
|
|
|
**⚠️ IMPORTANT**: When using `validateAndTransformQuery`, access query parameters via `req.validatedQuery` instead of `req.query`.
|
|
|
|
### Step 1: Create Validation Schema
|
|
|
|
Create a Zod schema for the query parameters. Since query parameters are originally strings or arrays of strings, use `z.preprocess` to transform them to other types:
|
|
|
|
```typescript
|
|
// api/custom/validators.ts
|
|
import { z } from "zod"
|
|
|
|
export const GetMyRouteSchema = z.object({
|
|
cart_id: z.string(), // String parameters don't need preprocessing
|
|
limit: z.preprocess(
|
|
(val) => {
|
|
if (val && typeof val === "string") {
|
|
return parseInt(val)
|
|
}
|
|
return val
|
|
},
|
|
z.number().optional()
|
|
),
|
|
status: z.enum(["active", "pending", "completed"]).optional(),
|
|
})
|
|
```
|
|
|
|
### Step 2: Add Middleware
|
|
|
|
```typescript
|
|
// api/middlewares.ts
|
|
import {
|
|
validateAndTransformQuery,
|
|
defineMiddlewares,
|
|
} from "@medusajs/framework/http"
|
|
import { GetMyRouteSchema } from "./custom/validators"
|
|
|
|
export default defineMiddlewares({
|
|
routes: [
|
|
{
|
|
matcher: "/store/my-route",
|
|
method: "GET",
|
|
middlewares: [
|
|
validateAndTransformQuery(GetMyRouteSchema, {}),
|
|
],
|
|
},
|
|
],
|
|
})
|
|
```
|
|
|
|
### Step 3: Use Validated Query in Route
|
|
|
|
```typescript
|
|
// api/store/my-route/route.ts
|
|
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
|
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
// Access validated query parameters (not req.query!)
|
|
const { cart_id, limit, status } = req.validatedQuery
|
|
|
|
// cart_id is string, limit is number, status is enum
|
|
const query = req.scope.resolve("query")
|
|
|
|
const { data } = await query.graph({
|
|
entity: "my_entity",
|
|
fields: ["id", "name"],
|
|
filters: { cart_id, status },
|
|
})
|
|
|
|
return res.json({ items: data })
|
|
}
|
|
```
|
|
|
|
## Request Query Config for List Endpoints
|
|
|
|
**⚠️ BEST PRACTICE**: For API routes that retrieve lists of resources, use request query config to allow clients to control fields, pagination, and ordering.
|
|
|
|
This pattern:
|
|
- Allows clients to specify which fields/relations to retrieve
|
|
- Enables client-controlled pagination
|
|
- Supports custom ordering
|
|
- Provides sensible defaults
|
|
|
|
### Step 1: Add Middleware with createFindParams
|
|
|
|
```typescript
|
|
// api/middlewares.ts
|
|
import {
|
|
validateAndTransformQuery,
|
|
defineMiddlewares,
|
|
} from "@medusajs/framework/http"
|
|
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
|
|
|
|
// createFindParams() generates a schema that accepts:
|
|
// - fields: Select specific fields/relations
|
|
// - offset: Skip N items
|
|
// - limit: Max items to return
|
|
// - order: Order by field(s) ASC/DESC
|
|
export const GetProductsSchema = createFindParams()
|
|
|
|
export default defineMiddlewares({
|
|
routes: [
|
|
{
|
|
matcher: "/store/products",
|
|
method: "GET",
|
|
middlewares: [
|
|
validateAndTransformQuery(
|
|
GetProductsSchema,
|
|
{
|
|
defaults: [
|
|
"id",
|
|
"title",
|
|
"variants.*", // Include all variant fields by default
|
|
],
|
|
isList: true, // Indicates this returns a list
|
|
defaultLimit: 15, // Default pagination limit
|
|
}
|
|
),
|
|
],
|
|
},
|
|
],
|
|
})
|
|
```
|
|
|
|
**Configuration Options:**
|
|
- `defaults`: Array of default fields and relations to retrieve
|
|
- `isList`: Boolean indicating if this returns a list (affects pagination)
|
|
- `allowed`: (Optional) Array of fields/relations allowed in the `fields` query param
|
|
- `defaultLimit`: (Optional) Default limit if not provided (default: 50)
|
|
|
|
### Step 2: Use Query Config in Route
|
|
|
|
**⚠️ CRITICAL**: When using `req.queryConfig`, do NOT explicitly set the `fields` property in your query. The `queryConfig` already contains the fields configuration, and setting it explicitly will cause TypeScript errors.
|
|
|
|
```typescript
|
|
// api/store/products/route.ts
|
|
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
|
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
const query = req.scope.resolve("query")
|
|
|
|
// ✅ CORRECT: Only use ...req.queryConfig (includes fields, pagination, etc.)
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
...req.queryConfig, // Contains fields, select, limit, offset, order
|
|
})
|
|
|
|
return res.json({ products })
|
|
}
|
|
|
|
// ❌ WRONG: Don't set fields explicitly when using queryConfig
|
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
const query = req.scope.resolve("query")
|
|
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title"], // ❌ Type error! queryConfig already sets fields
|
|
...req.queryConfig,
|
|
})
|
|
|
|
return res.json({ products })
|
|
}
|
|
```
|
|
|
|
**If you need additional filters**, only add those - not fields:
|
|
|
|
```typescript
|
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
const query = req.scope.resolve("query")
|
|
const { id } = req.params
|
|
|
|
// ✅ CORRECT: Add filters while using queryConfig
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
filters: { id }, // Additional filters are OK
|
|
...req.queryConfig, // Fields come from here
|
|
})
|
|
|
|
return res.json({ products })
|
|
}
|
|
```
|
|
|
|
### Step 3: Client Usage Examples
|
|
|
|
Clients can now control the API response:
|
|
|
|
```typescript
|
|
// Default response (uses middleware defaults)
|
|
GET /store/products
|
|
// Returns: id, title, variants.*
|
|
|
|
// Custom fields selection
|
|
GET /store/products?fields=id,title,description
|
|
// Returns: only id, title, description
|
|
|
|
// Pagination
|
|
GET /store/products?limit=10&offset=20
|
|
// Returns: 10 items, skipping first 20
|
|
|
|
// Ordering
|
|
GET /store/products?order=title
|
|
// Returns: products ordered by title ascending
|
|
|
|
GET /store/products?order=-created_at
|
|
// Returns: products ordered by created_at descending (- prefix)
|
|
|
|
// Combined
|
|
GET /store/products?fields=id,title,brand.*&limit=5&order=-created_at
|
|
// Returns: 5 items with custom fields, newest first
|
|
```
|
|
|
|
### Advanced: Custom Query Param + Query Config
|
|
|
|
You can combine custom query parameters with query config:
|
|
|
|
```typescript
|
|
// validators.ts
|
|
import { z } from "zod"
|
|
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
|
|
|
|
export const GetProductsSchema = createFindParams().merge(
|
|
z.object({
|
|
category_id: z.string().optional(),
|
|
in_stock: z.preprocess(
|
|
(val) => val === "true",
|
|
z.boolean().optional()
|
|
),
|
|
})
|
|
)
|
|
```
|
|
|
|
```typescript
|
|
// route.ts
|
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
const query = req.scope.resolve("query")
|
|
const { category_id, in_stock } = req.validatedQuery
|
|
|
|
const filters: any = {}
|
|
if (category_id) filters.category_id = category_id
|
|
if (in_stock !== undefined) filters.in_stock = in_stock
|
|
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
filters,
|
|
...req.queryConfig, // Still get fields, pagination, order
|
|
})
|
|
|
|
return res.json({ products })
|
|
}
|
|
```
|
|
|
|
## Import Organization
|
|
|
|
**⚠️ CRITICAL**: Always import workflows, modules, and other dependencies at the TOP of the file, never inside the route handler function body.
|
|
|
|
### ✅ CORRECT - Imports at Top
|
|
|
|
```typescript
|
|
// api/store/reviews/route.ts
|
|
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
import { createReviewWorkflow } from "../../../workflows/create-review"
|
|
import { CreateReviewSchema } from "./middlewares"
|
|
|
|
export async function POST(
|
|
req: MedusaRequest<CreateReviewSchema>,
|
|
res: MedusaResponse
|
|
) {
|
|
const { result } = await createReviewWorkflow(req.scope).run({
|
|
input: req.validatedBody
|
|
})
|
|
|
|
return res.json({ review: result })
|
|
}
|
|
```
|
|
|
|
### ❌ WRONG - Dynamic Imports in Route Body
|
|
|
|
```typescript
|
|
// api/store/reviews/route.ts
|
|
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
|
|
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
// ❌ WRONG: Don't use dynamic imports in route handlers
|
|
const { createReviewWorkflow } = await import("../../../workflows/create-review")
|
|
|
|
const { result } = await createReviewWorkflow(req.scope).run({
|
|
input: req.validatedBody
|
|
})
|
|
|
|
return res.json({ review: result })
|
|
}
|
|
```
|
|
|
|
**Why this matters:**
|
|
- Dynamic imports add unnecessary overhead to every request
|
|
- Makes code harder to read and maintain
|
|
- Breaks static analysis and TypeScript checking
|
|
- Can cause module resolution issues in production
|
|
|
|
## API Route Structure
|
|
|
|
**⚠️ IMPORTANT**: Medusa uses only GET, POST and DELETE as a convention.
|
|
- **GET** for reads
|
|
- **POST** for mutations (create/update)
|
|
- **DELETE** for deletions
|
|
|
|
Don't use PUT or PATCH.
|
|
|
|
### Basic API Route
|
|
|
|
```typescript
|
|
// api/store/my-route/route.ts
|
|
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
import { MedusaError } from "@medusajs/framework/utils"
|
|
|
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
const query = req.scope.resolve("query")
|
|
|
|
// Query data
|
|
const { data: items } = await query.graph({
|
|
entity: "entity_name",
|
|
fields: ["id", "name"],
|
|
})
|
|
|
|
return res.status(200).json({ items })
|
|
}
|
|
|
|
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
const { field } = req.validatedBody
|
|
|
|
// Execute workflow (mutations should always use workflows)
|
|
const { result } = await myWorkflow(req.scope).run({
|
|
input: { field },
|
|
})
|
|
|
|
return res.status(200).json({ result })
|
|
}
|
|
```
|
|
|
|
### Accessing Request Data
|
|
|
|
```typescript
|
|
// Validated body (from middleware)
|
|
const { email, name } = req.validatedBody
|
|
|
|
// Query parameters
|
|
const { page, limit } = req.query
|
|
|
|
// Route parameters
|
|
const { id } = req.params
|
|
|
|
// Resolve services
|
|
const query = req.scope.resolve("query")
|
|
const myService = req.scope.resolve("my-module")
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
Use `MedusaError` for consistent error responses:
|
|
|
|
```typescript
|
|
import { MedusaError } from "@medusajs/framework/utils"
|
|
|
|
// Not found
|
|
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Resource not found")
|
|
|
|
// Invalid data
|
|
throw new MedusaError(MedusaError.Types.INVALID_DATA, "Invalid input provided")
|
|
|
|
// Unauthorized
|
|
throw new MedusaError(MedusaError.Types.UNAUTHORIZED, "Authentication required")
|
|
|
|
// Conflict
|
|
throw new MedusaError(MedusaError.Types.CONFLICT, "Resource already exists")
|
|
|
|
// Other types: INVALID_STATE, NOT_ALLOWED, DUPLICATE_ERROR
|
|
```
|
|
|
|
### Error Response Format
|
|
|
|
Medusa automatically formats errors:
|
|
|
|
```json
|
|
{
|
|
"type": "not_found",
|
|
"message": "Resource not found"
|
|
}
|
|
```
|
|
|
|
## Protected Routes
|
|
|
|
### Default Protected Routes
|
|
|
|
All routes under these prefixes are automatically protected:
|
|
- `/admin/*` - Requires authenticated admin user
|
|
- `/store/customers/me/*` - Requires authenticated customer
|
|
|
|
### Custom Protected Routes
|
|
|
|
To protect routes under different prefixes, use the `authenticate` middleware:
|
|
|
|
```typescript
|
|
// api/middlewares.ts
|
|
import {
|
|
defineMiddlewares,
|
|
authenticate,
|
|
} from "@medusajs/framework/http"
|
|
|
|
export default defineMiddlewares({
|
|
routes: [
|
|
// Only allow authenticated admin users
|
|
{
|
|
matcher: "/custom/admin*",
|
|
middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
|
|
},
|
|
// Only allow authenticated customers
|
|
{
|
|
matcher: "/store/reviews*",
|
|
middlewares: [authenticate("customer", ["session", "bearer"])],
|
|
},
|
|
],
|
|
})
|
|
```
|
|
|
|
### Accessing Authenticated User
|
|
|
|
**⚠️ CRITICAL**: For routes protected with `authenticate` middleware, you MUST use `AuthenticatedMedusaRequest` instead of `MedusaRequest` to avoid type errors when accessing `req.auth_context.actor_id`.
|
|
|
|
```typescript
|
|
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
|
|
// ✅ CORRECT - Use AuthenticatedMedusaRequest for protected routes
|
|
export async function POST(
|
|
req: AuthenticatedMedusaRequest,
|
|
res: MedusaResponse
|
|
) {
|
|
// For admin routes
|
|
const userId = req.auth_context.actor_id // Admin user ID
|
|
|
|
// For customer routes
|
|
const customerId = req.auth_context.actor_id // Customer ID
|
|
|
|
// Your logic here
|
|
}
|
|
|
|
// ❌ WRONG - Don't use MedusaRequest for protected routes
|
|
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
const userId = req.auth_context.actor_id // Type error!
|
|
}
|
|
```
|
|
|
|
**See [authentication.md](authentication.md) for complete authentication patterns.**
|
|
|
|
## Using Workflows in API Routes
|
|
|
|
**⚠️ BEST PRACTICE**: Workflows are the standard way to perform mutations (create, update, delete) in Medusa. API routes should execute workflows and return their response.
|
|
|
|
### Example: Create Workflow
|
|
|
|
```typescript
|
|
import { createCustomersWorkflow } from "@medusajs/medusa/core-flows"
|
|
|
|
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
const { email } = req.validatedBody
|
|
|
|
const { result } = await createCustomersWorkflow(req.scope).run({
|
|
input: {
|
|
customersData: [
|
|
{
|
|
email,
|
|
has_account: false,
|
|
},
|
|
],
|
|
},
|
|
})
|
|
|
|
return res.json({ customer: result[0] })
|
|
}
|
|
```
|
|
|
|
### Example: Custom Workflow
|
|
|
|
```typescript
|
|
import { myCustomWorkflow } from "../../workflows/my-workflow"
|
|
|
|
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
const { data } = req.validatedBody
|
|
|
|
try {
|
|
const { result } = await myCustomWorkflow(req.scope).run({
|
|
input: { data },
|
|
})
|
|
|
|
return res.json({ result })
|
|
} catch (error) {
|
|
throw new MedusaError(
|
|
MedusaError.Types.INVALID_DATA,
|
|
error.message
|
|
)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Common Built-in Workflows
|
|
|
|
Ask MedusaDocs for specific workflow names and their input parameters:
|
|
- Customer workflows: create, update, delete customers
|
|
- Product workflows: create, update, delete products
|
|
- Order workflows: create, cancel, fulfill orders
|
|
- Cart workflows: create, update, complete carts
|
|
- And many more...
|
|
|
|
## API Route Organization
|
|
|
|
Organize routes by feature or domain:
|
|
|
|
```
|
|
src/api/
|
|
├── admin/
|
|
│ ├── custom-reports/
|
|
│ │ ├── route.ts
|
|
│ │ └── middlewares.ts
|
|
│ └── bulk-operations/
|
|
│ ├── route.ts
|
|
│ └── middlewares.ts
|
|
└── store/
|
|
├── newsletter/
|
|
│ ├── route.ts
|
|
│ └── middlewares.ts
|
|
└── reviews/
|
|
├── route.ts
|
|
├── [id]/
|
|
│ └── route.ts
|
|
└── middlewares.ts
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### Pattern: List with Query Config (Recommended)
|
|
|
|
```typescript
|
|
// middlewares.ts
|
|
import { validateAndTransformQuery } from "@medusajs/framework/http"
|
|
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
|
|
|
|
export const GetMyEntitiesSchema = createFindParams()
|
|
|
|
export default defineMiddlewares({
|
|
routes: [
|
|
{
|
|
matcher: "/store/my-entities",
|
|
method: "GET",
|
|
middlewares: [
|
|
validateAndTransformQuery(GetMyEntitiesSchema, {
|
|
defaults: ["id", "name", "created_at"],
|
|
isList: true,
|
|
defaultLimit: 15,
|
|
}),
|
|
],
|
|
},
|
|
],
|
|
})
|
|
|
|
// route.ts
|
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
const query = req.scope.resolve("query")
|
|
|
|
const { data, metadata } = await query.graph({
|
|
entity: "my_entity",
|
|
...req.queryConfig, // Handles fields, pagination automatically
|
|
})
|
|
|
|
return res.json({
|
|
items: data,
|
|
count: metadata.count,
|
|
limit: req.queryConfig.pagination.take,
|
|
offset: req.queryConfig.pagination.skip,
|
|
})
|
|
}
|
|
```
|
|
|
|
### Pattern: Retrieve Single Resource with Relations
|
|
|
|
```typescript
|
|
// For single resource endpoints, you can still use query config
|
|
// middlewares.ts
|
|
export const GetMyEntitySchema = createFindParams()
|
|
|
|
export default defineMiddlewares({
|
|
routes: [
|
|
{
|
|
matcher: "/store/my-entities/:id",
|
|
method: "GET",
|
|
middlewares: [
|
|
validateAndTransformQuery(GetMyEntitySchema, {
|
|
defaults: ["id", "name", "variants.*", "brand.*"],
|
|
isList: false, // Single resource
|
|
}),
|
|
],
|
|
},
|
|
],
|
|
})
|
|
|
|
// route.ts
|
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
const query = req.scope.resolve("query")
|
|
const { id } = req.params
|
|
|
|
const { data } = await query.graph({
|
|
entity: "my_entity",
|
|
filters: { id },
|
|
...req.queryConfig,
|
|
})
|
|
|
|
if (!data || data.length === 0) {
|
|
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Resource not found")
|
|
}
|
|
|
|
return res.json({ item: data[0] })
|
|
}
|
|
```
|
|
|
|
### Pattern: Search with Custom Filters + Query Config
|
|
|
|
```typescript
|
|
// validators.ts
|
|
export const GetMyEntitiesSchema = createFindParams().merge(
|
|
z.object({
|
|
q: z.string().optional(), // Search query
|
|
status: z.enum(["active", "pending", "completed"]).optional(),
|
|
})
|
|
)
|
|
|
|
// middlewares.ts
|
|
export default defineMiddlewares({
|
|
routes: [
|
|
{
|
|
matcher: "/store/my-entities",
|
|
method: "GET",
|
|
middlewares: [
|
|
validateAndTransformQuery(GetMyEntitiesSchema, {
|
|
defaults: ["id", "name", "status"],
|
|
isList: true,
|
|
}),
|
|
],
|
|
},
|
|
],
|
|
})
|
|
|
|
// route.ts
|
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
const query = req.scope.resolve("query")
|
|
const { q, status } = req.validatedQuery
|
|
|
|
const filters: any = {}
|
|
|
|
if (q) {
|
|
filters.name = { $like: `%${q}%` }
|
|
}
|
|
|
|
if (status) {
|
|
filters.status = status
|
|
}
|
|
|
|
const { data } = await query.graph({
|
|
entity: "my_entity",
|
|
filters,
|
|
...req.queryConfig, // Client can still control fields, pagination
|
|
})
|
|
|
|
return res.json({ items: data })
|
|
}
|
|
```
|
|
|
|
### Pattern: Manual Query (When Query Config Not Needed)
|
|
|
|
For simple queries where you don't need client-controlled fields/pagination:
|
|
|
|
```typescript
|
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
const query = req.scope.resolve("query")
|
|
|
|
const { data } = await query.graph({
|
|
entity: "my_entity",
|
|
fields: ["id", "name"],
|
|
filters: { status: "active" },
|
|
pagination: {
|
|
take: 10,
|
|
skip: 0,
|
|
},
|
|
})
|
|
|
|
return res.json({ items: data })
|
|
}
|
|
```
|