convex-functions について
Convex のクエリ、ミューテーション、アクション、HTTP アクションの実装ガイドです。データアクセス、検証、スケジューリング、エラー処理を扱い、バックエンド関数の作成やレビューに役立ちます。
主な機能
- 01クエリ、ミューテーション、アクションの役割を区別
- 02引数と戻り値のバリデーターを宣言
- 03インデックス読み取りとページネーションを説明
- 04内部関数と安全なスケジューリングを解説
- 05アクションのランタイムとエラー処理を案内
活用シーン
- 01Convex のデータ読み取りと書き込みを実装
- 02外部リクエストをアクションで処理
- 03内部関数を使ったスケジュール処理を設定
- 04検証、ページネーション、エラー処理の問題を調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ waynesutton/ builder-skills --skill convex-functionsインストール後、Claude Code で/convex-functionsを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yamlassets/small-logo.svg
詳細
Convex 関数
convex/ 内でエクスポートする関数はすべて、args と returns のバリデーターを含むオブジェクト形式を使います。ハンドラーが行う操作に応じて型を選びます。クエリは読み取り、ミューテーションは書き込み、アクションは外部への呼び出しを行います。
関数の種類を選ぶ
| 種類 | データベース | 外部呼び出し | 呼び出し可能な対象 | 用途 |
|---|---|---|---|---|
query | 読み取り | なし | クライアント、他の関数 | 読み取り。キャッシュされ、リアクティブです。 |
mutation | 読み取りと書き込み | なし | クライアント、他の関数 | 書き込み。1つのトランザクションで実行します。 |
action | runQuery と runMutation 経由のみ | あり | クライアント、スケジューラー、他のアクション | fetch、サードパーティ SDK、Node API |
internalQuery、internalMutation、internalAction | 公開形式と同じ | 同じ | 他の Convex 関数のみ | スケジュールされた処理、cron、権限が必要な書き込み |
httpAction | runQuery と runMutation 経由のみ | あり | convex/http.ts への HTTP リクエスト | Webhook、REST エンドポイント |
基本的には query または mutation を使います。ハンドラーが Convex の外部と通信する必要がある場合にのみ、action を使います。
オブジェクト形式
すべての関数で args と returns を宣言します。何も返さない関数では returns: v.null() を宣言し、null を返します。複数の関数が同じ形式を返す場合は、共通のドキュメントバリデーターを関数の外で定義します。
// convex/tasks.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
const taskValidator = v.object({
_id: v.id("tasks"),
_creationTime: v.number(),
userId: v.id("users"),
title: v.string(),
completed: v.boolean(),
});
export const get = query({
args: { taskId: v.id("tasks") },
returns: v.union(taskValidator, v.null()),
handler: async (ctx, args) => {
return await ctx.db.get(args.taskId);
},
});
export const remove = mutation({
args: { taskId: v.id("tasks") },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.delete(args.taskId);
return null;
},
});
データの読み取り
IDで1つのドキュメントを取得するには、ctx.db.get(id) を使います。それ以外の場合は、convex/schema.ts で定義したインデックスに対して withIndex を使います。テーブルクエリに .filter() を使わないでください。テーブル全体をスキャンします。
export const listByUser = query({
args: { userId: v.id("users") },
returns: v.array(taskValidator),
handler: async (ctx, args) => {
return await ctx.db
.query("tasks")
.withIndex("by_user", (q) => q.eq("userId", args.userId))
.order("desc")
.take(50);
},
});
想定するドキュメント数に応じて、最後に呼び出すメソッドを選びます。
| メソッド | 戻り値 | 用途 |
|---|---|---|
.unique() | 1つのドキュメントまたは null。複数ある場合はエラーになります | インデックスによって一致する項目が最大1つと保証される場合 |
.first() | 最初のドキュメントまたは null | 最新または最古の一致項目を取得する場合 |
.take(n) | 最大 n 件のドキュメント | 最近のフィードなど、件数に上限のあるリスト |
.collect() | 一致するすべての項目 | 結果セットが小さく、今後も小さいままの場合 |
.paginate(opts) | ページとカーソル | テーブルの件数に上限がない場合 |
ページネーションを使うクエリでは、引数として paginationOpts: paginationOptsValidator(convex/server から)を受け取ります。
データの書き込み
| メソッド | 動作 |
|---|---|
ctx.db.insert("tasks", doc) | 挿入し、新しい ID を返します |
ctx.db.patch(id, fields) | フィールドを浅くマージします。ドキュメントがない場合はエラーになります |
ctx.db.replace(id, doc) | ドキュメント全体を置き換えます。ドキュメントがない場合はエラーになります |
ctx.db.delete(id) | ドキュメントを削除します |
以前の値が不要なら、直接 patch してください。先に読み取ると、書き込み競合が起きる可能性のある時間が長くなります。ミューテーションは安全に再試行できるようにしてください。
export const rename = mutation({
args: { taskId: v.id("tasks"), title: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.taskId, { title: args.title });
return null;
},
});
内部関数と参照
query、mutation、action は公開関数です。デプロイ URL を知っている人なら誰でも呼び出せます。他の Convex コードからのみ実行するコード(スケジュールされたジョブ、cron、Webhook ハンドラー、権限が必要な書き込み)には、internalQuery、internalMutation、internalAction を使います。
./_generated/api 内の生成されたオブジェクトを通じて、関数を参照します。
api.tasks.getはconvex/tasks.ts内の公開関数を参照しますinternal.tasks.markPaidは同じファイル内の内部関数を参照します- フォルダーはパスに対応します。
convex/billing/invoices.tsはapi.billing.invoices.listになります
必ず internal.* をスケジュールしてください。スケジュールされた関数と cron はクライアントなしで実行されるため、公開参照を使うと、クライアントからの呼び出しなら実行される認証チェックが省略されます。
// convex/messages.ts
import { mutation, internalMutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
export const send = mutation({
args: { channelId: v.id("channels"), content: v.string() },
returns: v.id("messages"),
handler: async (ctx, args) => {
const messageId = await ctx.db.insert("messages", args);
await ctx.scheduler.runAfter(0, internal.messages.notifySubscribers, {
channelId: args.channelId,
messageId,
});
return messageId;
},
});
export const notifySubscribers = internalMutation({
args: { channelId: v.id("channels"), messageId: v.id("messages") },
returns: v.null(),
handler: async (ctx, args) => {
const subs = await ctx.db
.query("subscriptions")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.collect();
await Promise.all(
subs.map((sub) =>
ctx.db.insert("notifications", {
userId: sub.userId,
messageId: args.messageId,
read: false,
}),
),
);
return null;
},
});
アクションとランタイムの境界
アクションには ctx.db がありません。ctx.runQuery で読み取り、ctx.runMutation で書き込みます。呼び出しごとに別のトランザクションになるため、呼び出し回数を抑え、関連する読み取りと書き込みは1つのミューテーション内で行ってください。
fetch はデフォルトのランタイムで動作します。アクションで Node 組み込み機能または Node 専用 SDK が必要な場合に限り、ファイルの1行目に "use node"; を追加します。"use node" ファイルからエクスポートできるのはアクションのみです。クエリとミューテーションは別のファイルに置きます。
// convex/orders.ts (default runtime)
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { v, ConvexError } from "convex/values";
import { Doc } from "./_generated/dataModel";
export const charge = action({
args: { orderId: v.id("orders") },
returns: v.null(),
handler: async (ctx, args) => {
// Same file call: annotate the result so TypeScript does not hit a circular type
const order: Doc<"orders"> | null = await ctx.runQuery(
internal.orders.getForCharge,
{ orderId: args.orderId },
);
if (!order) {
throw new ConvexError("Order not found");
}
const res = await fetch("https://api.payments.example/charge", {
method: "POST",
body: JSON.stringify({ amount: order.total }),
});
await ctx.runMutation(internal.orders.setStatus, {
orderId: args.orderId,
status: res.ok ? "paid" : "failed",
});
return null;
},
});
Doc と Id は ./_generated/dataModel から取得します。注釈が必要なのは、呼び出す関数が同じファイルにある場合のみです。
エラー
クライアントが参照すべき内容には、convex/values の ConvexError をスローしてください。その data はクライアントに届きますが、通常の Error のメッセージは本番環境では伏せられます。見つからないレコードの検索など、想定される不在には null を返してください。実際の失敗、つまり未認証、権限不足、無効な入力の場合はスローしてください。
import { ConvexError } from "convex/values";
throw new ConvexError({ code: "NOT_FOUND", message: "Task not found" });
薄いラッパー
ハンドラーは短く保ってください。認証情報の取得、検証、ビジネスロジックは、最初の引数に ctx を取る通常の非同期関数にまとめ、ラッパーから呼び出してください。通常のヘルパーはテストしやすく、ctx.runQuery を経由せずにクエリとミューテーションの両方で共有できます。
import { QueryCtx, MutationCtx } from "./_generated/server";
import { ConvexError } from "convex/values";
export async function getCurrentUser(ctx: QueryCtx | MutationCtx) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new ConvexError("Not authenticated");
}
const user = await ctx.db
.query("users")
.withIndex("by_token", (q) =>
q.eq("tokenIdentifier", identity.tokenIdentifier),
)
.unique();
if (!user) {
throw new ConvexError("User not found");
}
return user;
}
クエリまたはミューテーションからは、ヘルパーを直接呼び出してください。ctx.runQuery と ctx.runMutation は、アクションやコンポーネントの境界で使用します。
よくある間違い
| 間違い | 問題が起きる理由 | 代わりに行うこと |
|---|---|---|
returns バリデーターを指定しない | 返却値の形が変化し、クライアントの型が実態と異なる | returns を宣言し、何も返さない場合は v.null() を使う |
テーブルクエリで .filter() を使う | テーブル全体をスキャンする | インデックスを追加し、withIndex を使う |
アクション内で ctx.db を使う | アクションにはデータベースハンドルがない | ctx.runQuery と ctx.runMutation を使う |
クエリまたはミューテーション内で fetch を使う | トランザクションの決定性が損なわれる | アクションに移す |
api.* をスケジュールする | クライアントなしで公開コードを実行し、認証をスキップする | internal.* をスケジュールする |
クエリを含むファイルで "use node" を使う | バンドラーがファイルを拒否する | アクションを別のファイルに分ける |
クエリで Date.now() を使う | キャッシュとリアクティビティが損なわれる | 時刻を引数として渡すか、ステータスフィールドに保存する |
1つのアクションから runQuery を何度も呼び出す | 呼び出しごとに別のトランザクションとなり、競合が発生する | 関連する処理を1つのミューテーションにまとめる |
ユーザー向けメッセージに通常の Error を使う | 本番環境ではメッセージが隠される | ConvexError を使う |
ctx.db またはスケジューラー呼び出しに await を付けない | 書き込みがコミットされない場合がある | すべての ctx 呼び出しを await する |
チェックリスト
- エクスポートするすべての関数で、
argsとreturnsを持つオブジェクト形式を使う - 返すものがない場合は、
returns: v.null()とreturn nullを使う - 読み取りには
ctx.db.get(id)またはwithIndexを使い、.filter()は使わない - 件数に上限のないテーブルでは
.collect()ではなく、.paginate()または.take(n)を使う - ミューテーションでは直接パッチし、安全に再試行できるようにする
- スケジュールされた処理と cron の呼び出し先には
internal.*を指定する - アクションでは
ctx.dbに触れない -
"use node"は、アクションをエクスポートし、Node が必要なファイルでのみ使う - 同じファイル内の
runQueryとrunMutationの結果には型注釈を付ける - クライアントに表示するエラーには
ConvexErrorを使う - すべての
ctx.*の Promise をawaitする