Medusa 自定义 API 路由(Custom API Routes)实战指南:文件系统路由、路径参数与中间件体系
【免费下载链接】medusaThe world's most flexible commerce platform for agents and developers项目地址: https://gitcode.com/GitHub_Trending/me/medusa
Medusa 的 API 路由(API Route)是一个 REST API 端点,它采用"文件即路由"的约定式设计:只要在应用的src/api目录下按约定命名与组织文件,就能自动注册出完整的 HTTP 接口,无需手动挂载 Express 路由。本文以 loyalty 插件 的真实实现为佐证,系统讲解路由文件的创建方式、支持的全部 HTTP 方法、路径参数、依赖注入容器(req.scope)以及middlewares.ts中间件体系的完整配置方法,并深入 framework 路由加载器 源码,揭示文件系统路由在启动时是如何被扫描、解析与注册的。读完本文,你可以独立在 Medusa 应用中编写出可用的自定义管理端、商城端乃至认证端 REST 接口。
文件系统路由:从route.ts文件到 REST 端点
Medusa 应用中的 API 路由创建在/src/api目录下的 TypeScript 或 JavaScript 文件中,文件名必须是route.ts或route.js。文件在目录中的相对位置,决定了该端点对外暴露的 URL 路径;文件中导出的函数名,则决定了它响应的 HTTP 方法。
例如,要创建一个GET /store/hello-world端点,只需创建文件src/api/store/hello-world/route.ts,内容如下:
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"; export async function GET(req: MedusaRequest, res: MedusaResponse) { res.json({ message: "Hello world!", }); }启动应用后,向/store/hello-world发起GET请求即可得到{ "message": "Hello world!" }的 JSON 响应。
从源码角度确认这一机制:框架的RoutesLoader.scanDir(routes-loader.ts)会递归扫描源目录,只挑选文件名恰好为route、扩展名为.js或.ts的文件,并把它们的相对路径转换为 URL 匹配模式,随后注册进路由表。src/api目录中以下划线_开头的目录/文件段会被跳过(routeFilePathSegment.some((segment) => segment.startsWith("_"))),可用于放置不希望暴露为路由的辅助模块。
支持的 HTTP 方法与多方法处理
基于文件的约定式路由支持以下 HTTP 方法:
- GET
- POST
- PUT
- PATCH
- DELETE
- OPTIONS
- HEAD
你可以在同一个route.ts文件中,通过导出与 HTTP 方法同名的函数,为同一路径定义多个方法的处理器:
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"; export async function GET(req: MedusaRequest, res: MedusaResponse) { // Handle GET requests } export async function POST(req: MedusaRequest, res: MedusaResponse) { // Handle POST requests } export async function PUT(req: MedusaRequest, res: MedusaResponse) { // Handle PUT requests }路由加载器在解析文件导出时,只把名字属于HTTP_METHODS列表且类型为函数的导出当作路由处理器,其余导出(如配置标志)会被忽略(routes-loader.ts)。方法集合定义在 types.ts 的HTTP_METHODS常量中,与上述 7 个方法一一对应。
路径参数:用[param]目录捕获动态片段
要创建接收路径参数(path parameter)的路由,在路由路径中创建一个名字形如[param]的目录即可。例如定义一个接收productId参数的路由,创建文件/api/products/[productId]/route.ts:
import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" export async function GET(req: MedusaRequest, res: MedusaResponse) { const { productId } = req.params; res.json({ message: `You're looking for product ${productId}` }) }路径参数会出现在req.params对象中,可直接解构使用。
若要接收多个路径参数,在文件路径中创建多个[param]目录即可。例如同时接收productId与variantId,创建文件/api/products/[productId]/variants/[variantId]/route.ts,请求/products/prod_123/variants/var_456时req.params即为{ productId: "prod_123", variantId: "var_456" }。
底层实现上,RoutesLoader.createRoutePath(routes-loader.ts)会通过正则PARAM_SEGMENT_MATCHER = /\[(\w+)\]/把[xxx]目录名转换为 Express 的:xxx参数语法,并在同一路径中发现重复参数名时直接抛出错误("Duplicate parameters found in route ..."),避免歧义。
loyalty 插件中就有一个典型的双用途参数路由:store/gift-cards/[idOrCode]/route.ts 用[idOrCode]目录同时承接礼品卡 ID 或兑换码:
export const GET = async ( req: AuthenticatedMedusaRequest<StoreGetGiftCardParams>, res: MedusaResponse ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY); const { idOrCode: code } = req.params; // ...通过 query.graph 按 code 查询礼品卡,未命中时抛出 NOT_FOUND res.json({ gift_card }); };在路由中使用容器(req.scope)
Medusa 的依赖注入容器(container)在路由处理器中通过req.scope暴露。可以用它解析模块的主服务(module service)以及其他已注册的资源,完成业务逻辑:
import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" export const GET = async ( req: MedusaRequest, res: MedusaResponse ) => { const productModuleService = req.scope.resolve("product") const [, count] = await productModuleService.listAndCount() res.json({ count, }) }上面的示例解析了product模块的主服务并调用listAndCount()统计商品总数。在实践中,loyalty 插件更常解析框架注册的通用查询服务(注册键ContainerRegistrationKeys.QUERY)来执行 GraphQL 风格的实体查询,例如 store/store-credit-accounts/claim/route.ts 中的POST处理器:它先解析QUERY服务与认证上下文req.auth_context.actor_id,运行claimStoreCreditAccountWorkflow.run(...)工作流完成领券业务,再通过graph.graph({ entity: "store_credit_account", ... })查询结果并返回。
除了服务,你还可以在处理器中访问:
req.auth_context:当前请求的认证信息(actor_id等),适用于经过认证的路由;req.queryConfig:由查询校验中间件解析出的字段选择配置;req.body:请求体内容。
中间件体系:middlewares.ts与defineMiddlewares
可以为路由挂载中间件:在/api/middlewares.ts中导出一个配置对象,声明将哪些中间件应用到哪些路由上。
例如,要为/store/custom路由应用一个自定义中间件函数,在/api/middlewares.ts中写入:
import { defineMiddlewares } from "@medusajs/framework/http" import type { MedusaRequest, MedusaResponse, MedusaNextFunction, } from "@medusajs/framework/http"; async function logger( req: MedusaRequest, res: MedusaResponse, next: MedusaNextFunction ) { console.log("Request received"); next(); } export default defineMiddlewares({ routes: [ { matcher: "/store/custom", middlewares: [logger], }, ], })其中:
matcher:可以是字符串或正则表达式,用于匹配要应用中间件的路由;middlewares:接收一个中间件函数数组。
从源码看,defineMiddlewares是@medusajs/framework/http提供的辅助函数(define-middlewares.ts),它规范化配置结构并返回MiddlewaresConfig。真正的加载工作由MiddlewareFileLoader(middleware-file-loader.ts)完成——它专门扫描名为middlewares(MIDDLEWARE_FILE_NAME)的文件,读取其 default 导出配置。
中间件路由配置的完整字段
根据 types.ts 中MiddlewareRoute与MiddlewaresConfig的类型定义,每条中间件路由配置支持以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
matcher | string \| RegExp | 匹配要应用中间件的路由路径 |
methods | MiddlewareVerb[] | 限定该配置仅对指定 HTTP 方法生效(USE/ALL或 7 个标准方法);旧版字段method已标记为废弃 |
middlewares | 中间件函数数组 | 依次应用到匹配路由的中间件 |
bodyParser | false \| { sizeLimit?, preserveRawBody? } | 覆盖该路由的 body parser 配置,false表示关闭解析,也可自定义大小限制 |
additionalDataValidator | ZodRawShape | 以 Zod shape 声明额外的请求数据校验规则,与既有路由校验器合并 |
policies | { resource, operation }[] | 声明该路由所需的 RBAC 资源与操作权限 |
methods字段让同一matcher可以对 GET 与 POST 应用不同的中间件组合。loyalty 插件的 store/gift-cards/middlewares.ts 展示了这一实践:
export const storeGiftCardsMiddlewares: MiddlewareRoute[] = [ { method: ["GET"], matcher: "/store/gift-cards/:code", middlewares: [ validateAndTransformQuery( StoreGetGiftCardParams, retrieveGiftCardTransformQueryConfig ), ], }, ];这里通过method: ["GET"]限定方法,用validateAndTransformQuery对查询参数进行 Zod 校验与字段转换——注意matcher中直接使用了 Express 风格的:code参数写法,与文件系统路由中[idOrCode]目录生成的:idOrCode是同一套参数机制。
统一组织多个路由的中间件
插件或应用通常为每个子目录维护一份middlewares.ts(如 admin/gift-cards/middlewares.ts、store/carts/middlewares.ts),再在顶层 api/middlewares.ts 统一汇总:
import { defineMiddlewares } from "@medusajs/framework"; import { adminGiftCardMiddlewares } from "./admin/gift-cards/middlewares"; import { storeGiftCardsMiddlewares } from "./store/gift-cards/middlewares"; // ... export default defineMiddlewares({ routes: [ ...adminGiftCardMiddlewares, ...storeGiftCardsMiddlewares, // ... ], });这种"局部声明、顶层汇总"的组织方式,使每个业务子模块的中间件职责内聚、便于复用与测试。
自定义全局错误处理
defineMiddlewares的配置对象还支持顶层errorHandler字段(值为false或自定义错误处理函数),用于覆盖全局错误处理行为。未提供时,框架使用内置的 error-handler 中间件 统一格式化异常响应。
深入原理:路由是如何被扫描与注册的
框架对src/api目录的处理分为两个阶段,均由 express-loader.ts 在应用启动时驱动:
路由加载(RoutesLoader):扫描
route.ts文件,生成RouteDescriptor并注册。每个描述符包含matcher(URL 模式)、method(HTTP 方法)、handler(处理函数)以及一组标志位(types.ts):optedOutOfAuth:是否跳过认证;shouldAppendAdminCors/shouldAppendStoreCors/shouldAppendAuthCors:是否为对应前缀的路由附加 CORS 策略。
中间件加载(MiddlewareFileLoader):扫描
middlewares.ts,解析出中间件描述符、body parser 配置路由、附加数据校验路由与全局错误处理函数,再统一装配到 Express 应用上。
其中路由类型(admin / store / auth)是根据路径前缀自动判定的:RoutesLoader用ADMIN_ROUTE_MATCH = /(\/admin$|\/admin\/)/、STORE_ROUTE_MATCH = /(\/store$|\/store\/)/、AUTH_ROUTE_MATCH = /(\/auth$|\/auth\/)/三个正则(routes-loader.ts)识别前缀,并据此附加对应的 CORS 策略与认证行为。
认证与 CORS 的控制标志
默认情况下,所有路由都会被框架的认证机制保护(shouldAuthenticate默认为true)并附加 CORS 策略。如果某个路由需要跳过认证(例如公开的 Webhook 回调端点),可以在route.ts中显式导出标志来覆盖默认行为:
export const AUTHENTICATE = false; // 跳过该路由的认证 export const CORS = false; // 跳过该路由的 CORS 策略路由加载器通过AUTHTHENTICATION_FLAG = "AUTHENTICATE"与CORS_FLAG = "CORS"检查文件导出(routes-loader.ts),并将结果写入描述符的optedOutOfAuth与shouldAppend*Cors标志。在 中间件夹具 与 routes-loader 相关测试 中,你可以看到这些标志与中间件装配行为的完整验证用例。
实践要点小结
- 每个路由文件必须命名为
route.ts/route.js,位于应用src/api目录下;目录层级即 URL 路径,[name]目录即路径参数。 - 一个文件可同时导出多个 HTTP 方法处理器;非方法名的导出(
AUTHENTICATE、CORS标志)用于控制认证与 CORS 行为。 - 业务逻辑通过
req.scope.resolve(...)获取模块服务与容器资源,工作流可通过workflow.run({ input, container: req.scope })编排复杂业务。 - 中间件集中在
src/api/middlewares.ts中声明,使用defineMiddlewares组织matcher+methods+middlewares(以及bodyParser、additionalDataValidator、policies)的配置项;loyalty 插件提供了多子模块中间件文件汇总合并的成熟范式。 - 框架的路由与中间件加载实现在 routes-loader.ts、middleware-file-loader.ts 与 express-loader.ts 中,配合tests下的测试用例,可作为你理解与调试自定义路由行为的权威参考。
【免费下载链接】medusaThe world's most flexible commerce platform for agents and developers项目地址: https://gitcode.com/GitHub_Trending/me/medusa
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考