mcpField
M
medusa

medusa

github.com/alinaqi開発711278 回インストール2 週間前に更新
リポジトリ

medusa について

medusaは、カスタマイズ可能なヘッドレスコマースを構築するチーム向けのNode.jsフレームワークです。バックエンド、Store API、組み込み管理画面に加え、モジュール、ルート、ワークフロー、イベント購読で拡張できます。

主な機能

  1. 01ヘッドレスコマースのバックエンドとStore API
  2. 02カスタマイズ可能な組み込み管理画面
  3. 03カスタムAPIルートとモジュール
  4. 04ワークフローとイベント購読
  5. 05定期ジョブとデプロイ例

活用シーン

  1. 01独立したフロントエンドのストア構築
  2. 02商品や注文のビジネスロジック拡張
  3. 03注文や在庫イベントへの対応
  4. 04管理画面とAPIのカスタマイズ

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/alinaqi/maggy --skill medusa

インストール後、Claude Code で/medusaを入力して読み込まれたか確認します。

詳細

Medusa Eコマーススキル

オープンソースでNode.jsネイティブ、完全にカスタマイズ可能なMedusaでヘッドレスEコマースを構築します。

ソース: Medusa Docs | API Reference | GitHub


Medusaを選ぶ理由

機能メリット
オープンソースセルフホスト可能で、ベンダーロックインがなく、MITライセンスです
Node.jsネイティブTypeScriptを採用し、使い慣れたスタックで、簡単にカスタマイズできます
ヘッドレス任意のフロントエンドに対応します(Next.js、Remix、モバイル)
モジュール式必要な機能だけを使え、あらゆる機能を拡張できます
管理画面を標準搭載ダッシュボードが含まれており、カスタマイズ可能です

クイックスタート

前提条件
# Required
node --version  # v20+ LTS
git --version
# PostgreSQL running locally or remote
新しいプロジェクトを作成する
# Scaffold new Medusa application
npx create-medusa-app@latest my-store

# This creates:
# - Medusa backend
# - PostgreSQL database (auto-configured)
# - Admin dashboard
# - Optional: Next.js storefront

cd my-store
npm run dev
アクセス先
URL用途
http://localhost:9000バックエンドAPI
http://localhost:9000/app管理ダッシュボード
http://localhost:8000ストアフロント(インストール済みの場合)
管理者ユーザーを作成する
npx medusa user -e [email protected] -p supersecret

プロジェクト構成

medusa-store/
├── src/
│   ├── admin/                    # Admin UI customizations
│   │   ├── widgets/              # Dashboard widgets
│   │   └── routes/               # Custom admin pages
│   ├── api/                      # Custom API routes
│   │   ├── store/                # Public storefront APIs
│   │   │   └── custom/
│   │   │       └── route.ts
│   │   └── admin/                # Admin APIs
│   │       └── custom/
│   │           └── route.ts
│   ├── jobs/                     # Scheduled tasks
│   ├── modules/                  # Custom business logic
│   ├── workflows/                # Multi-step processes
│   ├── subscribers/              # Event listeners
│   └── links/                    # Module relationships
├── .medusa/                      # Auto-generated (don't edit)
├── medusa-config.ts              # Configuration
├── package.json
└── tsconfig.json

設定

medusa-config.ts
import { defineConfig, loadEnv } from "@medusajs/framework/utils";

loadEnv(process.env.NODE_ENV || "development", process.cwd());

export default defineConfig({
  projectConfig: {
    databaseUrl: process.env.DATABASE_URL,
    http: {
      storeCors: process.env.STORE_CORS || "http://localhost:8000",
      adminCors: process.env.ADMIN_CORS || "http://localhost:9000",
      authCors: process.env.AUTH_CORS || "http://localhost:9000",
    },
    redisUrl: process.env.REDIS_URL,
  },
  admin: {
    disable: false,
    backendUrl: process.env.MEDUSA_BACKEND_URL || "http://localhost:9000",
  },
  modules: [
    // Add custom modules here
  ],
});
環境変数
# .env
DATABASE_URL=postgresql://user:pass@localhost:5432/medusa
REDIS_URL=redis://localhost:6379

# CORS (comma-separated for multiple origins)
STORE_CORS=http://localhost:8000
ADMIN_CORS=http://localhost:9000

# Backend URL
MEDUSA_BACKEND_URL=http://localhost:9000

# JWT Secrets
JWT_SECRET=your-super-secret-jwt-key
COOKIE_SECRET=your-super-secret-cookie-key

カスタムAPIルート

Store API(公開)
// src/api/store/hello/route.ts
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  res.json({
    message: "Hello from custom store API!",
  });
}

// Accessible at: GET /store/hello
Admin API(保護)
// src/api/admin/analytics/route.ts
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
import { Modules } from "@medusajs/framework/utils";

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const orderService = req.scope.resolve(Modules.ORDER);

  const orders = await orderService.listOrders({
    created_at: {
      $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // Last 30 days
    },
  });

  const totalRevenue = orders.reduce(
    (sum, order) => sum + (order.total || 0),
    0
  );

  res.json({
    orderCount: orders.length,
    totalRevenue,
  });
}

// Accessible at: GET /admin/analytics (requires auth)
パラメーター付きルート
// src/api/store/products/[id]/reviews/route.ts
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { id } = req.params;

  // Fetch reviews for product
  const reviews = await getReviewsForProduct(id);

  res.json({ reviews });
}

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { id } = req.params;
  const { rating, comment, customerId } = req.body;

  const review = await createReview({
    productId: id,
    rating,
    comment,
    customerId,
  });

  res.status(201).json({ review });
}

// Accessible at:
// GET  /store/products/:id/reviews
// POST /store/products/:id/reviews
ミドルウェア
// src/api/middlewares.ts
import { defineMiddlewares } from "@medusajs/framework/http";
import { authenticate } from "@medusajs/framework/http";

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/protected/*",
      middlewares: [authenticate("customer", ["session", "bearer"])],
    },
    {
      matcher: "/admin/*",
      middlewares: [authenticate("user", ["session", "bearer"])],
    },
  ],
});

モジュール(カスタムビジネスロジック)

カスタムモジュールを作成する
// src/modules/reviews/index.ts
import { Module } from "@medusajs/framework/utils";
import ReviewModuleService from "./service";

export const REVIEW_MODULE = "reviewModuleService";

export default Module(REVIEW_MODULE, {
  service: ReviewModuleService,
});
// src/modules/reviews/service.ts
import { MedusaService } from "@medusajs/framework/utils";

class ReviewModuleService extends MedusaService({}) {
  async createReview(data: CreateReviewInput) {
    // Implementation
  }

  async getProductReviews(productId: string) {
    // Implementation
  }

  async getAverageRating(productId: string) {
    // Implementation
  }
}

export default ReviewModuleService;
モジュールを登録する
// medusa-config.ts
import { REVIEW_MODULE } from "./src/modules/reviews";

export default defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/reviews",
      options: {},
    },
  ],
});
APIでモジュールを使用する
// src/api/store/products/[id]/reviews/route.ts
import { REVIEW_MODULE } from "../../../modules/reviews";

export async function GET(req: MedusaRequest, res: MedusaResponse) {
  const { id } = req.params;
  const reviewService = req.scope.resolve(REVIEW_MODULE);

  const reviews = await reviewService.getProductReviews(id);
  const averageRating = await reviewService.getAverageRating(id);

  res.json({ reviews, averageRating });
}

ワークフロー

ワークフローを定義する
// src/workflows/create-order-with-notification/index.ts
import {
  createWorkflow,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk";
import { Modules } from "@medusajs/framework/utils";

const createOrderStep = createStep(
  "create-order",
  async (input: CreateOrderInput, { container }) => {
    const orderService = container.resolve(Modules.ORDER);

    const order = await orderService.createOrders(input);

    return new StepResponse(order, order.id);
  },
  // Compensation (rollback) function
  async (orderId, { container }) => {
    const orderService = container.resolve(Modules.ORDER);
    await orderService.deleteOrders([orderId]);
  }
);

const sendNotificationStep = createStep(
  "send-notification",
  async (order: Order, { container }) => {
    const notificationService = container.resolve("notificationService");

    await notificationService.send({
      to: order.email,
      template: "order-confirmation",
      data: { order },
    });

    return new StepResponse({ sent: true });
  }
);

export const createOrderWithNotificationWorkflow = createWorkflow(
  "create-order-with-notification",
  (input: CreateOrderInput) => {
    const order = createOrderStep(input);
    const notification = sendNotificationStep(order);

    return { order, notification };
  }
);
ワークフローを実行する
// In an API route
import { createOrderWithNotificationWorkflow } from "../../../workflows/create-order-with-notification";

export async function POST(req: MedusaRequest, res: MedusaResponse) {
  const { result } = await createOrderWithNotificationWorkflow(req.scope).run({
    input: req.body,
  });

  res.json(result);
}

サブスクライバー(イベントリスナー)

サブスクライバーを作成する
// src/subscribers/order-placed.ts
import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework";

export default async function orderPlacedHandler({
  event,
  container,
}: SubscriberArgs<{ id: string }>) {
  const orderId = event.data.id;

  console.log(`Order placed: ${orderId}`);

  // Send notification, update analytics, etc.
  const notificationService = container.resolve("notificationService");
  await notificationService.sendOrderConfirmation(orderId);
}

export const config: SubscriberConfig = {
  event: "order.placed",
};
一般的なイベント
イベント発生条件
order.placed新しい注文が作成されたとき
order.updated注文が変更されたとき
order.canceled注文がキャンセルされたとき
order.completed注文のフルフィルメントが完了したとき
customer.created新しい顧客が登録されたとき
product.created新しい商品が追加されたとき
product.updated商品が変更されたとき
inventory.updated在庫が変更されたとき

スケジュール済みジョブ

// src/jobs/sync-inventory.ts
import type { MedusaContainer } from "@medusajs/framework";

export default async function syncInventoryJob(container: MedusaContainer) {
  const inventoryService = container.resolve("inventoryService");

  console.log("Running inventory sync...");

  await inventoryService.syncFromExternalSource();

  console.log("Inventory sync complete");
}

export const config = {
  name: "sync-inventory",
  schedule: "0 */6 * * *", // Every 6 hours
};

管理UIのカスタマイズ

カスタムウィジェット
// src/admin/widgets/sales-overview.tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk";
import { Container, Heading, Text } from "@medusajs/ui";

const SalesOverviewWidget = () => {
  return (
    <Container>
      <Heading level="h2">Sales Overview</Heading>
      <Text>Your custom sales data here...</Text>
    </Container>
  );
};

export const config = defineWidgetConfig({
  zone: "order.list.before", // Where to show the widget
});

export default SalesOverviewWidget;
ウィジェットゾーン
ゾーン場所
order.list.before注文リストの前
order.details.after注文詳細の後
product.list.before商品リストの前
product.details.after商品詳細の後
customer.list.before顧客リストの前
カスタム管理ルート
// src/admin/routes/analytics/page.tsx
import { defineRouteConfig } from "@medusajs/admin-sdk";
import { Container, Heading } from "@medusajs/ui";
import { ChartBar } from "@medusajs/icons";

const AnalyticsPage = () => {
  return (
    <Container>
      <Heading level="h1">Analytics Dashboard</Heading>
      {/* Your analytics charts */}
    </Container>
  );
};

export const config = defineRouteConfig({
  label: "Analytics",
  icon: ChartBar,
});

export default AnalyticsPage;

Store API(標準搭載)

商品
// Frontend: Fetch products
const response = await fetch("http://localhost:9000/store/products");
const { products } = await response.json();

// With filters
const response = await fetch(
  "http://localhost:9000/store/products?" +
  new URLSearchParams({
    category_id: "cat_123",
    limit: "20",
    offset: "0",
  })
);
カート
// Create cart
const { cart } = await fetch("http://localhost:9000/store/carts", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    region_id: "reg_123",
  }),
}).then(r => r.json());

// Add item
await fetch(`http://localhost:9000/store/carts/${cart.id}/line-items`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    variant_id: "variant_123",
    quantity: 1,
  }),
});

// Complete cart (create order)
const { order } = await fetch(
  `http://localhost:9000/store/carts/${cart.id}/complete`,
  { method: "POST" }
).then(r => r.json());
顧客認証
// Register
await fetch("http://localhost:9000/store/customers", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "[email protected]",
    password: "password123",
    first_name: "John",
    last_name: "Doe",
  }),
});

// Login
const { token } = await fetch("http://localhost:9000/store/auth/token", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "[email protected]",
    password: "password123",
  }),
}).then(r => r.json());

// Authenticated request
await fetch("http://localhost:9000/store/customers/me", {
  headers: {
    Authorization: `Bearer ${token}`,
  },
});

決済連携

Stripeの設定
npm install @medusajs/payment-stripe
// medusa-config.ts
export default defineConfig({
  modules: [
    {
      resolve: "@medusajs/payment-stripe",
      options: {
        apiKey: process.env.STRIPE_API_KEY,
      },
    },
  ],
});
管理画面での設定
  1. Settings → Regions に移動します
  2. Stripeを決済プロバイダーとして追加します
  3. 各リージョンに設定します

デプロイ

Railway
# Install Railway CLI
npm install -g @railway/cli

# Login and deploy
railway login
railway init
railway up
Render
# render.yaml
services:
  - type: web
    name: medusa-backend
    runtime: node
    plan: starter
    buildCommand: npm install && npm run build
    startCommand: npm run start
    envVars:
      - key: NODE_ENV
        value: production
      - key: DATABASE_URL
        fromDatabase:
          name: medusa-db
          property: connectionString
      - key: JWT_SECRET
        generateValue: true
      - key: COOKIE_SECRET
        generateValue: true

databases:
  - name: medusa-db
    plan: starter
Docker
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

EXPOSE 9000
CMD ["npm", "run", "start"]

CLIコマンド

# Development
npm run dev                    # Start dev server

# Database
npx medusa db:migrate          # Run migrations
npx medusa db:sync             # Sync schema

# Users
npx medusa user -e email -p pass  # Create admin user

# Build
npm run build                  # Build for production
npm run start                  # Start production server

チェックリスト

セットアップ
  • PostgreSQLデータベースを設定済み
  • Redisを設定済み(任意ですが推奨)
  • 管理者ユーザーを作成済み
  • CORSオリジンを設定済み
  • JWT/Cookieシークレットを設定済み
カスタマイズ
  • ビジネスロジック用のカスタムモジュール
  • フロントエンド用のカスタムAPIルート
  • イベント処理用のサブスクライバー
  • 複雑な処理用のワークフロー
デプロイ
  • 環境変数を設定済み
  • データベースのマイグレーションを実行済み
  • HTTPSを有効化済み
  • 管理画面のURLを保護済み

アンチパターン

  • .medusaフォルダーの編集 - 自動生成されるため、上書きされます
  • データベースへの直接アクセス - サービスとモジュールを使用してください
  • 複雑な処理でワークフローを省略する - ワークフローを使うとロールバックできます
  • URLのハードコーディング - 環境変数を使用してください
  • TypeScriptエラーを無視する - フレームワークは型に依存しています

コメント