docs: add new API route guides + fixes and improvements to existing ones (#8987)
- Add new documentation pages for API route intro, responses, errors, and validation - General fixes and improvements across existing API route guides > Note: I didn't work on the guide for additional data validation. Will work on this as part of my next focus on workflows.
This commit is contained in:
@@ -14,7 +14,9 @@ For example, if you allow only origins starting with `http://localhost:7001` to
|
||||
|
||||
### CORS Configurations
|
||||
|
||||
You configure allowed origins for Store and Admin API Routes using the `storeCors` and `adminCors` properties of the `http` configuration in `medusa-config.js`. Each of these configurations accepts a URL pattern to identify allowed origins.
|
||||
The `storeCors` and `adminCors` properties of Medusa's `http` configuration set the allowed origins for routes starting with `/store` and `/admin` respectively.
|
||||
|
||||
These configurations accept a URL pattern to identify allowed origins.
|
||||
|
||||
For example:
|
||||
|
||||
@@ -42,8 +44,6 @@ Learn more about the CORS configurations in [this resource guide](!resources!/re
|
||||
|
||||
## CORS in Store and Admin Routes
|
||||
|
||||
Medusa applies the CORS middleware with the specified configurations in `medusa-config.js` on all routes starting with `/store` and `/admin`.
|
||||
|
||||
To disable the CORS middleware for a route, export a `CORS` variable in the route file with its value set to `false`.
|
||||
|
||||
For example:
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
import { Table } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Throwing and Handling Errors`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this guide, you'll learn how to throw errors in your Medusa application, how it affects an API route's response, and how to change the default error handler of your Medusa application.
|
||||
|
||||
## Throw MedusaError
|
||||
|
||||
When throwing an error in your API routes, middlewares, workflows, or any customization, throw a `MedusaError`, which is imported from `@medusajs/utils`.
|
||||
|
||||
The Medusa application's API route error handler then wraps your thrown error in a uniform object and returns it in the response.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
if (!req.query.q) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"The `q` query parameter is required."
|
||||
)
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The `MedusaError` class accepts in its constructor two parameters:
|
||||
|
||||
1. The first is the error's type. `MedusaError` has a static property `Types` that you can use. `Types` is an enum whose possible values are explained in the next section.
|
||||
2. The second is the message to show in the error response.
|
||||
|
||||
### Error Object in Response
|
||||
|
||||
The error object returned in the response has two properties:
|
||||
|
||||
- `type`: The error's type.
|
||||
- `message`: The error message, if available.
|
||||
- `code`: A common snake-case code. Its values can be:
|
||||
- `invalid_request_error` for the `DUPLICATE_ERROR` type.
|
||||
- `api_error`: for the `DB_ERROR` type.
|
||||
- `invalid_state_error` for `CONFLICT` error type.
|
||||
- `unknown_error` for any unidentified error type.
|
||||
- For other error types, this property won't be available unless you provide a code as a third parameter to the `MedusaError` constructor.
|
||||
|
||||
### MedusaError Types
|
||||
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Type</Table.HeaderCell>
|
||||
<Table.HeaderCell>Description</Table.HeaderCell>
|
||||
<Table.HeaderCell className="w-1/5">Status Code</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`DB_ERROR`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates a database error.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`500`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`DUPLICATE_ERROR`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates a duplicate of a record already exists. For example, when trying to create a customer whose email is registered by another customer.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`422`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`INVALID_ARGUMENT` and `UNEXPECTED_STATE`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates an error that occurred due to incorrect arguments or other unexpected state.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`500`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`INVALID_DATA`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates a validation error.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`400`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`UNAUTHORIZED`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates that a user is not authorized to perform an action or access a route.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`401`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`NOT_FOUND`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates that the requested resource, such as a route or a record, isn't found.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`404`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`NOT_ALLOWED`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates that an operation isn't allowed.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`400`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`CONFLICT`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates that a request conflicts with another previous or ongoing request. The error message in this case is ignored for a default message.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`409`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`PAYMENT_AUTHORIZATION_ERROR`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates an error has occurred while authorizing a payment.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`422`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
Other error types
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Any other error type results in an `unknown_error` code and message.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`500`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
|
||||
---
|
||||
|
||||
## Override Error Handler
|
||||
|
||||
The `defineMiddlewares` function used to apply middlewares on routes accepts an `errorHandler` in its object parameter. Use it to override the default error handler for API routes.
|
||||
|
||||
<Note>
|
||||
|
||||
This error handler will also be used for errors thrown in Medusa's API routes and resources.
|
||||
|
||||
</Note>
|
||||
|
||||
For example, create `src/api/middlewares.ts` with the following:
|
||||
|
||||
```ts title="src/api/middlewares.ts" collapsibleLines="1-8" expandMoreLabel="Show Imports"
|
||||
import {
|
||||
defineMiddlewares,
|
||||
MedusaNextFunction,
|
||||
MedusaRequest,
|
||||
MedusaResponse
|
||||
} from "@medusajs/medusa"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
|
||||
export default defineMiddlewares({
|
||||
errorHandler: (
|
||||
error: MedusaError | any,
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse,
|
||||
next: MedusaNextFunction
|
||||
) => {
|
||||
res.status(400).json({
|
||||
error: "Something happened."
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
The `errorHandler` property's value is a function that accepts four parameters:
|
||||
|
||||
1. The error thrown. Its type can be `MedusaError` or any other thrown error type.
|
||||
2. A request object of type `MedusaRequest`.
|
||||
3. A response object of type `MedusaResponse`.
|
||||
4. A function of type MedusaNextFunction that executes the next middleware in the stack.
|
||||
|
||||
This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message.
|
||||
@@ -20,7 +20,7 @@ import type {
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export const GET = (
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
@@ -29,7 +29,7 @@ export const GET = (
|
||||
})
|
||||
}
|
||||
|
||||
export const POST = (
|
||||
export const POST = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
|
||||
@@ -10,6 +10,14 @@ In this chapter, you’ll learn about middlewares and how to create them.
|
||||
|
||||
A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler function.
|
||||
|
||||
Middlwares are used to guard API routes, parse request content types other than `application/json`, manipulate request data, and more.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
As Medusa's server is based on Express, you can use any [Express middleware](https://expressjs.com/en/resources/middleware.html).
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Middleware?
|
||||
@@ -55,48 +63,6 @@ In the example above, you define a middleware that logs the message `Received a
|
||||
|
||||
---
|
||||
|
||||
## Request URLs with Trailing Backslashes
|
||||
|
||||
A middleware whose `matcher` pattern doesn't end with a backslash won't be applied for requests to URLs with a trailing backslash.
|
||||
|
||||
For example, consider you have the following middleware:
|
||||
|
||||
```ts
|
||||
import { defineMiddlewares } from "@medusajs/medusa"
|
||||
import type {
|
||||
MedusaNextFunction,
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export default defineMiddlewares({
|
||||
routes: [
|
||||
{
|
||||
matcher: "/custom",
|
||||
middlewares: [
|
||||
(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse,
|
||||
next: MedusaNextFunction
|
||||
) => {
|
||||
console.log("Received a request!")
|
||||
|
||||
next()
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
If you send a request to `http://localhost:9000/custom`, the middleware will run.
|
||||
|
||||
However, if you send a request to `http://localhost:9000/custom/`, the middleware won't run.
|
||||
|
||||
In general, avoid adding trailing backslashes when sending requests to API routes.
|
||||
|
||||
---
|
||||
|
||||
## Test the Middleware
|
||||
|
||||
To test the middleware:
|
||||
@@ -143,9 +109,41 @@ You must call the `next` function in the middleware. Otherwise, other middleware
|
||||
|
||||
---
|
||||
|
||||
## Middleware for Routes with Path Parameters
|
||||
|
||||
To indicate a path parameter in a middleware's `matcher` pattern, use the format `:{param-name}`.
|
||||
|
||||
For example:
|
||||
|
||||
export const pathParamHighlights = [["11", ":id", "Indicates that the API route accepts an `id` path parameter."]]
|
||||
|
||||
```ts title="src/api/middlewares.ts" collapsibleLines="1-7" expandMoreLabel="Show Imports" highlights={pathParamHighlights}
|
||||
import { defineMiddlewares } from "@medusajs/medusa"
|
||||
import type {
|
||||
MedusaNextFunction,
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export default defineMiddlewares({
|
||||
routes: [
|
||||
{
|
||||
matcher: "/store/custom/:id",
|
||||
middlewares: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
This applies a middleware to the routes defined in the file `src/api/store/custom/[id]/route.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Restrict HTTP Methods
|
||||
|
||||
In addition to the `matcher` configuration, you can restrict which HTTP methods the middleware is applied to.
|
||||
Restrict which HTTP methods the middleware is applied to using the `method` property of the middleware route object.
|
||||
|
||||
For example:
|
||||
|
||||
@@ -164,6 +162,38 @@ export default defineMiddlewares({
|
||||
{
|
||||
matcher: "/store*",
|
||||
method: ["POST", "PUT"],
|
||||
middlewares: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
`method`'s value is one or more HTTP methods to apply the middleware to.
|
||||
|
||||
This example applies the middleware only when a `POST` or `PUT` request is sent to an API route path starting with `/store`.
|
||||
|
||||
---
|
||||
|
||||
## Request URLs with Trailing Backslashes
|
||||
|
||||
A middleware whose `matcher` pattern doesn't end with a backslash won't be applied for requests to URLs with a trailing backslash.
|
||||
|
||||
For example, consider you have the following middleware:
|
||||
|
||||
```ts collapsibleLines="1-7" expandMoreLabel="Show Imports"
|
||||
import { defineMiddlewares } from "@medusajs/medusa"
|
||||
import type {
|
||||
MedusaNextFunction,
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export default defineMiddlewares({
|
||||
routes: [
|
||||
{
|
||||
matcher: "/custom",
|
||||
middlewares: [
|
||||
(
|
||||
req: MedusaRequest,
|
||||
@@ -180,6 +210,8 @@ export default defineMiddlewares({
|
||||
})
|
||||
```
|
||||
|
||||
The object in the `routes` array accepts the property `method` whose value is one or more HTTP methods to apply the middleware to.
|
||||
If you send a request to `http://localhost:9000/custom`, the middleware will run.
|
||||
|
||||
This applies the middleware only when a `POST` or `PUT` request is sent to an API route path starting with `/store`.
|
||||
However, if you send a request to `http://localhost:9000/custom/`, the middleware won't run.
|
||||
|
||||
In general, avoid adding trailing backslashes when sending requests to API routes.
|
||||
@@ -0,0 +1,14 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} API Routes Advanced Guides`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you'll focus more on API routes to learn about topics such as:
|
||||
|
||||
- Creating API routes for different HTTP methods.
|
||||
- Accepting parameters in your API routes.
|
||||
- Formatting response data and headers.
|
||||
- Applying middlewares on API routes.
|
||||
- Validating request body parameters.
|
||||
- Protecting API routes by requiring user authentication.
|
||||
@@ -10,7 +10,7 @@ In this chapter, you’ll learn about path, query, and request body parameters.
|
||||
|
||||
To create an API route that accepts a path parameter, create a directory within the route file's path whose name is of the format `[param]`.
|
||||
|
||||
For example, to create an API Route at the path `/hello-world/{id}`, where `{id}` is a path parameter, create the file `src/api/store/hello-world/[id]/route.ts` with the following content:
|
||||
For example, to create an API Route at the path `/hello-world/:id`, where `:id` is a path parameter, create the file `src/api/store/hello-world/[id]/route.ts` with the following content:
|
||||
|
||||
export const singlePathHighlights = [
|
||||
["11", "req.params.id", "Access the path parameter `id`"]
|
||||
@@ -22,7 +22,7 @@ import type {
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export const GET = (
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
@@ -38,7 +38,7 @@ The `MedusaRequest` object has a `params` property. `params` holds the path para
|
||||
|
||||
To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`.
|
||||
|
||||
For example, create an API route at `src/api/store/hello-world/[id]/name/[name]/route.ts`:
|
||||
For example, to create an API route at `/store/hello-world/:id/name/:name`, create the file `src/api/store/hello-world/[id]/name/[name]/route.ts` with the following content:
|
||||
|
||||
export const multiplePathHighlights = [
|
||||
["12", "req.params.id", "Access the path parameter `id`"],
|
||||
@@ -51,7 +51,7 @@ import type {
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export const GET = (
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
@@ -63,13 +63,13 @@ export const GET = (
|
||||
}
|
||||
```
|
||||
|
||||
This API route expects two path parameters: `id` and `name`.
|
||||
You access the `id` and `name` path parameters using the `req.params` property.
|
||||
|
||||
---
|
||||
|
||||
## Query Parameters
|
||||
|
||||
You can access all query parameters in the `query` property of the `MedusaRequest` object.
|
||||
You can access all query parameters in the `query` property of the `MedusaRequest` object. `query` is an object of key-value pairs, where the key is a query parameter's name, and the value is its value.
|
||||
|
||||
For example:
|
||||
|
||||
@@ -83,16 +83,18 @@ import type {
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export async function GET(
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
) => {
|
||||
res.json({
|
||||
message: `Hello ${req.query.name}`,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The value of `req.query.name` is the value passed in `?name=John`, for example.
|
||||
|
||||
---
|
||||
|
||||
## Request Body Parameters
|
||||
@@ -116,7 +118,7 @@ type HelloWorldReq = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export const POST = (
|
||||
export const POST = async (
|
||||
req: MedusaRequest<HelloWorldReq>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
@@ -128,6 +130,12 @@ export const POST = (
|
||||
|
||||
In this example, you use the `name` request body parameter to create the message in the returned response.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
The `MedusaRequest` type accepts a type argument that indicates the type of the request body. This is useful for auto-completion and to avoid typing errors.
|
||||
|
||||
</Note>
|
||||
|
||||
To test it out, send the following request to your Medusa application:
|
||||
|
||||
```bash apiTesting testApiUrl="http://localhost:9000/store/hello-world" testApiMethod="POST" testBodyParams={{ "name": "" }}
|
||||
|
||||
@@ -14,12 +14,11 @@ A protected route is a route that requires requests to be user-authenticated bef
|
||||
|
||||
## Default Protected Routes
|
||||
|
||||
Medusa applies an authentication guard on the following routes:
|
||||
Medusa applies an authentication guard on routes starting with `/admin`, including custom API routes.
|
||||
|
||||
- Routes starting with `/admin` require an authenticated admin user.
|
||||
- Routes starting with `/store/customers/me` require an authenticated customer.
|
||||
Requests to `/admin` must be user-authenticated to access the route.
|
||||
|
||||
<Note>
|
||||
<Note title="Tip">
|
||||
|
||||
Refer to the API Reference for [Admin](https://docs.medusajs.com/api/admin#authentication) and [Store](https://docs.medusajs.com/api/store#authentication) authentication methods.
|
||||
|
||||
@@ -27,102 +26,9 @@ Refer to the API Reference for [Admin](https://docs.medusajs.com/api/admin#authe
|
||||
|
||||
---
|
||||
|
||||
## Authentication Opt-Out
|
||||
|
||||
To disable the authentication guard on custom routes under the `/admin` or `/store/customers/me` path prefixes, export an `AUTHENTICATE` variable in the route file with its value set to `false`.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/api/store/customers/me/custom/route.ts" highlights={[["12"]]} apiTesting testApiUrl="http://localhost:9000/store/customers/me/custom" testApiMethod="GET"
|
||||
import type { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: "Hello",
|
||||
})
|
||||
}
|
||||
|
||||
export const AUTHENTICATE = false
|
||||
```
|
||||
|
||||
Now, any request sent to the `/store/customers/me/custom` API route is allowed, regardless if the customer is authenticated.
|
||||
|
||||
---
|
||||
|
||||
## Access Logged-In Customer
|
||||
|
||||
You can access the logged-in customer’s ID in all API routes starting with `/store` using the `auth_context.actor_id` property of the `MedusaRequest` object.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/api/store/customers/me/custom/route.ts" highlights={[["17", "req.auth_context.actor_id", "Access the logged-in customer's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { ModuleRegistrationName } from "@medusajs/utils"
|
||||
import { ICustomerModuleService } from "@medusajs/types"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const customerModuleService: ICustomerModuleService = req.scope.resolve(
|
||||
ModuleRegistrationName.CUSTOMER
|
||||
)
|
||||
|
||||
const customer = await customerModuleService.retrieveCustomer(
|
||||
req.auth_context.actor_id
|
||||
)
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
In this example, you resolve the Customer Module's main service, then use it to retrieve the logged-in customer, if available.
|
||||
|
||||
---
|
||||
|
||||
## Access Logged-In Admin User
|
||||
|
||||
You can access the logged-in admin user’s ID in all API Routes starting with `/admin` using the `auth_context.actor_id` property of the `MedusaRequest` object.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/api/admin/custom/route.ts" highlights={[["17", "req.auth_context.actor_id", "Access the logged-in admin user's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { ModuleRegistrationName } from "@medusajs/utils"
|
||||
import { IUserModuleService } from "@medusajs/types"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const userModuleService: IUserModuleService = req.scope.resolve(
|
||||
ModuleRegistrationName.USER
|
||||
)
|
||||
|
||||
const user = await userModuleService.retrieveUser(
|
||||
req.auth_context.actor_id
|
||||
)
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
In the route handler, you resolve the User Module's main service, then use it to retrieve the logged-in admin user.
|
||||
|
||||
---
|
||||
|
||||
## Protect Custom API Routes
|
||||
|
||||
To protect custom API Routes that don’t start with `/store/customers/me` or `/admin`, use the `authenticate` middleware imported from `@medusajs/medusa`.
|
||||
To protect custom API Routes to only allow authenticated customer or admin users, use the `authenticate` middleware imported from `@medusajs/medusa`.
|
||||
|
||||
For example:
|
||||
|
||||
@@ -165,3 +71,106 @@ The `authenticate` middleware function accepts three parameters:
|
||||
2. An array of the types of authentication methods allowed. Both `user` and `customer` scopes support `session` and `bearer`. The `admin` scope also supports the `api-key` authentication method.
|
||||
3. An optional object of configurations accepting the following property:
|
||||
- `allowUnauthenticated`: (default: `false`) A boolean indicating whether authentication is required. For example, you may have an API route where you want to access the logged-in customer if available, but guest customers can still access it too.
|
||||
|
||||
---
|
||||
|
||||
## Authentication Opt-Out
|
||||
|
||||
To disable the authentication guard on custom routes under the `/admin` path prefix, export an `AUTHENTICATE` variable in the route file with its value set to `false`.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/api/admin/custom/route.ts" highlights={[["15"]]}
|
||||
import type {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: "Hello",
|
||||
})
|
||||
}
|
||||
|
||||
export const AUTHENTICATE = false
|
||||
```
|
||||
|
||||
Now, any request sent to the `/admin/custom` API route is allowed, regardless if the admin user is authenticated.
|
||||
|
||||
---
|
||||
|
||||
## Authenticated Request Type
|
||||
|
||||
To access the authentication details in an API route, such as the logged-in user's ID, set the type of the first request parameter to `AuthenticatedMedusaRequest`. It extends `MedusaRequest`.
|
||||
|
||||
The `auth_context.actor_id` property of `AuthenticatedMedusaRequest` holds the ID of the authenticated user or customer. If there isn't any authenticated user or customer, `auth_context` is `undefined`.
|
||||
|
||||
### Retrieve Logged-In Customer's Details
|
||||
|
||||
You can access the logged-in customer’s ID in all API routes starting with `/store` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/api/store/custom/route.ts" highlights={[["19", "req.auth_context.actor_id", "Access the logged-in customer's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { ModuleRegistrationName } from "@medusajs/utils"
|
||||
import { ICustomerModuleService } from "@medusajs/types"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
if (req.auth_context?.actor_id) {
|
||||
// retrieve customer
|
||||
const customerModuleService: ICustomerModuleService = req.scope.resolve(
|
||||
ModuleRegistrationName.CUSTOMER
|
||||
)
|
||||
|
||||
const customer = await customerModuleService.retrieveCustomer(
|
||||
req.auth_context.actor_id
|
||||
)
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
In this example, you resolve the Customer Module's main service, then use it to retrieve the logged-in customer, if available.
|
||||
|
||||
### Retrieve Logged-In Admin User's Details
|
||||
|
||||
You can access the logged-in admin user’s ID in all API Routes starting with `/admin` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/api/admin/custom/route.ts" highlights={[["17", "req.auth_context.actor_id", "Access the logged-in admin user's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { ModuleRegistrationName } from "@medusajs/utils"
|
||||
import { IUserModuleService } from "@medusajs/types"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const userModuleService: IUserModuleService = req.scope.resolve(
|
||||
ModuleRegistrationName.USER
|
||||
)
|
||||
|
||||
const user = await userModuleService.retrieveUser(
|
||||
req.auth_context.actor_id
|
||||
)
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
In the route handler, you resolve the User Module's main service, then use it to retrieve the logged-in admin user.
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} API Route Response`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to send a response in your API route.
|
||||
|
||||
## Send a JSON Response
|
||||
|
||||
To send a JSON response, use the `json` method of the `MedusaResponse` object passed as the second parameter of your API route handler.
|
||||
|
||||
For example:
|
||||
|
||||
export const jsonHighlights = [
|
||||
["7", "json", "Return a JSON object."]
|
||||
]
|
||||
|
||||
```ts title="src/api/store/custom/route.ts" highlights={jsonHighlights} apiTesting testApiUrl="http://localhost:9000/store/custom" testApiMethod="GET"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa";
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: "Hello, World!"
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This API route returns the following JSON object:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Hello, World!"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Set Response Status Code
|
||||
|
||||
By default, setting the JSON data using the `json` method returns a response with a `200` status code.
|
||||
|
||||
To change the status code, use the `status` method of the `MedusaResponse` object.
|
||||
|
||||
For example:
|
||||
|
||||
export const statusHighlight = [
|
||||
["7", "status", "Set the response code to `201`."]
|
||||
]
|
||||
|
||||
```ts title="src/api/store/custom/route.ts" highlights={statusHighlight} apiTesting testApiUrl="http://localhost:9000/store/custom" testApiMethod="GET"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa";
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.status(201).json({
|
||||
message: "Hello, World!"
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The response of this API route has the status code `201`.
|
||||
|
||||
---
|
||||
|
||||
## Change Response Content Type
|
||||
|
||||
To return response data other than a JSON object, use the `writeHead` method of the `MedusaResponse` object. It allows you to set the response headers, including the content type.
|
||||
|
||||
For example, to create an API route that returns an event stream:
|
||||
|
||||
export const streamHighlights = [
|
||||
["7", "writeHead", "Set the response's headers."],
|
||||
["7", "200", "Set the status code."],
|
||||
["8", `"Content-Type"`, "Set the response's content type."],
|
||||
["13", "interval", "Simulate stream data using an interval"],
|
||||
["14", "write", "Write stream data."],
|
||||
["17", "on", "Stop the stream when the request is terminated."]
|
||||
]
|
||||
|
||||
```ts highlights={streamHighlights}
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa";
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
})
|
||||
|
||||
const interval = setInterval(() => {
|
||||
res.write("Streaming data...\n")
|
||||
}, 3000)
|
||||
|
||||
req.on("end", () => {
|
||||
clearInterval(interval)
|
||||
res.end()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The `writeHead` method accepts two parameters:
|
||||
|
||||
1. The first one is the response's status code.
|
||||
2. The second is an object of key-value pairs to set the headers of the response.
|
||||
|
||||
This API route opens a stream by setting the `Content-Type` in the header to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds.
|
||||
|
||||
---
|
||||
|
||||
## Do More with Responses
|
||||
|
||||
The `MedusaResponse` type is based on [Express's Response](https://expressjs.com/en/api.html#res). Refer to their API reference for other uses of responses.
|
||||
@@ -0,0 +1,135 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Request Body Parameter Validation`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to validate request body parameters in your custom API route.
|
||||
|
||||
## Example Scenario
|
||||
|
||||
Consider you're creating a `POST` API route at `/store/custom`. It accepts two paramters `a` and `b` that are required numbers, and returns their sum.
|
||||
|
||||
The next steps explain how to add validation to this API route, as an example.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create Zod Schema
|
||||
|
||||
Medusa uses [Zod](https://zod.dev/) to validate the body parameters of an incoming request.
|
||||
|
||||
To use Zod to validate your custom schemas, create a `validators.ts` file in any `src/api` subfolder. This file holds Zod schemas for each of your API routes.
|
||||
|
||||
For example, create the file `src/api/store/custom/validators.ts` with the following content:
|
||||
|
||||
```ts title="src/api/store/custom/validators.ts"
|
||||
import { z } from "zod"
|
||||
|
||||
export const PostStoreCustomSchema = z.object({
|
||||
a: z.number(),
|
||||
b: z.number()
|
||||
})
|
||||
```
|
||||
|
||||
The `PostStoreCustomSchema` variable is a Zod schema that indicates the request body is valid if:
|
||||
|
||||
1. It's an object.
|
||||
2. It has a property `a` that is a required number.
|
||||
3. It has a property `b` that is a required number.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Add Validation Middleware
|
||||
|
||||
To use this schema for validating the body parameters of requests to `/store/custom`, use the `validateAndTransformBody` middleware provided by `@medusajs/medusa`. It accepts the Zod schema as a parameter.
|
||||
|
||||
For example, create the file `src/api/middlewares.ts` with the following content:
|
||||
|
||||
```ts title="src/api/middlewares.ts"
|
||||
import { defineMiddlewares } from "@medusajs/medusa"
|
||||
import {
|
||||
validateAndTransformBody
|
||||
} from "@medusajs/medusa/dist/api/utils/validate-body"
|
||||
import { PostStoreCustomSchema } from "./store/custom/validators"
|
||||
|
||||
export default defineMiddlewares({
|
||||
routes: [
|
||||
{
|
||||
matcher: "/store/custom",
|
||||
method: "POST",
|
||||
middlewares: [
|
||||
validateAndTransformBody(PostStoreCustomSchema)
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
This applies the `validateAndTransformBody` middleware on `POST` requests to `/store/custom`. It uses the `PostStoreCustomSchema` as the validation schema.
|
||||
|
||||
### How the Validation Works
|
||||
|
||||
If a request's body parameters don't pass the validation, the `validateAndTransformBody` middleware throws an error indicating the validation errors.
|
||||
|
||||
If a request's body parameters are validated successfully, the middleware sets the validated body parameters in the `validatedBody` property of `MedusaRequest`.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Use Validated Body in API Route
|
||||
|
||||
In your API route, consume the validated body using the `validatedBody` property of `MedusaRequest`.
|
||||
|
||||
For example, create the file `src/api/store/custom/route.ts` with the following content:
|
||||
|
||||
export const routeHighlights = [
|
||||
["5", "PostStoreCustomSchemaType", "Infer the request body type from the schema to pass it as a type parameter to `MedusaRequest`."],
|
||||
["14", "", "Access the body parameters using `validatedBody`."]
|
||||
]
|
||||
|
||||
```ts title="src/api/store/custom/route.ts" highlights={routeHighlights}
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa";
|
||||
import { z } from "zod"
|
||||
import { PostStoreCustomSchema } from "./validators";
|
||||
|
||||
type PostStoreCustomSchemaType = z.infer<
|
||||
typeof PostStoreCustomSchema
|
||||
>
|
||||
|
||||
export const POST = async (
|
||||
req: MedusaRequest<PostStoreCustomSchemaType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
sum: req.validatedBody.a + req.validatedBody.b
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
In the API route, you use the `validatedBody` property of `MedusaRequest` to access the values of the `a` and `b` properties.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
To pass the request body's type as a type parameter to `MedusaRequest`, use Zod's `infer` type that accepts the type of a schema as a parameter.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
To test out the validation, send a `POST` request to `/store/custom`. You can try sending incorrect request body parameters.
|
||||
|
||||
For example, if you omit the `a` parameter, you'll receive a `400` response code with the following response data:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "invalid_data",
|
||||
"message": "Invalid request: Field 'a' is required"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Learn More About Validation Schemas
|
||||
|
||||
To see different examples and learn more about creating a validation schema, refer to [Zod's documentation](https://zod.dev).
|
||||
@@ -18,8 +18,6 @@ export const generatedEditDates = {
|
||||
"app/advanced-development/workflows/parallel-steps/page.mdx": "2024-07-31T17:01:33+03:00",
|
||||
"app/advanced-development/page.mdx": "2024-07-04T17:26:03+03:00",
|
||||
"app/first-customizations/page.mdx": "2024-05-07T18:00:28+02:00",
|
||||
"app/debugging-and-testing/page.mdx": "2024-09-02T11:06:13.298Z",
|
||||
"app/basics/medusa-container/page.mdx": "2024-08-05T07:24:27+00:00",
|
||||
"app/debugging-and-testing/page.mdx": "2024-05-03T17:36:38+03:00",
|
||||
"app/basics/medusa-container/page.mdx": "2024-09-03T07:31:40.214Z",
|
||||
"app/architectural-modules/page.mdx": "2024-07-04T17:26:03+03:00",
|
||||
@@ -35,7 +33,7 @@ export const generatedEditDates = {
|
||||
"app/advanced-development/admin/widgets/page.mdx": "2024-08-06T09:44:22+02:00",
|
||||
"app/advanced-development/data-models/page.mdx": "2024-07-04T17:26:03+03:00",
|
||||
"app/advanced-development/modules/remote-link/page.mdx": "2024-07-24T09:16:01+02:00",
|
||||
"app/advanced-development/api-routes/protected-routes/page.mdx": "2024-07-31T17:01:33+03:00",
|
||||
"app/advanced-development/api-routes/protected-routes/page.mdx": "2024-09-04T10:11:25.860Z",
|
||||
"app/advanced-development/workflows/add-workflow-hook/page.mdx": "2024-08-13T09:55:37+03:00",
|
||||
"app/advanced-development/events-and-subscribers/data-payload/page.mdx": "2024-07-16T17:12:05+01:00",
|
||||
"app/advanced-development/data-models/default-properties/page.mdx": "2024-07-02T12:34:44+03:00",
|
||||
@@ -57,12 +55,12 @@ export const generatedEditDates = {
|
||||
"app/advanced-development/modules/module-links/page.mdx": "2024-07-24T09:16:01+02:00",
|
||||
"app/advanced-development/data-models/searchable-property/page.mdx": "2024-07-04T17:26:03+03:00",
|
||||
"app/advanced-development/scheduled-jobs/execution-number/page.mdx": "2024-07-02T09:41:15+00:00",
|
||||
"app/advanced-development/api-routes/parameters/page.mdx": "2024-07-04T17:26:03+03:00",
|
||||
"app/advanced-development/api-routes/http-methods/page.mdx": "2024-07-04T17:26:03+03:00",
|
||||
"app/advanced-development/api-routes/parameters/page.mdx": "2024-09-04T08:17:50.071Z",
|
||||
"app/advanced-development/api-routes/http-methods/page.mdx": "2024-09-04T08:15:11.609Z",
|
||||
"app/advanced-development/admin/tips/page.mdx": "2024-08-05T13:20:34+03:00",
|
||||
"app/advanced-development/api-routes/cors/page.mdx": "2024-07-25T17:14:06+02:00",
|
||||
"app/advanced-development/api-routes/cors/page.mdx": "2024-09-04T08:24:47.068Z",
|
||||
"app/advanced-development/admin/ui-routes/page.mdx": "2024-08-06T09:44:22+02:00",
|
||||
"app/advanced-development/api-routes/middlewares/page.mdx": "2024-09-02T13:52:00.236Z",
|
||||
"app/advanced-development/api-routes/middlewares/page.mdx": "2024-09-04T09:45:12.441Z",
|
||||
"app/advanced-development/modules/isolation/page.mdx": "2024-07-04T17:26:03+03:00",
|
||||
"app/advanced-development/data-models/configure-properties/page.mdx": "2024-07-04T17:26:03+03:00",
|
||||
"app/advanced-development/data-models/index/page.mdx": "2024-07-04T17:26:03+03:00",
|
||||
@@ -73,5 +71,9 @@ export const generatedEditDates = {
|
||||
"app/debugging-and-testing/testing-tools/integration-tests/workflows/page.mdx": "2024-09-02T10:57:04.202Z",
|
||||
"app/debugging-and-testing/testing-tools/page.mdx": "2024-09-02T10:08:29.388Z",
|
||||
"app/debugging-and-testing/testing-tools/unit-tests/module-example/page.mdx": "2024-09-02T11:04:27.232Z",
|
||||
"app/debugging-and-testing/testing-tools/unit-tests/page.mdx": "2024-09-02T11:03:26.997Z"
|
||||
"app/debugging-and-testing/testing-tools/unit-tests/page.mdx": "2024-09-02T11:03:26.997Z",
|
||||
"app/advanced-development/api-routes/page.mdx": "2024-09-04T09:36:33.961Z",
|
||||
"app/advanced-development/api-routes/responses/page.mdx": "2024-09-04T09:40:38.986Z",
|
||||
"app/advanced-development/api-routes/validation/page.mdx": "2024-09-04T09:50:52.129Z",
|
||||
"app/advanced-development/api-routes/errors/page.mdx": "2024-09-04T11:03:55.017Z"
|
||||
}
|
||||
@@ -90,8 +90,9 @@ export const sidebar = numberSidebarItems(
|
||||
chapterTitle: "Advanced",
|
||||
children: [
|
||||
{
|
||||
type: "sub-category",
|
||||
type: "link",
|
||||
title: "API Routes",
|
||||
path: "/advanced-development/api-routes",
|
||||
children: [
|
||||
{
|
||||
type: "link",
|
||||
@@ -103,16 +104,31 @@ export const sidebar = numberSidebarItems(
|
||||
path: "/advanced-development/api-routes/parameters",
|
||||
title: "Parameters",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/advanced-development/api-routes/responses",
|
||||
title: "Response",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/advanced-development/api-routes/middlewares",
|
||||
title: "Middlewares",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/advanced-development/api-routes/validation",
|
||||
title: "Validation",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/advanced-development/api-routes/protected-routes",
|
||||
title: "Protected Routes",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/advanced-development/api-routes/errors",
|
||||
title: "Errors",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/advanced-development/api-routes/cors",
|
||||
|
||||
Reference in New Issue
Block a user