Webhook
外部サービスが呼び出せる HTTP エンドポイントを公開します。
Webhook を使う理由
アプリケーションが外部サービスから HTTP リクエストを受け取る必要があるケースがあります。たとえば、決済プロバイダが課金完了を通知する、ソース管理プラットフォームが新しいコミットを報告する、監視ツールがアラートを送信する、といった場合です。
Webhook では、関数にデコレータを付けてデプロイするだけです。
// A decorated method that handles incoming HTTP requests
@webhook('handleStripePayment')
async handleStripePayment(request: WebhookRequest): Promise<any> {
const invoiceId = request.body.data.object.id;
const customerId = request.body.data.object.customer;
await this.recordPayment(customerId, invoiceId);
return this.createWebhookResponse({ received: true }, 200);
}
ルーティング不要。Express サーバ不要。URL の背後にある関数だけ。
概要
Webhook は HTTP エンドポイントとして公開されるバックエンド関数です。外部サービスはこれらのエンドポイントにリクエストを送信し、あなたの関数が受信データを処理します。Squid は URL ルーティング、リクエストのパース、レスポンスのシリアライズを担当します。
Webhook を使うべきとき
| ユースケース | 推奨 |
|---|---|
| 外部サービスから HTTP リクエストを受け取る | ✅ Webhook |
| Squid client から関数を呼び出す | Executables を使用 |
| データベースの変更に反応する | Triggers を使用 |
| スケジュール実行する | Schedulers を使用 |
仕組み
SquidServiceを継承したクラス内のメソッドに@webhook('webhookId')を付与します- Squid がデプロイ時に webhook を検出して登録します
- Squid が webhook を HTTP エンドポイントとして公開します
- 外部サービスがエンドポイント URL にリクエストを送信します
- あなたの関数がリクエストを受け取り、レスポンスを返します
クイックスタート
前提条件
squid init-backendで初期化された Squid backend プロジェクト@squidcloud/backendパッケージがインストールされていること
Step 1: Webhook を作成する
SquidService を継承するサービスクラスを作成し、Webhook 関数を追加します。
import { SquidService, webhook, WebhookRequest } from '@squidcloud/backend';
export class ExampleService extends SquidService {
@webhook('hello')
async hello(request: WebhookRequest): Promise<any> {
const name = request.queryParams['name'] || 'World';
return this.createWebhookResponse({ message: `Hello, ${name}!` }, 200);
}
}
Step 2: サービスを export する
サービスが service の index ファイルから export されていることを確認します。
export * from './example-service';
Step 3: バックエンドを起動またはデプロイする
ローカル開発では、Squid CLI を使ってバックエンドをローカル実行します。
squid start
クラウドにデプロイするには、deploying your backend を参照してください。
Step 4: Webhook をテストする
デプロイ後、Webhook は次の URL で利用できます。
https://[YOUR_APP_ID].[APP_REGION].squid.cloud/webhooks/hello?name=Squid
curl "https://[YOUR_APP_ID].[APP_REGION].squid.cloud/webhooks/hello?name=Squid"
レスポンス:
{ "message": "Hello, Squid!" }
コアコンセプト
Webhook URL 形式
デプロイ後、各 webhook は ID に基づく URL でアクセスできます。
Production:
https://[YOUR_APP_ID].[APP_REGION].squid.cloud/webhooks/[WEBHOOK_ID]
Dev environment:
https://[YOUR_APP_ID]-dev.[APP_REGION].squid.cloud/webhooks/[WEBHOOK_ID]
Local development:
https://[YOUR_APP_ID]-dev-[YOUR_SQUID_DEVELOPER_ID].[APP_REGION].squid.cloud/webhooks/[WEBHOOK_ID]
WebhookRequest オブジェクト
すべての webhook 関数は、HTTP コンテキスト全体を含む WebhookRequest オブジェクトを受け取ります。
@webhook('inspectRequest')
async inspectRequest(request: WebhookRequest): Promise<any> {
console.log('HTTP method:', request.httpMethod);
console.log('Body:', request.body);
console.log('Query params:', request.queryParams);
console.log('Headers:', request.headers);
console.log('Raw body:', request.rawBody);
console.log('Files:', request.files);
return { received: true };
}
| プロパティ | 型 | 説明 |
|---|---|---|
body | any | パース済みのリクエストボディ |
rawBody | string | undefined | 未パースのリクエストボディ(文字列)。署名検証に便利 |
queryParams | Record<string, string> | URL クエリパラメータ |
headers | Record<string, string> | HTTP ヘッダー(キーは小文字) |
httpMethod | 'post' | 'get' | 'put' | 'delete' | リクエストの HTTP メソッド |
files | SquidFile[] | undefined | リクエストに含まれるアップロードファイル |
レスポンスの作成
Webhook からレスポンスを返す方法は 2 つあります。
値を直接 return する:
JSON にシリアライズ可能な return 値は、200 ステータスコードのレスポンスボディとして送信されます。
@webhook('simpleResponse')
async simpleResponse(request: WebhookRequest): Promise<any> {
return { status: 'ok', timestamp: Date.now() };
}
createWebhookResponse を使って完全に制御する:
ステータスコード、ヘッダー、ボディを明示的に設定します。
@webhook('customResponse')
async customResponse(request: WebhookRequest): Promise<any> {
const data = await this.processData(request.body);
return this.createWebhookResponse(
{ result: data }, // body
201, // status code
{ 'X-Request-Id': '123' } // custom headers
);
}
throwWebhookResponse を使って即時に返す:
任意のタイミングで実行を中断し、レスポンスを返します。早期のバリデーション失敗に有用です。
@webhook('validateAndProcess')
async validateAndProcess(request: WebhookRequest): Promise<any> {
if (!request.body?.orderId) {
// Immediately returns a 400 response
this.throwWebhookResponse({
body: { error: 'Missing orderId' },
statusCode: 400,
});
}
const result = await this.processOrder(request.body.orderId);
return this.createWebhookResponse({ result }, 200);
}
対応している HTTP メソッド
Webhook は GET、POST、PUT、DELETE リクエストを受け付けます。request.httpMethod を確認してメソッド別に処理できます。
@webhook('resource')
async resource(request: WebhookRequest): Promise<any> {
switch (request.httpMethod) {
case 'get':
return this.getResource(request.queryParams['id']);
case 'post':
return this.createResource(request.body);
case 'delete':
return this.deleteResource(request.queryParams['id']);
default:
return this.createWebhookResponse({ error: 'Method not allowed' }, 405);
}
}
ファイルアップロード
Webhook はファイルアップロードも受信できます。ファイルは request.files 配列上の SquidFile オブジェクトとして利用できます。
@webhook('uploadFile')
async uploadFile(request: WebhookRequest): Promise<any> {
const files = request.files || [];
if (files.length === 0) {
return this.createWebhookResponse({ error: 'No files provided' }, 400);
}
const file = files[0];
console.log('Filename:', file.originalName);
console.log('MIME type:', file.mimetype);
console.log('Size:', file.size);
// Access file content as Uint8Array
const content = new TextDecoder().decode(file.data);
return { filename: file.originalName, size: file.size };
}
Squid client から Webhook を呼び出す
squid.executeWebhook を使って Squid client から webhook を呼び出すこともできます。
const result = await squid.executeWebhook('hello', {
queryParams: { name: 'Squid' },
});
console.log(result); // { message: "Hello, Squid!" }
const result = await squid.executeWebhook('processOrder', {
body: { orderId: 'order-123', items: ['item-1', 'item-2'] },
headers: { 'X-Idempotency-Key': 'unique-key-123' },
});
エラーハンドリング
エラーを throw する
Webhook 関数が未処理のエラーを throw すると、Squid は 500 レスポンスを返します。throwWebhookResponse または try/catch を使い、意味のあるエラーレスポンスを返してください。
@webhook('processPayment')
async processPayment(request: WebhookRequest): Promise<any> {
try {
if (!request.body?.amount) {
return this.createWebhookResponse({ error: 'Missing amount' }, 400);
}
const result = await this.chargeCustomer(request.body);
return this.createWebhookResponse({ result }, 200);
} catch (error) {
console.error('Payment processing failed:', error);
return this.createWebhookResponse({ error: 'Internal error' }, 500);
}
}
Webhook 署名の検証
多くの外部サービスは webhook ペイロードに署名し、真正性を検証できるようにしています。request.rawBody と request.headers を使って署名を検証してください。
import * as crypto from 'crypto';
@webhook('verifiedWebhook')
async verifiedWebhook(request: WebhookRequest): Promise<any> {
const signature = request.headers['x-signature'];
const secret = this.secrets['WEBHOOK_SIGNING_SECRET'] as string;
if (!this.verifySignature(request.rawBody || '', signature, secret)) {
return this.createWebhookResponse({ error: 'Invalid signature' }, 401);
}
// Signature is valid, process the event
return this.handleEvent(request.body);
}
private verifySignature(rawBody: string, signature: string, secret: string): boolean {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
よくあるエラー
| エラー | 原因 | 解決策 |
|---|---|---|
| Webhook not found (404) | Webhook ID がどの @webhook とも一致しない | スペルを確認し、サービスが export されていることを確認 |
| 500 response | webhook 関数内の未処理エラー | try/catch を追加し、意味のあるエラーレスポンスを返す |
| Empty response body | 関数が undefined を返している | 値を返すか createWebhookResponse を使う |
| Incorrect URL | app ID、region、environment が間違っている | .env の SQUID_APP_ID と SQUID_REGION が正しいか確認 |
レート制限(Rate Limiting)
@limits デコレータを使って、Webhook を悪用から保護できます。レートおよびクォータ制限の詳細は、Rate and quota limiting を参照してください。
ベストプラクティス
-
適切なステータスコードを返す。
createWebhookResponseを使い、意味のある HTTP ステータスコードを返してください(成功は 200、不正入力は 400、未認証は 401、サーバエラーは 500)。 -
Webhook 署名を検証する。 外部サービスから webhook を受け取る場合、
request.rawBodyとサービスの signing secret を使って、常にリクエスト署名を検証してください。 -
素早く応答する。 外部サービスにはタイムアウト制限があることが一般的です。処理に時間がかかる場合は、Webhook には即時に受領応答し、データ処理は非同期で行ってください。
-
冪等性(idempotency)を前提に設計する。 外部サービスは webhook 配信をリトライする場合があります。リクエストボディ内の一意識別子を使って重複配信を検出し、スキップしてください。
-
入力を早期に検証する。 webhook 関数の冒頭で必須フィールドを確認してください。
throwWebhookResponseを使い、処理を始める前にエラーを返せます。 -
受信リクエストをログに残す。 デバッグに役立つよう、Webhook のイベントタイプや主要な識別子をログに記録してください。production ではリクエストボディ全文などの機微情報をログに出さないようにしてください。
コード例
決済イベントを処理する
import { SquidService, webhook, WebhookRequest } from '@squidcloud/backend';
interface PaymentEvent {
type: string;
data: {
object: {
id: string;
customer: string;
amount: number;
status: string;
};
};
}
export class PaymentService extends SquidService {
@webhook('handlePayment')
async handlePayment(request: WebhookRequest<PaymentEvent>): Promise<any> {
const event = request.body;
if (event.type !== 'payment_intent.succeeded') {
return this.createWebhookResponse({ received: true }, 200);
}
const payment = event.data.object;
const payments = this.squid.collection('payments');
await payments.doc(payment.id).insert({
customerId: payment.customer,
amount: payment.amount,
status: payment.status,
createdAt: new Date().toISOString(),
});
console.log(`Recorded payment ${payment.id} for customer ${payment.customer}`);
return this.createWebhookResponse({ received: true }, 200);
}
}
Stripe の webhook チュートリアル全体は Stripe and Squid Webhooks を参照してください。
REST スタイルの API を構築する
import { SquidService, webhook, WebhookRequest } from '@squidcloud/backend';
interface Task {
id: string;
title: string;
completed: boolean;
}
export class TaskApiService extends SquidService {
@webhook('tasks')
async tasks(request: WebhookRequest): Promise<any> {
switch (request.httpMethod) {
case 'get':
return this.listTasks();
case 'post':
return this.createTask(request.body);
default:
return this.createWebhookResponse({ error: 'Method not allowed' }, 405);
}
}
private async listTasks(): Promise<any> {
const tasks = await this.squid
.collection<Task>('tasks')
.query()
.dereference()
.snapshot();
return this.createWebhookResponse(tasks, 200);
}
private async createTask(body: any): Promise<any> {
if (!body?.title) {
return this.createWebhookResponse({ error: 'Missing title' }, 400);
}
const taskId = crypto.randomUUID();
const task: Task = { id: taskId, title: body.title, completed: false };
await this.squid.collection<Task>('tasks').doc(taskId).insert(task);
return this.createWebhookResponse(task, 201);
}
}
ファイルアップロードを受信して処理する
import { SquidService, webhook, WebhookRequest } from '@squidcloud/backend';
export class FileWebhookService extends SquidService {
@webhook('uploadDocument')
async uploadDocument(request: WebhookRequest): Promise<any> {
const files = request.files || [];
if (files.length === 0) {
return this.createWebhookResponse({ error: 'No files provided' }, 400);
}
const results: Array<{ name: string; docId?: string; size?: number; error?: string }> = [];
for (const file of files) {
// Validate file type
if (!file.mimetype.startsWith('text/') && !file.mimetype.includes('pdf')) {
results.push({ name: file.originalName, error: 'Unsupported file type' });
continue;
}
// Store metadata
const docId = crypto.randomUUID();
await this.squid.collection('documents').doc(docId).insert({
name: file.originalName,
mimeType: file.mimetype,
size: file.size,
uploadedAt: new Date().toISOString(),
});
results.push({ name: file.originalName, docId, size: file.size });
}
return this.createWebhookResponse({ uploaded: results }, 200);
}
}
関連項目(See Also)
- Executables - client からバックエンド関数を呼び出す
- Triggers - データベース変更に反応する
- Schedulers - スケジュールでコードを実行する
- Rate and quota limiting - バックエンド関数を保護する
- Stripe Webhooks Tutorial - エンドツーエンドの Stripe 連携