Webhooks
外部サービスが呼び出せる HTTP エンドポイントを公開します。
Webhooks を使う理由
アプリケーションが外部サービスから HTTP リクエストを受け取る必要があるケースがあります。たとえば、決済プロバイダーが請求の完了を通知する、ソースコード管理プラットフォームが新しいコミットを報告する、監視ツールがアラートを送信する、といった場合です。
webhooks では、関数にデコレーターを付けてデプロイするだけです。
// 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 の背後にある関数だけで完結します。
概要
Webhooks は、HTTP エンドポイントとして公開されるバックエンド関数です。外部サービスはこれらのエンドポイントへリクエストを送信し、あなたの関数が受信データを処理します。Squid が URL ルーティング、リクエストのパース、レスポンスのシリアライズを担当します。
Webhooks を使うべきとき
| ユースケース | 推奨 |
|---|---|
| 外部サービスから HTTP リクエストを受け取る | ✅ Webhook |
| Squid client から関数を呼び出す | Executables を使用 |
| データベースの変更に反応する | Triggers を使用 |
| スケジュールでコードを実行する | Schedulers を使用 |
仕組み
SquidServiceを拡張したクラス内で、メソッドに@webhook('webhookId')を付ける- Squid がデプロイ時に webhook を検出して登録する
- Squid が webhook を HTTP エンドポイントとして公開する
- 外部サービスがエンドポイント URL にリクエストを送信する
- 関数がリクエストを受け取り、レスポンスを返す
クイックスタート
前提条件
squid initで初期化された Squid backend プロジェクト@squidcloud/backendパッケージがNPMからインストール済み
ステップ 1: webhook を作成する
SquidService を拡張する service クラスを作成し、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);
}
}
ステップ 2: service をエクスポートする
service index ファイルから service がエクスポートされていることを確認します。
export * from './example-service';
ステップ 3: backend を起動またはデプロイする
ローカル開発では、Squid CLI を使って backend をローカルで起動します。
squid start
クラウドにデプロイする場合は、deploying your backend を参照してください。
ステップ 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 シリアライズ可能な戻り値は、ステータスコード 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 メソッド
Webhooks は 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);
}
}
ファイルアップロード
Webhooks はファイルアップロードを受け取れます。ファイルは 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 から webhooks を呼び出す
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' },
});
エラーハンドリング
エラーを投げる
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 とも一致しない | スペルを確認し、service がエクスポートされていることを確認する |
| 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 デコレーターを使って、webhooks を不正利用から保護します。レート制限とクォータ制限の詳細は、Rate and quota limiting を参照してください。
ベストプラクティス
-
適切なステータスコードを返す。
createWebhookResponseを使い、意味のある HTTP ステータスコード(成功は 200、不正な入力は 400、未認証は 401、サーバーエラーは 500)を返します。 -
webhook 署名を検証する。 外部サービスから webhooks を受信する際は、
request.rawBodyとサービスの signing secret を使って必ずリクエスト署名を検証してください。 -
素早く応答する。 外部サービスにはタイムアウト制限があることが多いです。処理に時間がかかる場合は、webhook をすぐに acknowledge し、データ処理は非同期に行ってください。
-
冪等性 (idempotency) を前提に設計する。 外部サービスは webhook 配信をリトライすることがあります。リクエストボディ内の一意な識別子を使って重複配信を検出し、スキップしてください。
-
入力を早期にバリデーションする。 webhook 関数の冒頭で必須フィールドをチェックします。
throwWebhookResponseを使って処理前にエラーを返してください。 -
受信リクエストをログに残す。 デバッグのために webhook の event type と主要な識別子をログに残します。本番環境ではリクエストボディ全体のような機微なデータをログに出さないでください。
コード例
決済イベントを処理する
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 連携