技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
medusa-development【数据与研究】Best practices for building commerce applications with Medusa v2, the headless e-commerce framework. Use when defining Medusa data models, writing workflows and steps with the Workflow SDK, creating API routes or subscribers, building module services that extend MedusaService, throwing MedusaError, or customizing the Medusa admin dashboard.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Medusa is a headless commerce framework built around modules, data models, and workflows; almost every piece of business logic — from an API route to a scheduled job — should be expressed as a workflow made of discrete, composable steps.
model utility from @medusajs/framework/utils to declare the module's data model(s) under src/modules/<module>/models/.src/modules/<module>/service.ts that extends MedusaService when the module has data models, exposing async methods for domain operations.medusa-config.ts so Medusa's container can resolve it.createStep from @medusajs/framework/workflows-sdk, including a compensation function for anything that needs to be undone on failure.createWorkflow, using transform for data shaping and when for conditional branches.req.scope.resolve("query") or the workflow-level useQueryGraphStep) to fetch data instead of calling module services directly for reads.MedusaError (from @medusajs/framework/utils) instead of a plain Error, so the API layer can map it to the correct HTTP status and error code.import { MedusaError } from "@medusajs/framework/utils"
if (!product) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Product with id "${productId}" was not found`
)
}
model utility from @medusajs/framework/utils to define data models.model.define should be snake_case.id field to a data model, always make it a primary key with .primaryKey().id field — any other identifier should be a text field instead.// src/modules/loyalty/models/loyalty-account.ts
import { model } from "@medusajs/framework/utils"
const LoyaltyAccount = model.define("loyalty_account", {
id: model.id().primaryKey(),
customer_id: model.text(),
points_balance: model.number().default(0),
tier: model.enum(["bronze", "silver", "gold"]).default("bronze"),
})
export default LoyaltyAccount
MedusaService so it inherits generated CRUD methods for each model.// src/modules/loyalty/service.ts
import { MedusaService } from "@medusajs/framework/utils"
import LoyaltyAccount from "./models/loyalty-account"
class LoyaltyModuleService extends MedusaService({
LoyaltyAccount,
}) {
async addPoints(accountId: string, points: number) {
const account = await this.retrieveLoyaltyAccount(accountId)
return await this.updateLoyaltyAccounts({
id: account.id,
points_balance: account.points_balance + points,
})
}
}
export default LoyaltyModuleService
@medusajs/framework/workflows-sdk) to define it.transform for any data transformation between steps — don't manipulate step output directly in the workflow function body.when to define conditional branches instead of a plain if around step calls.await when calling steps inside a workflow — step invocation returns a special reference the workflow engine resolves, and await-ing it breaks the workflow's ability to orchestrate compensation and retries.async — the function body only describes the step graph, it doesn't execute imperatively.// src/workflows/redeem-loyalty-points.ts
import {
createStep,
createWorkflow,
StepResponse,
transform,
when,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { MedusaError } from "@medusajs/framework/utils"
import { LOYALTY_MODULE } from "../modules/loyalty"
import LoyaltyModuleService from "../modules/loyalty/service"
type RedeemPointsInput = {
accountId: string
points: number
}
const deductPointsStep = createStep(
"deduct-points-step",
async (input: RedeemPointsInput, { container }) => {
const loyaltyService: LoyaltyModuleService = container.resolve(LOYALTY_MODULE)
const account = await loyaltyService.retrieveLoyaltyAccount(input.accountId)
if (account.points_balance < input.points) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Insufficient points balance"
)
}
const previousBalance = account.points_balance
const updated = await loyaltyService.updateLoyaltyAccounts({
id: account.id,
points_balance: previousBalance - input.points,
})
return new StepResponse(updated, { accountId: account.id, previousBalance })
},
async (compensationInput, { container }) => {
if (!compensationInput) return
const loyaltyService: LoyaltyModuleService = container.resolve(LOYALTY_MODULE)
await loyaltyService.updateLoyaltyAccounts({
id: compensationInput.accountId,
points_balance: compensationInput.previousBalance,
})
}
)
export const redeemLoyaltyPointsWorkflow = createWorkflow(
"redeem-loyalty-points",
(input: RedeemPointsInput) => {
const account = deductPointsStep(input)
const tierDowngrade = when(account, (acc) => acc.points_balance === 0)
.then(() => {
return transform({ account }, (data) => ({
...data.account,
tier: "bronze" as const,
}))
})
return new WorkflowResponse(account)
}
)
src/api/; the route handler should validate input, call the workflow, and shape the HTTP response — nothing more.// src/api/store/loyalty/[id]/route.ts
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const query = req.scope.resolve("query")
const { data: accounts } = await query.graph({
entity: "loyalty_account",
fields: ["id", "points_balance", "tier"],
filters: { id: req.params.id },
})
res.json({ loyalty_account: accounts[0] })
}
export async function POST(req: MedusaRequest, res: MedusaResponse) {
const { result } = await redeemLoyaltyPointsWorkflow(req.scope).run({
input: req.validatedBody as { accountId: string; points: number },
})
res.json({ loyalty_account: result })
}
// src/subscribers/order-placed.ts
import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { redeemLoyaltyPointsWorkflow } from "../workflows/redeem-loyalty-points"
export default async function orderPlacedHandler({ event, container }: SubscriberArgs<{ id: string }>) {
await redeemLoyaltyPointsWorkflow(container).run({
input: { accountId: event.data.id, points: 0 },
})
}
export const config: SubscriberConfig = { event: "order.placed" }
@medusajs/js-sdk) rather than raw fetch.// src/admin/widgets/loyalty-widget.tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
const LoyaltyWidget = ({ data }: { data: { id: string } }) => {
const { data: result } = useQuery({
queryFn: () => sdk.admin.customer.retrieve(data.id),
queryKey: ["customer", data.id],
})
return <p className="text-ui-fg-subtle">{result?.customer.email}</p>
}
export const config = defineWidgetConfig({ zone: "customer.details.after" })
export default LoyaltyWidget
Error instead of MedusaError, which loses the mapped HTTP status code and structured error type on the API response.model.define name in camelCase instead of snake_case, causing inconsistency with the rest of the schema.