Advanced

Webhook with Claude Code

Aprenda sobre webhook usando o Claude Code. Dicas praticas e exemplos de codigo incluidos.

Webhook とは

Webhookはイベント発生時にHTTPリクエストで外部システムに通知する仕組みです。決済完了通知、Git pushイベント、フォーム送信など多くの場面で使われます。Claude Codeを使えば、堅牢なWebhook実装を効率的に構築できます。

Webhook受信側の実装

import express from "express";
import crypto from "crypto";

const app = express();

// rawBodyを保持するミドルウェア
app.use("/webhooks", express.raw({ type: "application/json" }));

// 署名検証
function verifyWebhookSignature(
  payload: Buffer,
  signature: string,
  secret: string
): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expected}`)
  );
}

app.post("/webhooks/stripe", async (req, res) => {
  const signature = req.headers["stripe-signature"] as string;
  const secret = process.env.STRIPE_WEBHOOK_SECRET!;

  // 署名検証
  if (!verifyWebhookSignature(req.body, signature, secret)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const event = JSON.parse(req.body.toString());

  // 冪等性チェック
  const processed = await redis.get(`webhook:${event.id}`);
  if (processed) {
    return res.json({ status: "already_processed" });
  }

  try {
    await processWebhookEvent(event);

    // 処理済みとしてマーク(24時間保持)
    await redis.set(`webhook:${event.id}`, "1", "EX", 86400);

    res.json({ status: "processed" });
  } catch (error) {
    console.error("Webhook processing failed:", error);
    res.status(500).json({ error: "Processing failed" });
  }
});

async function processWebhookEvent(event: any) {
  switch (event.type) {
    case "payment_intent.succeeded":
      await handlePaymentSuccess(event.data.object);
      break;
    case "customer.subscription.updated":
      await handleSubscriptionUpdate(event.data.object);
      break;
    case "invoice.payment_failed":
      await handlePaymentFailed(event.data.object);
      break;
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }
}

Webhook送信側の実装

interface WebhookConfig {
  id: string;
  url: string;
  secret: string;
  events: string[];
  active: boolean;
}

class WebhookSender {
  private maxRetries = 3;
  private retryDelays = [1000, 5000, 30000]; // ms

  async send(
    config: WebhookConfig,
    event: string,
    payload: Record<string, unknown>
  ): Promise<void> {
    const body = JSON.stringify({
      id: crypto.randomUUID(),
      event,
      timestamp: new Date().toISOString(),
      data: payload,
    });

    const signature = this.sign(body, config.secret);

    for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
      try {
        const response = await fetch(config.url, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "X-Webhook-Signature": `sha256=${signature}`,
            "X-Webhook-Event": event,
            "X-Webhook-Delivery": crypto.randomUUID(),
          },
          body,
          signal: AbortSignal.timeout(10000),
        });

        if (response.ok) {
          await this.logDelivery(config.id, event, "success", attempt);
          return;
        }

        if (response.status >= 400 && response.status < 500) {
          // クライアントエラーはリトライしない
          await this.logDelivery(config.id, event, "client_error", attempt);
          return;
        }

        throw new Error(`HTTP ${response.status}`);
      } catch (error) {
        if (attempt < this.maxRetries) {
          await new Promise((r) => setTimeout(r, this.retryDelays[attempt]));
        } else {
          await this.logDelivery(config.id, event, "failed", attempt);
          throw error;
        }
      }
    }
  }

  private sign(payload: string, secret: string): string {
    return crypto
      .createHmac("sha256", secret)
      .update(payload)
      .digest("hex");
  }

  private async logDelivery(
    configId: string,
    event: string,
    status: string,
    attempts: number
  ) {
    await prisma.webhookDelivery.create({
      data: { configId, event, status, attempts },
    });
  }
}

Webhook登録API

const router = express.Router();

router.post("/api/webhooks", async (req, res) => {
  const { url, events } = req.body;

  // URLの検証
  try {
    const parsed = new URL(url);
    if (parsed.protocol !== "https:") {
      return res.status(400).json({ error: "HTTPS required" });
    }
  } catch {
    return res.status(400).json({ error: "Invalid URL" });
  }

  // シークレットの生成
  const secret = crypto.randomBytes(32).toString("hex");

  const webhook = await prisma.webhookConfig.create({
    data: {
      url,
      events,
      secret,
      active: true,
      userId: req.user!.id,
    },
  });

  // シークレットは作成時のみ返す
  res.status(201).json({
    id: webhook.id,
    url: webhook.url,
    events: webhook.events,
    secret,
  });
});

イベントのディスパッチ

class EventDispatcher {
  private sender = new WebhookSender();

  async dispatch(event: string, payload: Record<string, unknown>) {
    // 該当イベントを購読しているWebhookを取得
    const configs = await prisma.webhookConfig.findMany({
      where: {
        active: true,
        events: { has: event },
      },
    });

    // 並列で送信(キューに入れる場合はBullMQを使用)
    const results = await Promise.allSettled(
      configs.map((config) => this.sender.send(config, event, payload))
    );

    const failed = results.filter((r) => r.status === "rejected");
    if (failed.length > 0) {
      console.error(`${failed.length}/${configs.length} webhooks failed`);
    }
  }
}

// Usage example
const dispatcher = new EventDispatcher();

// 注文完了時
async function completeOrder(orderId: string) {
  const order = await prisma.order.update({
    where: { id: orderId },
    data: { status: "completed" },
  });

  await dispatcher.dispatch("order.completed", {
    orderId: order.id,
    total: order.total,
    customerId: order.customerId,
  });
}

Claude Codeでの活用

Webhook実装をClaude Codeに依頼する例です。非同期処理についてはジョブキュー・非同期処理、イベント設計はイベント駆動アーキテクチャも参照してください。

Webhookシステムを実装して。
- 受信: Stripe/GitHub Webhookの署名検証
- 送信: HMAC署名付き、リトライ機能付き
- 冪等性の保証
- 配信ログとモニタリング
- 登録・管理用のREST API

WebhookのセキュリティについてはOWASP Webhook Securityを参照してください。Claude Codeの活用法は公式ドキュメントで確認できます。

Summary

Webhookはシステム間連携の重要なパターンです。Claude Codeを使えば、署名検証、リトライ、冪等性保証を含む堅牢なWebhook基盤を効率的に構築できます。

#Claude Code #Webhook #API design #security #非同期処理