medusa について
medusaは、カスタマイズ可能なヘッドレスコマースを構築するチーム向けのNode.jsフレームワークです。バックエンド、Store API、組み込み管理画面に加え、モジュール、ルート、ワークフロー、イベント購読で拡張できます。
主な機能
- 01ヘッドレスコマースのバックエンドとStore API
- 02カスタマイズ可能な組み込み管理画面
- 03カスタムAPIルートとモジュール
- 04ワークフローとイベント購読
- 05定期ジョブとデプロイ例
活用シーン
- 01独立したフロントエンドのストア構築
- 02商品や注文のビジネスロジック拡張
- 03注文や在庫イベントへの対応
- 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,
},
},
],
});
管理画面での設定
- Settings → Regions に移動します
- Stripeを決済プロバイダーとして追加します
- 各リージョンに設定します
デプロイ
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エラーを無視する - フレームワークは型に依存しています
コメント
Markdown 対応