components-guide について
独立したコンポーネントで Convex バックエンドを構成するためのガイドスキルです。機能の再利用、データ分離、明確な API 境界を目指す開発者向けです。
主な機能
- 01スキーマとデータを分離したコンポーネント
- 02コンポーネントの設定と統合
- 03親からコンポーネントへの通信パターン
- 04再利用可能なコンポーネント作成の指針
- 05モノリスからの段階的な移行
活用シーン
- 01大規模なバックエンドからストレージや課金を切り出す
- 02複数の同階層コンポーネントを組み合わせる
- 03独自スキーマを持つ再利用可能なコンポーネントの構築
- 04モノリスのバックエンド機能を段階的に移行する
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ get-convex/ convex-agent-plugins --skill components-guideインストール後、Claude Code で/components-guideを入力して読み込まれたか確認します。
詳細
Convex コンポーネントガイド
コンポーネントを使って機能をカプセル化し、保守しやすく再利用可能なバックエンドを構築しましょう。
Convex コンポーネントとは?
コンポーネントは、機能をまとめた自己完結型のミニバックエンドです。以下のものが含まれます。
- 独自のデータベーススキーマ
- 独自の関数(クエリ、ミューテーション、アクション)
- 独自のデータ(分離されたテーブル)
- 明確な API 境界
**たとえるなら、**バックエンド向けの npm パッケージ、またはデプロイの複雑さを伴わないマイクロサービスです。
コンポーネントを使う理由
従来のアプローチ(モノリシック)
convex/
├── users.ts (500 lines)
├── files.ts (600 lines - upload, storage, permissions, rate limiting)
├── payments.ts (400 lines - Stripe, webhooks, billing)
├── notifications.ts (300 lines)
└── analytics.ts (200 lines)
Total: One big codebase, everything mixed together
コンポーネントを使ったアプローチ(カプセル化)
convex/
├── components/
│ ├── storage/ (File uploads - reusable)
│ ├── billing/ (Payments - reusable)
│ ├── notifications/ (Alerts - reusable)
│ └── analytics/ (Tracking - reusable)
├── convex.config.ts (Wire components together)
└── domain/ (Your actual business logic)
├── users.ts (50 lines - uses components)
└── projects.ts (75 lines - uses components)
Total: Clean, focused, reusable
クイックスタート
1. コンポーネントをインストールする
# Official components from npm
npm install @convex-dev/ratelimiter
2. convex.config.ts で設定する
import { defineApp } from "convex/server";
import ratelimiter from "@convex-dev/ratelimiter/convex.config";
export default defineApp({
components: {
ratelimiter,
},
});
3. コードで使用する
import { components } from "./_generated/api";
export const createPost = mutation({
handler: async (ctx, args) => {
// Use the component
await components.ratelimiter.check(ctx, {
key: `user:${ctx.user._id}`,
limit: 10,
period: 60000, // 10 requests per minute
});
return await ctx.db.insert("posts", args);
},
});
兄弟コンポーネントのパターン
同じ階層で複数のコンポーネントが連携します。
// convex.config.ts
export default defineApp({
components: {
// Sibling components - each handles one concern
auth: authComponent,
storage: storageComponent,
payments: paymentsComponent,
emails: emailComponent,
analytics: analyticsComponent,
},
});
例:兄弟コンポーネントを使った機能全体
// convex/subscriptions.ts
import { components } from "./_generated/api";
export const subscribe = mutation({
args: { plan: v.string() },
handler: async (ctx, args) => {
// 1. Verify authentication (auth component)
const user = await components.auth.getCurrentUser(ctx);
// 2. Create payment (payments component)
const subscription = await components.payments.createSubscription(ctx, {
userId: user._id,
plan: args.plan,
amount: getPlanAmount(args.plan),
});
// 3. Track conversion (analytics component)
await components.analytics.track(ctx, {
event: "subscription_created",
userId: user._id,
plan: args.plan,
});
// 4. Send confirmation (emails component)
await components.emails.send(ctx, {
to: user.email,
template: "subscription_welcome",
data: { plan: args.plan },
});
// 5. Store subscription in main app
await ctx.db.insert("subscriptions", {
userId: user._id,
paymentId: subscription.id,
plan: args.plan,
status: "active",
});
return subscription;
},
});
この構成で実現できること:
- ✅ 各コンポーネントが単一の目的に特化します
- ✅ コンポーネントを複数の機能で再利用できます
- ✅ 実装を簡単に差し替えられます(メールプロバイダーの変更など)
- ✅ コンポーネントを個別に更新できます
- ✅ 関心事を明確に分離できます
公式コンポーネント
コンポーネントディレクトリをご覧ください。
認証
- @convex-dev/better-auth - Better Auth との連携
ストレージ
- @convex-dev/r2 - Cloudflare R2 のファイルストレージ
- @convex-dev/storage - ファイルのアップロード/ダウンロード
決済
- @convex-dev/polar - Polar の請求およびサブスクリプション
AI
- @convex-dev/agent - AI エージェントのワークフロー
- @convex-dev/embeddings - ベクトルの保存と検索
バックエンドユーティリティ
- @convex-dev/ratelimiter - レート制限
- @convex-dev/aggregate - データ集計
- @convex-dev/action-cache - アクション結果のキャッシュ
- @convex-dev/sharded-counter - 分散カウンター
- @convex-dev/migrations - スキーマのマイグレーション
- @convex-dev/workflow - ワークフローのオーケストレーション
独自のコンポーネントを作成する
コンポーネントを作成する場面
適切な理由:
- 機能が自己完結している
- 複数のプロジェクトで再利用する
- チームやコミュニティと共有したい
- 独自のデータモデルを持つ複雑な機能である
- サードパーティ連携のラッパーである
適切でない理由:
- 一度限りのビジネスロジックである
- メインアプリと密接に結合している
- 単純なユーティリティ関数である
構成
mkdir -p convex/components/notifications
// convex/components/notifications/convex.config.ts
import { defineComponent } from "convex/server";
export default defineComponent("notifications");
// convex/components/notifications/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
notifications: defineTable({
userId: v.id("users"),
message: v.string(),
read: v.boolean(),
createdAt: v.number(),
})
.index("by_user", ["userId"])
.index("by_user_and_read", ["userId", "read"]),
});
// convex/components/notifications/send.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const send = mutation({
args: {
userId: v.id("users"),
message: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.insert("notifications", {
userId: args.userId,
message: args.message,
read: false,
createdAt: Date.now(),
});
},
});
export const markRead = mutation({
args: { notificationId: v.id("notifications") },
handler: async (ctx, args) => {
await ctx.db.patch(args.notificationId, { read: true });
},
});
// convex/components/notifications/read.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
export const list = query({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
return await ctx.db
.query("notifications")
.withIndex("by_user", q => q.eq("userId", args.userId))
.order("desc")
.collect();
},
});
export const unreadCount = query({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
const unread = await ctx.db
.query("notifications")
.withIndex("by_user_and_read", q =>
q.eq("userId", args.userId).eq("read", false)
)
.collect();
return unread.length;
},
});
コンポーネントを使用する
// convex.config.ts
import { defineApp } from "convex/server";
import notifications from "./components/notifications/convex.config";
export default defineApp({
components: {
notifications, // Your local component
},
});
// convex/tasks.ts - main app code
import { components } from "./_generated/api";
export const completeTask = mutation({
args: { taskId: v.id("tasks") },
handler: async (ctx, args) => {
const task = await ctx.db.get(args.taskId);
await ctx.db.patch(args.taskId, { completed: true });
// Use your component
await components.notifications.send(ctx, {
userId: task.userId,
message: `Task "${task.title}" completed!`,
});
},
});
コンポーネント間の通信パターン
✅ 親 → コンポーネント(良い例)
// Main app calls component
await components.storage.upload(ctx, file);
await components.analytics.track(ctx, event);
✅ 親 → 複数の兄弟コンポーネント(良い例)
// Main app orchestrates multiple components
await components.auth.verify(ctx);
const file = await components.storage.upload(ctx, data);
await components.notifications.send(ctx, message);
✅ コンポーネントが親のデータを受け取る(良い例)
// Pass IDs from parent's tables to component
await components.audit.log(ctx, {
userId: user._id, // From parent's users table
action: "delete",
resourceId: task._id, // From parent's tasks table
});
// Component stores these as strings/IDs
// but doesn't access parent tables directly
❌ コンポーネント → 親のテーブル(悪い例)
// Inside component code - DON'T DO THIS
const user = await ctx.db.get(userId); // Error! Can't access parent tables
❌ 兄弟 → 兄弟(悪い例)
コンポーネント同士は直接呼び出せません。これが必要な場合は、メインアプリに組み込むか、設計を見直してください。
実際の例
マルチテナント SaaS
// convex.config.ts
export default defineApp({
components: {
auth: "@convex-dev/better-auth",
organizations: "./components/organizations",
billing: "./components/billing",
storage: "@convex-dev/r2",
analytics: "./components/analytics",
emails: "./components/emails",
},
});
各コンポーネント:
auth- ユーザー認証とセッションorganizations- テナントの分離と権限billing- Stripe 連携とサブスクリプションstorage- R2 へのファイルアップロードanalytics- イベント追跡とメトリクスemails- SendGrid を使ったメール送信
E コマースプラットフォーム
export default defineApp({
components: {
cart: "./components/cart",
inventory: "./components/inventory",
orders: "./components/orders",
payments: "@convex-dev/polar",
shipping: "./components/shipping",
recommendations: "./components/recommendations",
},
});
AI アプリケーション
export default defineApp({
components: {
agent: "@convex-dev/agent",
embeddings: "./components/embeddings",
documents: "./components/documents",
chat: "./components/chat",
workflow: "@convex-dev/workflow",
},
});
モノリシックな構成からの移行
ステップ 1:機能を特定する
Current monolith:
- File uploads (mixed with main app)
- Rate limiting (scattered everywhere)
- Analytics (embedded in functions)
ステップ 2:機能を 1 つ切り出す
# Create component
mkdir -p convex/components/storage
# Move storage code to component
# Update imports in main app
ステップ 3:個別にテストする
# Component has its own tests
# No coupling to main app
ステップ 4:繰り返す ほかの機能も段階的に切り出します。
ベストプラクティス
1. 単一責任
各コンポーネントは 1 つのことを適切に行います。
- ✅ ストレージコンポーネントはファイルを扱います
- ✅ 認証コンポーネントは認証を扱います
- ❌ すべてを含む「utils」コンポーネントは作成しないでください
2. 明確な API サーフェス
// Export only what's needed
export { upload, download, delete } from "./storage";
// Keep internals private
// (Don't export helper functions)
3. 依存関係を最小限にする
// ✅ Good: Pass data as arguments
await components.audit.log(ctx, {
userId: user._id,
action: "delete"
});
// ❌ Bad: Component accesses parent tables
// (Not even possible, but shows the principle)
4. コンポーネントをバージョン管理する
{
"name": "@yourteam/notifications-component",
"version": "1.0.0"
}
5. コンポーネントをドキュメント化する
README に以下を記載します。
- コンポーネントの機能
- インストール方法
- 使用方法
- API リファレンス
- 例
トラブルシューティング
コンポーネントが見つからない
# Make sure component is in convex.config.ts
# Run: npx convex dev
親のテーブルにアクセスできない
This is by design! Components are sandboxed.
Pass data as arguments instead.
コンポーネントの競合
Each component has isolated tables.
Components can't see each other's data.
詳細を見る
チェックリスト
- コンポーネントディレクトリを閲覧して、既存のソリューションを確認します
- npm でコンポーネントをインストールします:
npm install @convex-dev/component-name -
convex.config.tsで設定します - 機能をカプセル化するために、兄弟コンポーネントを使用します
- 再利用可能な機能のために、独自のコンポーネントを作成します
- コンポーネントの役割を絞り込みます(単一責任)
- コンポーネントを個別にテストします
- コンポーネントの API をドキュメント化します
- コンポーネントを適切にバージョン管理します
覚えておいてください: コンポーネントの目的は、カプセル化と再利用性です。迷ったときは、モノリシックなコードよりコンポーネントを優先してください!
コメント
Markdown 対応