jpskill.com
🛠️ 開発・MCP コミュニティ

convex-http

Convex環境で、Webhook処理(StripeやGitHub連携など)、REST APIエンドポイント作成、ファイル送受信、ブラウザからのCORS対応などをスムーズに行えるようにHTTP通信を支援するSkill。

📜 元の英語説明(参考)

HTTP actions for webhooks and API endpoints in Convex. Use when building webhook handlers (Stripe, Clerk, GitHub), creating REST API endpoints, handling file uploads/downloads, or implementing CORS for browser requests.

🇯🇵 日本人クリエイター向け解説

一言でいうと

Convex環境で、Webhook処理(StripeやGitHub連携など)、REST APIエンドポイント作成、ファイル送受信、ブラウザからのCORS対応などをスムーズに行えるようにHTTP通信を支援するSkill。

※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。

⚡ おすすめ: コマンド1行でインストール(60秒)

下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。

🍎 Mac / 🐧 Linux
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o convex-http.zip https://jpskill.com/download/8731.zip && unzip -o convex-http.zip && rm convex-http.zip
🪟 Windows (PowerShell)
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/8731.zip -OutFile "$d\convex-http.zip"; Expand-Archive "$d\convex-http.zip" -DestinationPath $d -Force; ri "$d\convex-http.zip"

完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。

💾 手動でダウンロードしたい(コマンドが難しい人向け)
  1. 1. 下の青いボタンを押して convex-http.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → convex-http フォルダができる
  3. 3. そのフォルダを C:\Users\あなたの名前\.claude\skills\(Win)または ~/.claude/skills/(Mac)へ移動
  4. 4. Claude Code を再起動

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。

詳しい使い方ガイドを見る →
最終更新
2026-05-18
取得日時
2026-05-18
同梱ファイル
1

📖 Skill本文(日本語訳)

※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。

Convex HTTP Actions

基本的な HTTP ルーター

// convex/http.ts
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";

const http = httpRouter();

http.route({
  path: "/health",
  method: "GET",
  handler: httpAction(async () => {
    return new Response(JSON.stringify({ status: "ok" }), {
      status: 200,
      headers: { "Content-Type": "application/json" },
    });
  }),
});

export default http;

Webhook の処理

// convex/http.ts
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { internal } from "./_generated/api";

const http = httpRouter();

http.route({
  path: "/webhooks/stripe",
  method: "POST",
  handler: httpAction(async (ctx, request) => {
    const signature = request.headers.get("stripe-signature");
    if (!signature) {
      return new Response("Missing signature", { status: 400 });
    }

    const body = await request.text();

    try {
      await ctx.runAction(internal.stripe.verifyAndProcess, { body, signature });
      return new Response("OK", { status: 200 });
    } catch (error) {
      return new Response("Webhook error", { status: 400 });
    }
  }),
});

export default http;

Webhook シグネチャの検証

// convex/stripe.ts
"use node";

import { internalAction } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export const verifyAndProcess = internalAction({
  args: { body: v.string(), signature: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const event = stripe.webhooks.constructEvent(
      args.body,
      args.signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );

    switch (event.type) {
      case "checkout.session.completed":
        await ctx.runMutation(internal.payments.handleCheckout, {
          sessionId: event.data.object.id,
        });
        break;
    }
    return null;
  },
});

CORS の設定

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
};

// プリフライトの処理
http.route({
  path: "/api/data",
  method: "OPTIONS",
  handler: httpAction(async () => {
    return new Response(null, { status: 204, headers: corsHeaders });
  }),
});

// 実際のエンドポイント
http.route({
  path: "/api/data",
  method: "POST",
  handler: httpAction(async (ctx, request) => {
    const body = await request.json();
    return new Response(JSON.stringify({ success: true }), {
      status: 200,
      headers: { "Content-Type": "application/json", ...corsHeaders },
    });
  }),
});

パスパラメータ

動的なルートには pathPrefix を使用します。

http.route({
  pathPrefix: "/api/users/",
  method: "GET",
  handler: httpAction(async (ctx, request) => {
    const url = new URL(request.url);
    const userId = url.pathname.replace("/api/users/", "");

    const user = await ctx.runQuery(internal.users.get, { userId });
    if (!user) return new Response("Not found", { status: 404 });

    return Response.json(user);
  }),
});

API キー認証

http.route({
  path: "/api/protected",
  method: "GET",
  handler: httpAction(async (ctx, request) => {
    const apiKey = request.headers.get("X-API-Key");
    if (!apiKey) {
      return Response.json({ error: "Missing API key" }, { status: 401 });
    }

    const isValid = await ctx.runQuery(internal.auth.validateApiKey, { apiKey });
    if (!isValid) {
      return Response.json({ error: "Invalid API key" }, { status: 403 });
    }

    const data = await ctx.runQuery(internal.data.getProtected, {});
    return Response.json(data);
  }),
});

ファイルのアップロード

http.route({
  path: "/api/upload",
  method: "POST",
  handler: httpAction(async (ctx, request) => {
    const bytes = await request.bytes();
    const contentType = request.headers.get("Content-Type") ?? "application/octet-stream";

    const blob = new Blob([bytes], { type: contentType });
    const storageId = await ctx.storage.store(blob);

    return Response.json({ storageId });
  }),
});

ファイルのダウンロード

http.route({
  pathPrefix: "/files/",
  method: "GET",
  handler: httpAction(async (ctx, request) => {
    const url = new URL(request.url);
    const fileId = url.pathname.replace("/files/", "") as Id<"_storage">;

    const fileUrl = await ctx.storage.getUrl(fileId);
    if (!fileUrl) return new Response("Not found", { status: 404 });

    return Response.redirect(fileUrl, 302);
  }),
});

エラー処理ヘルパー

function jsonResponse(data: unknown, status = 200) {
  return new Response(JSON.stringify(data), {
    status,
    headers: { "Content-Type": "application/json" },
  });
}

http.route({
  path: "/api/process",
  method: "POST",
  handler: httpAction(async (ctx, request) => {
    try {
      const body = await request.json();
      if (!body.data) {
        return jsonResponse({ error: "Missing data field" }, 400);
      }
      const result = await ctx.runMutation(internal.process.handle, body);
      return jsonResponse({ success: true, result });
    } catch (error) {
      return jsonResponse({ error: "Internal server error" }, 500);
    }
  }),
});

参考文献

📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開

Convex HTTP Actions

Basic HTTP Router

// convex/http.ts
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";

const http = httpRouter();

http.route({
  path: "/health",
  method: "GET",
  handler: httpAction(async () => {
    return new Response(JSON.stringify({ status: "ok" }), {
      status: 200,
      headers: { "Content-Type": "application/json" },
    });
  }),
});

export default http;

Webhook Handling

// convex/http.ts
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { internal } from "./_generated/api";

const http = httpRouter();

http.route({
  path: "/webhooks/stripe",
  method: "POST",
  handler: httpAction(async (ctx, request) => {
    const signature = request.headers.get("stripe-signature");
    if (!signature) {
      return new Response("Missing signature", { status: 400 });
    }

    const body = await request.text();

    try {
      await ctx.runAction(internal.stripe.verifyAndProcess, { body, signature });
      return new Response("OK", { status: 200 });
    } catch (error) {
      return new Response("Webhook error", { status: 400 });
    }
  }),
});

export default http;

Webhook Signature Verification

// convex/stripe.ts
"use node";

import { internalAction } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export const verifyAndProcess = internalAction({
  args: { body: v.string(), signature: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const event = stripe.webhooks.constructEvent(
      args.body,
      args.signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );

    switch (event.type) {
      case "checkout.session.completed":
        await ctx.runMutation(internal.payments.handleCheckout, {
          sessionId: event.data.object.id,
        });
        break;
    }
    return null;
  },
});

CORS Configuration

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
};

// Handle preflight
http.route({
  path: "/api/data",
  method: "OPTIONS",
  handler: httpAction(async () => {
    return new Response(null, { status: 204, headers: corsHeaders });
  }),
});

// Actual endpoint
http.route({
  path: "/api/data",
  method: "POST",
  handler: httpAction(async (ctx, request) => {
    const body = await request.json();
    return new Response(JSON.stringify({ success: true }), {
      status: 200,
      headers: { "Content-Type": "application/json", ...corsHeaders },
    });
  }),
});

Path Parameters

Use pathPrefix for dynamic routes:

http.route({
  pathPrefix: "/api/users/",
  method: "GET",
  handler: httpAction(async (ctx, request) => {
    const url = new URL(request.url);
    const userId = url.pathname.replace("/api/users/", "");

    const user = await ctx.runQuery(internal.users.get, { userId });
    if (!user) return new Response("Not found", { status: 404 });

    return Response.json(user);
  }),
});

API Key Authentication

http.route({
  path: "/api/protected",
  method: "GET",
  handler: httpAction(async (ctx, request) => {
    const apiKey = request.headers.get("X-API-Key");
    if (!apiKey) {
      return Response.json({ error: "Missing API key" }, { status: 401 });
    }

    const isValid = await ctx.runQuery(internal.auth.validateApiKey, { apiKey });
    if (!isValid) {
      return Response.json({ error: "Invalid API key" }, { status: 403 });
    }

    const data = await ctx.runQuery(internal.data.getProtected, {});
    return Response.json(data);
  }),
});

File Upload

http.route({
  path: "/api/upload",
  method: "POST",
  handler: httpAction(async (ctx, request) => {
    const bytes = await request.bytes();
    const contentType = request.headers.get("Content-Type") ?? "application/octet-stream";

    const blob = new Blob([bytes], { type: contentType });
    const storageId = await ctx.storage.store(blob);

    return Response.json({ storageId });
  }),
});

File Download

http.route({
  pathPrefix: "/files/",
  method: "GET",
  handler: httpAction(async (ctx, request) => {
    const url = new URL(request.url);
    const fileId = url.pathname.replace("/files/", "") as Id<"_storage">;

    const fileUrl = await ctx.storage.getUrl(fileId);
    if (!fileUrl) return new Response("Not found", { status: 404 });

    return Response.redirect(fileUrl, 302);
  }),
});

Error Handling Helper

function jsonResponse(data: unknown, status = 200) {
  return new Response(JSON.stringify(data), {
    status,
    headers: { "Content-Type": "application/json" },
  });
}

http.route({
  path: "/api/process",
  method: "POST",
  handler: httpAction(async (ctx, request) => {
    try {
      const body = await request.json();
      if (!body.data) {
        return jsonResponse({ error: "Missing data field" }, 400);
      }
      const result = await ctx.runMutation(internal.process.handle, body);
      return jsonResponse({ success: true, result });
    } catch (error) {
      return jsonResponse({ error: "Internal server error" }, 500);
    }
  }),
});

References