mcpField
C
components-guide

components-guide

github.com/get-convex開発69546 回インストール1 日前に更新
リポジトリ

components-guide について

Convex 開発者向けに、独自のデータと API 境界を持つバックエンドコンポーネントの設計・利用方法を解説します。導入設定、連携パターン、再利用可能なコンポーネントの作成を扱います。

主な機能

  1. 01データと関数の分離を解説
  2. 02インストールとアプリ設定の説明
  3. 03親・兄弟コンポーネントの連携例
  4. 04独自コンポーネントの構成例
  5. 05API 境界と再利用の指針

活用シーン

  1. 01ストレージや通知機能のコンポーネント化
  2. 02アプリへの npm コンポーネント導入
  3. 03複数プロジェクトで再利用する機能の設計

インストール

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

$npx skills add https://github.com/get-convex/agent-skills --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;
  },
});

コンポーネント間の通信パターン

親からコンポーネントへ(良い例)
// 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
兄弟から兄弟へ(悪い例)

コンポーネント同士が直接呼び出し合うことはできません。これが必要な場合は、メインアプリに実装するか、設計を見直してください。

ベストプラクティス

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 リファレンス
  • 使用例

チェックリスト

  • コンポーネントディレクトリで既存のソリューションを確認する
  • npm からコンポーネントをインストールする:npm install @convex-dev/component-name
  • convex.config.ts で設定する
  • 機能をカプセル化するために兄弟コンポーネントを使用する
  • 再利用可能な機能には独自のコンポーネントを作成する
  • コンポーネントの責務を絞る(単一責任)
  • コンポーネントを個別にテストする
  • コンポーネントの API をドキュメント化する
  • コンポーネントを適切にバージョン管理する

コメント