medusa-development について
Medusa Development は、ヘッドレスコマースフレームワーク Medusa で機能を開発する人向けのスキルです。データモデル、モジュールサービス、ステップで構成するワークフローを中心に、API、ジョブ、サブスクライバーからの呼び出し方を案内します。
主な機能
- 01データモデルとモジュールサービスの指針
- 02組み合わせ可能なワークフローステップ
- 03ルートやジョブからのワークフロー呼び出し
- 04Query によるデータ読み取り
- 05管理画面カスタマイズの規約
活用シーン
- 01コマースモジュールとデータモデルの追加
- 02業務ロジックのワークフロー化
- 03ルートやスケジュールジョブからの呼び出し
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ mindrally/ skills --skill medusa-developmentインストール後、Claude Code で/medusa-developmentを入力して読み込まれたか確認します。
詳細
Medusa 開発
Medusa は、モジュール、データモデル、ワークフローを中心に構築されたヘッドレスコマースフレームワークです。API ルートからスケジュールされたジョブまで、ほぼすべてのビジネスロジックを、個別に組み合わせ可能なステップで構成されるワークフローとして表現する必要があります。
Medusa の機能を構築するワークフロー
- データモデルを定義または拡張する —
@medusajs/framework/utilsのmodelユーティリティを使用して、src/modules/<module>/models/の下にモジュールのデータモデルを宣言します。 - モジュールサービスを作成する — モジュールにデータモデルがある場合は、
src/modules/<module>/service.tsにMedusaServiceを継承するサービスを作成し、ドメイン操作を行う非同期メソッドを公開します。 - モジュールを登録する — Medusa のコンテナで解決できるように、
medusa-config.tsにモジュールを追加します。 - ステップを構築する —
@medusajs/framework/workflows-sdkのcreateStepを使用して、作業の各単位をステップとして定義します。失敗時に取り消す必要がある処理には、補償関数を含めます。 - ワークフローを組み立てる —
createWorkflowを使用してステップを連結し、データの整形にはtransformを、条件分岐にはwhenを使用します。 - ワークフローを公開する — API ルート、スケジュールされたジョブ、またはサブスクライバーからワークフローを呼び出します。ルート、ジョブ、サブスクライバーのハンドラーにビジネスロジックを直接記述しないでください。
- Query でデータを読み取る — 読み取り時にはモジュールサービスを直接呼び出さず、Medusa の Query(
req.scope.resolve("query")またはワークフローレベルのuseQueryGraphStep)を使用してデータを取得します。
一般的なルール
- ファイルをインポートする際に型エイリアスを使用しないでください。ローカルエイリアスを介して再エクスポートするのではなく、型と値をそれぞれのソースモジュールから直接インポートします。
- エラーをスローする際は、通常の
Errorではなく、必ずMedusaError(@medusajs/framework/utilsから)をスローしてください。これにより、API レイヤーが適切な HTTP ステータスとエラーコードに対応付けられます。 - 読み取り時はモジュールのサービスメソッドを直接呼び出さず、常に Query を使用してデータを取得してください。Query はモジュール間のリンクを認識し、1 回の呼び出しでモジュール間のデータを結合できます。
import { MedusaError } from "@medusajs/framework/utils"
if (!product) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Product with id "${productId}" was not found`
)
}
データモデルのルール
@medusajs/framework/utilsのmodelユーティリティを使用してデータモデルを定義します。- データモデルの変数名はキャメルケースにし、
model.defineに渡す名前はスネークケースにします。 - データモデルに
idフィールドを追加する場合は、必ず.primaryKey()を指定して主キーにします。 - データモデルに指定できる
idフィールドは 1 つだけです。それ以外の識別子にはtextフィールドを使用します。 - データモデルのフィールド名はスネークケースにします。
// 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を継承させ、各モデル用に生成される CRUD メソッドを利用できるようにします。
// 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
ワークフロールール
- ワークフローまたはステップを作成する際は、必ず Medusa の Workflow SDK(
@medusajs/framework/workflows-sdk)を使用して定義してください。 - API ルート、スケジュールジョブ、またはサブスクライバーで機能を作成する際は、ハンドラー内にロジックを直接記述せず、必ずワークフローを作成してください。
- ワークフローを作成する際は、ワークフロー内の個々の作業単位ごとに必ずステップを作成してください。
- ワークフローでは、ステップ間のデータ変換には必ず
transformを使用してください。ワークフロー関数本体でステップの出力を直接操作しないでください。 - ワークフローでは、ステップ呼び出しを囲む単純な
if文ではなく、条件分岐の定義にwhenを使用してください。 - ワークフロー内でステップを呼び出す際に
awaitを使用しないでください。ステップの呼び出しはワークフローエンジンが解決する特別な参照を返します。これをawaitすると、補償処理や再試行を調整するワークフローの機能が損なわれます。 - ワークフローでは、ワークフロー関数自体を
asyncにしないでください。関数本体はステップのグラフを記述するだけで、命令型に処理を実行するものではありません。 - 補償関数の入力に型を指定しないでください。補償関数はステップの invoke 関数が返した値を受け取り、Medusa がこれを自動的に推論します。
- ワークフロー内ではステップのみを使用してください。ワークフロー関数内でサービス、Query、その他の副作用を伴うコードを直接呼び出さず、そのロジックはステップに記述してください。
// 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)
}
)
API ルートとデータの読み取り
src/api/配下の API ルートを通じてワークフローを公開してください。ルートハンドラーでは入力を検証し、ワークフローを呼び出して、HTTP レスポンスを整形してください。それ以外の処理は行わないでください。- 読み取り用のデータ(一覧/詳細エンドポイント)の取得には、モジュールサービスを直接解決するのではなく、必ず Query を使用してください。
// 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 })
}
サブスクライバーとスケジュールジョブ
- サブスクライバーとスケジュールジョブでは、API ルートとまったく同様にワークフローを呼び出してください。同じビジネスロジックを起動する、別のトリガーにすぎません。
// 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" }
管理画面のカスタマイズルール
- 管理画面のカスタマイズ(ウィジェット、カスタムページ)からリクエストを送信する際は、生の
fetchではなく、必ず Medusa の JS SDK(@medusajs/js-sdk)を使用してください。 - 管理画面のカスタマイズのスタイリングには TailwindCSS を使用し、Medusa Admin の UI 自体の規約に合わせてください。
// 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
よくある間違い
- ワークフローを通さず、API ルートハンドラーからモジュールサービスを直接呼び出すこと。これにより、ワークフローが提供する再試行、補償処理、標準的なイベント/オブザーバビリティのフックがスキップされます。
MedusaErrorではなく通常のErrorをスローすること。これにより、API レスポンスで対応する HTTP ステータスコードと構造化されたエラータイプが失われます。- ワークフロー関数内でステップ呼び出しを await すること。これにより、ワークフローエンジンがステップのグラフを構築できなくなります。
- Query を使用せず、モジュールサービスを解決してデータを読み取ること。これにより、Query が自動的に解決するモジュール間の結合やリンクを取得できません。
- データモデルのフィールド名や
model.defineの名前にスネークケースではなくキャメルケースを使用すること。これにより、スキーマ全体の一貫性が損なわれます。
追加リソース
- Medusa のドキュメント:https://docs.medusajs.com/llms-full.txt
コメント
Markdown 対応