HonoでStripeのWebhookを受け取る場合、実装自体はそれほど難しくありません。
ただ、実際に書いてみると、
- Webhookの署名を検証する
- 生のRequest Bodyを扱う
-
event.typeでイベントを分岐する - イベントごとの型を扱う
- テスト用のWebhook Requestを作る
といった処理が必要になります。
今回は、TypeScript向けWebhookフレームワーク Hibiki を使って、HonoでStripe Webhookを型安全に処理してみます。
ちなみにHibikiは僕が開発しているフレームワークのためステマです。
だけど実際使いやすいフレームワークだとは思うのでお付き合いください。
完成形
先に完成形を見ると、次のようになります。
import { Hono } from "hono"
import { Hibiki } from "@hibiki-js/core"
import { stripe } from "@hibiki-js/stripe"
import { hibiki } from "@hibiki-js/hono"
const webhooks = new Hibiki().use(
stripe({
secret: process.env.STRIPE_WEBHOOK_SECRET!,
}),
)
webhooks.on("stripe.checkout.session.completed", async ({ event }) => {
console.log(event.data.object.id)
})
const app = new Hono()
app.post(
"/webhooks/stripe",
hibiki(webhooks, "stripe"),
)
export default app
Webhookの署名検証やイベントのパースはHibiki側で行われます。
stripe.checkout.session.completedを指定すると、そのイベントに対応した型としてeventを扱えます。
インストール
今回は以下の3つを使います。
npm install @hibiki-js/core @hibiki-js/stripe @hibiki-js/hono
Hono本体は@hibiki-js/honoのpeer dependencyなので、まだ入っていない場合は別途インストールします。
npm install hono
Hibikiを作成する
まず、HibikiにStripe Providerを登録します。
import { Hibiki } from "@hibiki-js/core"
import { stripe } from "@hibiki-js/stripe"
const webhooks = new Hibiki().use(
stripe({
secret: process.env.STRIPE_WEBHOOK_SECRET!,
}),
)
secretにはStripe Dashboardなどで取得したWebhook Signing Secretを指定します。
whsec_...
みたいな形のやつです。
イベントハンドラを書く
次に、受け取りたいStripeイベントを登録します。
webhooks.on("stripe.checkout.session.completed", async ({ event }) => {
console.log(event.data.object.id)
})
Hibikiでは、Provider名.イベント名でイベントを指定します。
stripe.checkout.session.completed
ここで重要なのが型推論です。
stripe.checkout.session.completedを指定すると、ハンドラ内のeventもそのイベントに対応した型として扱われます。
例えば、こんな感じにかけます。
webhooks.on("stripe.checkout.session.completed", async ({ event }) => {
const session = event.data.object
console.log(session.id)
console.log(session.customer)
})
イベントごとに自分で型をキャストする必要はありません。
Honoに組み込む
Honoとの接続には@hibiki-js/honoを使います。
import { Hono } from "hono"
import { hibiki } from "@hibiki-js/hono"
const app = new Hono()
app.post(
"/webhooks/stripe",
hibiki(webhooks, "stripe"),
)
これで、POST /webhooks/stripeに届いたRequestがHibikiへ渡されます。
全体では次のようになります。
import { Hono } from "hono"
import { Hibiki } from "@hibiki-js/core"
import { stripe } from "@hibiki-js/stripe"
import { hibiki } from "@hibiki-js/hono"
const webhooks = new Hibiki().use(
stripe({
secret: process.env.STRIPE_WEBHOOK_SECRET!,
}),
)
webhooks.on("stripe.checkout.session.completed", async ({ event }) => {
console.log(event.data.object.id)
})
const app = new Hono()
app.post(
"/webhooks/stripe",
hibiki(webhooks, "stripe"),
)
export default app
複数イベントを処理する
複数のStripeイベントを処理したい場合は、on()を追加するだけです。
webhooks.on("stripe.checkout.session.completed", async ({ event }) => {
console.log("Checkout completed:", event.data.object.id)
})
webhooks.on("stripe.invoice.paid", async ({ event }) => {
console.log("Invoice paid:", event.data.object.id)
})
webhooks.on("stripe.customer.subscription.deleted", async ({ event }) => {
console.log("Subscription deleted:", event.data.object.id)
})
イベントごとにそれぞれ対応する型が推論されます。
そのため、大きなswitch文にすべてのイベント処理をまとめる必要がありません。
Stripe SDKは不要
HibikiのStripe Providerでは、Webhook署名検証のためにStripe SDKを利用していません。
署名検証にはWeb Crypto APIのHMAC-SHA256を利用しています。
そのため、Webhookを受信するだけであれば、stripeパッケージをruntime dependencyとして追加する必要はありません。
Hibikiの各パッケージもruntime dependency 0で作られています。
もちろん、Webhook処理の中でStripe APIを呼び出したい場合にはStripe SDKを併用できます。
例えば、こんな感じの使い方も可能です。
webhooks.on("stripe.checkout.session.completed", async ({ event }) => {
const session = event.data.object
// ここからStripe SDKを使って別のAPIを呼ぶ
})
署名検証は自動で行われる
Webhookでは、受け取ったJSONをそのまま信用してはいけません。
Stripeから送られてきたRequestであることを確認するために、Webhook Signatureを検証する必要があります。
Hibikiでは、Webhook Secretを設定しておけば、イベントをパースする前に署名検証が行われます。
stripe({
secret: process.env.STRIPE_WEBHOOK_SECRET!,
})
アプリケーション側では、以下のような形で処理できます。
webhooks.on("stripe.checkout.session.completed", async ({ event }) => {
// 署名検証後のイベント
})
また、署名検証で必要になる元のRequest BodyもHibiki側で扱います。
Timestamp tolerance
Stripe Providerでは署名のTimestamp toleranceも指定できます。
例えば300秒にする場合は、以下のような形にします。
const webhooks = new Hibiki().use(
stripe({
secret: process.env.STRIPE_WEBHOOK_SECRET!,
tolerance: 300,
}),
)
未対応イベント
Webhook Endpointには、アプリケーションで使用していないイベントが送られてくることもあります。
Hibikiでは、Hibiki側で未対応のProvider Eventを受信した場合、デフォルトでは200を返します。
これによって、不要なWebhook再送が発生しにくくなっています。
未対応イベントをエラーとして扱いたい場合は、strictEvents: true を利用できます。
この場合は400が返されます。
また、Hibikiが対応しているイベントでもハンドラが登録されていなければ204を返します。
テストする
Webhookは実装だけでなく、テスト用の署名付きRequestを作るのも少し面倒です。
Hibikiには@hibiki-js/testingがあります。
npm install -D @hibiki-js/testing
例えばStripeイベントを発生させるテストは次のように書けます。
import { Hibiki } from "@hibiki-js/core"
import { stripe } from "@hibiki-js/stripe"
import { createWebhookTest } from "@hibiki-js/testing"
const app = new Hibiki().use(
stripe({
secret: "whsec_test",
}),
)
app.on("stripe.checkout.session.completed", ({ event }) => {
console.log(event.data.object.id)
})
const webhook = createWebhookTest(app, {
secrets: {
stripe: "whsec_test",
},
})
await webhook.emitEvent("stripe.checkout.session.completed", {
id: "cs_test",
object: "checkout.session",
})
より細かくRequest HeaderやTimestampを制御したい場合は、低レベルAPIのstripeRequestも利用できます。
Cloudflare Workersとも相性が良い
Hibiki CoreはWeb StandardsのRequest / Responseをベースにしています。
Honoも複数Runtimeで動作するため、Hono + Hibikiという組み合わせをCloudflare Workersなどで利用できます。
Hibiki側がNode.js固有のRequestオブジェクトへ依存していないため、
Hono + Hibiki + Cloudflare Workers
のような構成でも同じAPIを利用できます。
Hibikiのリポジトリには、Hono + StripeのExampleも置いています。
Stripe以外のWebhookも同じAPIで扱える
Hibikiでは現在、StripeのほかにGitHub Providerも提供しています。
そのうちSlackとかDiscordにも対応しよっかなって思ってます。
例えば、以下のようにすると同じHibikiインスタンスに複数Providerを登録できます。
import { github } from "@hibiki-js/github"
const webhooks = new Hibiki()
.use(
stripe({
secret: process.env.STRIPE_WEBHOOK_SECRET!,
}),
)
.use(
github({
secret: process.env.GITHUB_WEBHOOK_SECRET!,
}),
)
イベント処理も同じ形になります。
webhooks.on("stripe.checkout.session.completed", async ({ event }) => {
// Stripe
})
webhooks.on("github.issues.opened", async ({ event }) => {
// GitHub
})
Providerごとに署名方法やPayload形式は異なりますが、その違いをProvider Package側へ閉じ込めるのがHibikiの設計です。
まとめ
HonoでStripe Webhookを扱う場合、Hibikiを使うと、
- Stripe Webhookの署名検証
- Event Payloadのパース
- イベントの振り分け
- イベントごとの型推論
- Honoとの接続
- Webhookのテスト
をまとめて扱えます。
最小構成なら、
const webhooks = new Hibiki().use(
stripe({
secret: process.env.STRIPE_WEBHOOK_SECRET!,
}),
)
webhooks.on("stripe.checkout.session.completed", ({ event }) => {
console.log(event.data.object.id)
})
app.post(
"/webhooks/stripe",
hibiki(webhooks, "stripe"),
)
まで短くできます。
Hibiki自体はまだ公開したばかりなので、Hono + Stripeで実際に使った際のフィードバックがあればIssueなどで教えていただけると助かります。
Getting Started:
Hono:
Stripe: