AstroでStripeのWebhookイベントを処理するREST APIを作成する方法

Astroでは、静的なページだけでなくREST APIを作ることもできます。

APIを作成する場合、`src/pages`ディレクトリ内に、`[API Path name].js`(TypeScriptでは`[API Path name].ts`)を作成し、次のようなコードを追加します。

```typescript
import { APIRoute } from "astro";

export const get: APIRoute = async () => {
    return {
        body: JSON.stringify({
            message: "Hello World"
        })
    }
}
```

## Stripe Webhookの「署名チェック」を行う方法

AstroやNext.jsなどのウェブフレームワークでは、リクエスト内容を簡単に取得できるようなパーサー処理が内蔵されています。

しかしStripeなどの一部サービスでは、Webhookの署名チェック時に「パース前のリクエストBody」が必要になることがあります。

Astroでは2023/04時点でBodyのパーサーをオフにする方法が見つかりませんでしたので、次のような実装で対応します。

```typescript
import { APIRoute } from "astro";
import Stripe from "stripe";

const STRIPE_SECRET_API_KEY = import.meta.env.STRIPE_SECRET_API_KEY;
const STRIPE_WEBHOOK_SECRET = import.meta.env.STRIPE_WEBHOOK_SECRET;

const stripe = new Stripe(STRIPE_SECRET_API_KEY, {
	apiVersion: "2022-11-15"
});

export const post: APIRoute = async (context) => {
    const { request } = context;
    const signature = request.headers.get("stripe-signature");
    try {
        const body = await request.arrayBuffer();
        const event = stripe.webhooks.constructEvent(
            Buffer.from(body),
          signature,
          STRIPE_WEBHOOK_SECRET
        );
        return {
            body: JSON.stringify({
                message: "ok",
            })
        };
      } catch (err) {
        const errorMessage = `⚠️  Webhook signature verification failed. ${err.message}`
        console.log(errorMessage);
        return new Response(errorMessage, {
            status: 400
        })
      }
}
```

リクエストBodyを`request.json()`や`request.text()`ではなく、`request.arrayBuffer()`で取得します。

その後、`Buffer.from()`を通すことでパース前と同等のデータを`constructEvent`へ渡すことができます。

## [Appendix] 汎用的なハンドラーにする方法

TypeScriptの型をより活用したい場合には、次のような関数を作ることもできます。

```typescript
export const post: APIRoute = async (context) => {
    return withStripeWebhookVerification(context, async (event) => {
        return {
            body: JSON.stringify(event)
        }

    })
}
```


まず、Stripe Webhookの型定義をまとめたライブラリをインストールしましょう。

```bash
% npm install -D stripe-event-types
```

https://github.com/kgajera/stripe-event-types

続いてWebhookの署名チェックや型定義のカスタマイズを行う`withStripeWebhookVerification`関数を作ります。

```typescript
/// <reference types="stripe-event-types" />
import { EndpointOutput } from "astro";
import { APIContext, APIRoute } from "astro";
import Stripe from 'stripe';
const STRIPE_SECRET_API_KEY = import.meta.env.STRIPE_SECRET_API_KEY;
const STRIPE_WEBHOOK_SECRET = import.meta.env.STRIPE_WEBHOOK_SECRET;

const stripe = new Stripe(STRIPE_SECRET_API_KEY, {
	apiVersion: '2022-11-15'
});


export type StripeWebhookAPIRoute = (event: Stripe.DiscriminatedEvent, context: APIContext) => EndpointOutput | Response | Promise<EndpointOutput | Response>

export const withStripeWebhookVerification = async (context: APIContext, callback: StripeWebhookAPIRoute) => {
    const { request } = context;
    const signature = request.headers.get('stripe-signature');
    try {
        const body = await request.arrayBuffer();
        const event = stripe.webhooks.constructEvent(
            Buffer.from(body),
          signature,
          STRIPE_WEBHOOK_SECRET
        );
        return callback(event as Stripe.DiscriminatedEvent, context);
    } catch (err) {
        const errorMessage = `⚠️  Webhook signature verification failed. ${err.message}`
        console.log(errorMessage);
        return new Response(errorMessage, {
            status: 400
        });
    };
};
```

あとはWebhook APIにしたいコードで、`withStripeWebhookVerification`を呼び出すだけです。


```typescript
export const post: APIRoute = async (context) => {
    return withStripeWebhookVerification(context, async (event) => {
        switch (event.type) {
            case 'customer.created': {
                console.log(event.data.object.email)
                break
            }
            case 'customer.subscription.created': {
                console.log(event.data.object.billing_cycle_anchor)
                break
            }
            default:
                break
        }
        return {
            body: JSON.stringify(event)
        }

    })
}
```

## 参考資料など

https://stripe.com/docs/webhooks/quickstart

https://docs.astro.build/en/core-concepts/endpoints/#server-endpoints-api-routes

https://github.com/kgajera/stripe-event-types