convex-create-component について
再利用可能な Convex コンポーネントの設計と実装を支援するスキルです。ローカル、パッケージ、ハイブリッドの構成選択から、アプリへの組み込みと検証までを案内します。
主な機能
- 01ローカル・パッケージ・ハイブリッド構成の選択
- 02テーブルと公開 API の設計
- 03コンポーネント作成とアプリ接続
- 04認証と環境アクセスをアプリ側に維持
- 05Convex codegen と dev による検証
活用シーン
- 01アプリから再利用可能なバックエンドロジックを分離
- 02専用テーブルとワークフローを持つ連携機能の構築
- 03複数アプリでの Convex 機能の共有
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ convex/ agent-skills --skill convex-create-componentインストール後、Claude Code で/convex-create-componentを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlassets/icon.svgreferences/advanced-patterns.mdhybrid-components.mdlocal-components.mdpackaged-components.md詳細
Convex コンポーネントの作成
明確な境界と、アプリ向けのシンプルな API を備えた、再利用可能な Convex コンポーネントを作成します。
使用する場面
- 既存のアプリに新しい Convex コンポーネントを作成する場合
- 再利用可能なバックエンドロジックをコンポーネントに切り出す場合
- 独自のテーブルとワークフローを管理するサードパーティー連携を構築する場合
- 複数のアプリで再利用できるよう Convex の機能をパッケージ化する場合
使用しない場面
- メインアプリに属する、単発のビジネスロジックの場合
- Convex のテーブルや関数を必要としない、シンプルなユーティリティーの場合
convex/に置くべきアプリレベルのオーケストレーションの場合- 通常の TypeScript ライブラリで十分な場合
ワークフロー
- ユーザーが何を構築しようとしているのか、最終的な目標は何かを尋ねます。リポジトリを見れば答えが明らかな場合は、その旨を伝え、作業を進める前に確認します。
- 以下の判断表を使って形態を選び、該当するリファレンスファイルを読みます。
- コンポーネントが妥当かどうかを判断します。機能が独立したテーブル、バックエンド関数、または再利用可能な永続状態を必要としない場合は、通常のアプリコードか一般的なライブラリを優先します。
- 次の項目について簡潔な計画を立てます。
- コンポーネントが所有するテーブル
- 公開する関数
- アプリから渡す必要があるデータ(認証、環境変数、親 ID)
- ラッパーや HTTP マウントとしてアプリ側に残すもの
convex.config.ts、schema.ts、関数ファイルを含むコンポーネント構造を作成します。- 関数では、アプリの生成ファイルではなく、コンポーネント独自の
./_generated/serverインポートを使用します。 app.use(...)でコンポーネントをアプリに接続します。アプリにconvex/convex.config.tsがまだない場合は作成します。ctx.runQuery、ctx.runMutation、またはctx.runActionを使い、components.<name>経由でアプリからコンポーネントを呼び出します。- React クライアント、HTTP 呼び出し元、または公開 API からアクセスする必要がある場合は、コンポーネントの関数を直接公開するのではなく、アプリ内にラッパー関数を作成します。
npx convex devを実行し、コード生成、型、境界に関する問題を修正してから完了します。
形態を選ぶ
ユーザーに尋ねてから、次のいずれかを選びます。
| 目標 | 形態 | リファレンス |
|---|---|---|
| このアプリ専用のコンポーネント | ローカル | references/local-components.md |
| 複数のアプリで公開または共有する | パッケージ化 | references/packaged-components.md |
| ローカルコードと共有ライブラリコードの両方が必要 | ハイブリッド | references/hybrid-components.md |
| 未定の場合 | ローカルを既定に | references/local-components.md |
作業を進める前に、リファレンスファイルを1つだけ読みます。
既定の方法
ユーザーが npm パッケージを明示的に希望しない限り、ローカルコンポーネントを既定にします。
convex/components/<componentName>/の下に配置します- 独自の
convex.config.tsでdefineComponent(...)を使って定義します - アプリの
convex/convex.config.tsからapp.use(...)でインストールします npx convex devにコンポーネント独自の_generated/ファイルを生成させます
コンポーネントのスケルトン
テーブルと2つの関数、およびアプリ側の接続処理を含む、最小限のローカルコンポーネントです。
// 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.string(),
message: v.string(),
read: v.boolean(),
}).index("by_user_read", ["userId", "read"]),
});
// convex/components/notifications/lib.ts
import { v } from "convex/values";
import { mutation, query } from "./_generated/server.js";
export const send = mutation({
args: { userId: v.string(), message: v.string() },
returns: v.id("notifications"),
handler: async (ctx, args) => {
return await ctx.db.insert("notifications", {
userId: args.userId,
message: args.message,
read: false,
});
},
});
export const listUnread = query({
args: { userId: v.string() },
returns: v.array(
v.object({
_id: v.id("notifications"),
_creationTime: v.number(),
userId: v.string(),
message: v.string(),
read: v.boolean(),
}),
),
handler: async (ctx, args) => {
return await ctx.db
.query("notifications")
.withIndex("by_user_read", (q) =>
q.eq("userId", args.userId).eq("read", false),
)
.collect();
},
});
// convex/convex.config.ts
import { defineApp } from "convex/server";
import notifications from "./components/notifications/convex.config.js";
const app = defineApp();
app.use(notifications);
export default app;
// convex/notifications.ts (app-side wrapper)
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { components } from "./_generated/api";
import { getAuthUserId } from "@convex-dev/auth/server";
export const sendNotification = mutation({
args: { message: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Not authenticated");
await ctx.runMutation(components.notifications.lib.send, {
userId,
message: args.message,
});
return null;
},
});
export const myUnread = query({
args: {},
handler: async (ctx) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Not authenticated");
return await ctx.runQuery(components.notifications.lib.listUnread, {
userId,
});
},
});
リファレンスのパス形式に注意してください。convex/components/notifications/lib.ts 内の関数は、アプリから components.notifications.lib.send として呼び出します。
重要なルール
ctx.authはコンポーネント内で利用できないため、認証処理はアプリ側に置いてください。- コンポーネント関数から
process.envを読み取ることはできないため、環境変数へのアクセスはアプリ側に置いてください。 - 境界を越えて親アプリの ID を渡す場合は、文字列として渡してください。アプリ側の
ComponentApiではId型が通常の文字列になるためです。 - コンポーネントにはアプリ側のテーブル名前空間へのアクセス権がないため、コンポーネントの引数やスキーマで、アプリが所有するテーブルに対して
v.id("parentTable")を使わないでください。 query、mutation、actionは、アプリの生成ファイルではなく、コンポーネント自身の./_generated/serverからインポートしてください。- コンポーネントの関数をクライアントに直接公開しないでください。クライアントからのアクセスが必要な場合は、アプリ側にラッパーを作成してください。コンポーネントは内部用であり、アプリが提供する認証や環境変数へのアクセスの設定が必要なためです。
- コンポーネントで HTTP ハンドラーを定義する場合は、アプリの
convex/http.tsでルートをマウントしてください。コンポーネントは独自の HTTP ルートを登録できないためです。 - コンポーネントでページネーションが必要な場合は、組み込みの
.paginate()ではなく、convex-helpersのpaginatorを使用してください。.paginate()はコンポーネントの境界を越えて動作しないためです。 - データベースクエリの後に Convex の
.filter()を使うのではなく、クエリ対象のフィールドにインデックスを定義してください。 - すべての公開コンポーネント関数に
argsとreturnsのバリデーターを追加してください。コンポーネントの境界では、明示的な型コントラクトが必要なためです。
パターン
認証と環境変数へのアクセス
// Bad: component code cannot rely on app auth or env
const identity = await ctx.auth.getUserIdentity();
const apiKey = process.env.OPENAI_API_KEY;
// Good: the app resolves auth and env, then passes explicit values
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Not authenticated");
await ctx.runAction(components.translator.translate, {
userId,
apiKey: process.env.OPENAI_API_KEY,
text: args.text,
});
クライアント向け API
// Bad: assuming a component function is directly callable by clients
export const send = components.notifications.send;
// Good: re-export through an app mutation or query
export const sendNotification = mutation({
args: { message: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Not authenticated");
await ctx.runMutation(components.notifications.lib.send, {
userId,
message: args.message,
});
return null;
},
});
境界を越える ID
// Bad: parent app table IDs are not valid component validators
args: {
userId: v.id("users"),
}
// Good: treat parent-owned IDs as strings at the boundary
args: {
userId: v.string(),
}
高度なパターン
コールバック用の関数ハンドル、スキーマからのバリデーターの導出、globals テーブルを使った静的設定、クラスベースのクライアントラッパーなどの追加パターンについては、references/advanced-patterns.md を参照してください。
検証
次の順番で検証してください。
npx convex codegen --component-dir convex/components/<name>npx convex codegennpx convex dev
重要事項:
- 新しいリポジトリでは、
CONVEX_DEPLOYMENTを設定するまで、これらのコマンドが失敗することがあります。 - codegen を実行するまでは、コンポーネント内の
./_generated/*のインポートと、アプリ側のcomponents.<name>...参照は型チェックに失敗します。 - Convex へのログインまたはデプロイの設定が原因で検証が止まった場合は、推測せずにそこで中断し、その具体的な手順をユーザーに確認してください。
参考ファイル
ユーザーが目的を確認した後、次のいずれか1つだけを読んでください。
references/local-components.mdreferences/packaged-components.mdreferences/hybrid-components.md
公式ドキュメント: コンポーネントの作成
チェックリスト
- ユーザーに何を作りたいかを尋ね、内容を確認した
- 該当する参考ファイルを読んだ
- コンポーネントが適切な抽象化であることを確認した
- テーブル、公開 API、境界、アプリ側のラッパーを計画した
- コンポーネントが
convex/components/<name>/に配置されている(公開する場合はパッケージレイアウト) - コンポーネントが自身の
./_generated/serverからインポートしている - 認証、環境変数へのアクセス、HTTP ルートがアプリ側に置かれている
- 親アプリの ID が
v.string()として境界を越えている - 公開関数に
argsとreturnsのバリデーターがある -
npx convex devを実行し、codegen または型の問題を修正した
コメント
Markdown 対応