Expressで、Stripe Appsと連携するAPIサーバーを用意する方法



この記事は、[Stripe Apps を25日間紹介し続ける Advent Calendar 2022](https://qiita.com/advent-calendar/2022/stripe-apps) 20日目の記事です。

![スクリーンショット 2022-11-24 17.56.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/e0261865-0418-daa2-423a-5f4d70558b1b.png)

さまざまなサービスとStripe Appsアプリを連携させる場合、「すでにあるAPIサーバーやシステムと接続させる」必要が発生します。

今回はExpressを例に、安全にStripe Appsアプリと外部のAPIサーバーを連携させる方法を紹介します。

## ExpressでStripe Appsと連携するAPIを作成する

### Step0: プロジェクトのセットアップ

まずは簡単なExpressアプリを用意しましょう。

ライブリロードなどの機能を手軽に手に入れるため、今回はNxで作成します。

便宜上、今回はプロジェクト名を`express-stripe`で統一して進めます。

```bash
$ npx create-nx-workspace --preset=express
Need to install the following packages:
  create-nx-workspace@15.3.3
Ok to proceed? (y) y

 >  NX   Let's create a new workspace [https://nx.dev/getting-started/intro]

✔ Repository name                       · express-stripe
✔ Application name                      · express-stripe
✔ Enable distributed caching to make your CI faster · No
```

セットアップ後、プロジェクトのディレクトリに移動してアプリを起動しましょう。

```bash
$ cd express-stripe
$ npm run start

chunk (runtime: main) main.js (main) 689 bytes [entry] [rendered]
webpack compiled successfully (24f80a260b1d420d)
Debugger listening on ws://localhost:9229/8faba312-c87d-47dc-b5e7-ef9d2d27b319
Debugger listening on ws://localhost:9229/8faba312-c87d-47dc-b5e7-ef9d2d27b319
For help, see: https://nodejs.org/en/docs/inspector
Type-checking in progress...
Listening at http://localhost:3333/api
No errors found.
```

APIの準備はできました。
ただし、CORSが未設定のため、ブラウザから呼び出すとエラーが発生します。

デモ用のAPIとして、`apps/express-stripe/src/main.ts`を次のように変更しましょう。

```diff_typescript
const app = express();
+app.use((request, response, next) => {
+  response.header('Access-Control-Allow-Origin', '*');
+  response.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
+  response.header(
+    'Access-Control-Allow-Headers',
+      'Content-Type, Stripe-Signature'
+  );
+  if (request.method.toLocaleUpperCase() === 'OPTIONS') {
+    return response.send(200);
+  }
+  next();
+});
app.use('/assets', express.static(path.join(__dirname, 'assets')));
```

これでAPIが用意できました。

### Step1: シンプルなAPIを連携させてみよう

さっそくStripe AppsのアプリとAPIを連携させてみましょう。

Stripe Appsアプリの、`src/views/App.tsx`を次のように実装します。

```tsx
import { ContextView, Box } from "@stripe/ui-extension-sdk/ui";
import { useEffect, useState } from 'react';
import { ExtensionContextValue } from "@stripe/ui-extension-sdk/context";


const App = (props: ExtensionContextValue) => {
  const [message, setMessage] = useState("");
  useEffect(() => {
    fetch(new URL("http://localhost:3333/api"))
      .then(response => response.json())
      .then(data => {
        setMessage(data.message);
      })
      .catch(console.log);
  }, []);
  return (
    <ContextView
      title="API呼び出しサンプル"
    >
      <Box>{message}</Box>
    </ContextView>
  );
};

export default App;

```

アプリを更新すると、Express APIからのレスポンスが画面に表示されます。

![スクリーンショット 2022-12-19 16.37.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/8587ea88-8789-bb4c-2836-bc5537f8daf5.png)

### Step2: 署名検証を追加して、より安全に連携する

より安全にAPIサーバーを運用するために、Stripeが用意する署名検証の仕組みが利用できます。

#### 開発中のアプリを、Stripeにアップロードする

署名シークレットを取得するには、Stripeアカウントにアプリを登録する必要があります。

そのため、Stripe CLIで、開発中のアプリを一度アップロードしましょう。

```bash
$ stripe apps upload
⬆ You are about to upload your Stripe App First Stripe App version "0.0.1" to テストアカウント(日本).
? Would you like to proceed? [Y/n] █
```

[y]を入力すると、ビルドとアップロードが始まります。

```bash
✔ Built files for production
✔ Packaged files for upload
✔ Uploaded First Stripe App

🌐 Press Enter to continue on the Stripe dashboard
```
と表示されますので、[Enterキー]を押してStripeダッシュボードに移動しましょう。

![スクリーンショット 2022-12-19 16.48.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/add5a791-cf2f-60a4-1112-604ff1a867cb.png)

アプリの画面に移動できれば、アップロード完了です。

:::note info
アップロードしたアプリは、公開操作を行うまでは利用できません。
「必要な情報を取得するために、アプリを登録する」くらいの気軽さでアップロードしてみてください。
:::

:::note warn
**ビルドエラーにご注意**
アップロード前に、ビルドが行われます。
このビルド時にエラーが発生すると、アップロード処理が中断されます。

```bash
Would you like to proceed: y
×  Failed to build files
✘ [ERROR] Expected identifier but found "{"

    .build/manifest.js:23:2:
      23 │   {
         ╵   ^

```

`.build/`のコードエラーが出た場合は、`rm -rf ./.build/`で中身を一度削除してみましょう。
:::

#### アプリの署名シークレットを取得する

アプリを登録できたので、署名シークレットを取得しましょう。

アプリ詳細画面の[...]をクリックすると、[署名シークレット]が表示されます。

![スクリーンショット 2022-12-19 16.52.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/64dcd8fd-655b-4695-2466-cd3584ef1408.png)

[署名シークレット]をクリックすると、アプリ用の署名シークレットが発行されます。

![スクリーンショット 2022-12-19 16.53.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/2de17faa-e993-e61b-0e01-97f5110a1994.png)

#### サーバー側で署名検証処理を実装する

署名シークレットを取得できたので、サーバー側で検証処理を組み込みましょう。

まずはStripe SDKをインストールします。

```bash
$ npm i stripe
```

続いて`.env`を作成して、StripeのAPIキーと署名シークレットを環境変数に登録します。

```
STRIPE_SECRET_API_KEY=sk_test_xxx
STRIPE_APP_SECRET=absec_xxxx
```

`apps/express-stripe/src/main.ts`を次のように変更しましょう。

```diff_typescript
import * as express from 'express';
import * as path from 'path';
+import Stripe from 'stripe';
+const stripe = new Stripe(process.env.STRIPE_SECRET_API_KEY as string, {
+  apiVersion: "2022-11-15"
+});

const app = express();
+app.use(express.json());

...
-app.get('/api', (request, response) => {
+app.post('/api', (request, response) => {
+  const sig = request.headers['stripe-signature'];
+  const payload = JSON.stringify(request.body);
+  try {
+    stripe.webhooks.signature.verifyHeader(payload, sig as string, process.env.STRIPE_APP_SECRET as string);
+  } catch (error) {
+    response.status(400).send(error.message);
+    return;
+  }
  response.send({ message: 'Welcome to express-stripe!' });
});

```

アプリを再読み込みすると、APIがエラーを返すようになります。

![スクリーンショット 2022-12-19 17.06.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/cd92c1c7-a714-3fed-eb6a-d4f0461b0cfe.png)


#### アプリから、署名検証に対応したリクエストを送信する

サーバー側の準備ができましたので、アプリ側も署名検証に対応しましょう。

Stripe Appsアプリの、`src/views/App.tsx`を次のように変更します。

```diff_typescript
import { ExtensionContextValue } from "@stripe/ui-extension-sdk/context";
+import fetchStripeSignature from '@stripe/ui-extension-sdk/signature';

const App = (props: ExtensionContextValue) => {
  const [message, setMessage] = useState("");
+  const {
+    id: userId,
+    account: {
+      id: accountId,
+    }
+  } = props.userContext;
  useEffect(() => {
-    fetch(new URL("http://localhost:3333/api"))
+    if (!userId || !accountId) return;
+    fetchStripeSignature()
+    .then(signature => {
+      const signagurePayload = {
+        user_id: userId,
+        account_id: accountId,
+      }
+      return fetch(new URL("http://localhost:3333/api"), {
+        method: "POST",
+        headers: {
+          "Stripe-Signature": signature,
+          "Content-Type": "application/json"
+        },
+        body: JSON.stringify(signagurePayload)
+      })
+    })
      .then(response => response.json())
      .then(data => {
        setMessage(data.message);
      })
      .catch(console.log);
  }, [userId, accountId]);
  return (
    <ContextView
      title="API呼び出しサンプル"
    >
      <Box>{message}</Box>
    </ContextView>
  );
};
```

変更を保存すると、API呼び出しが再度成功します。

![スクリーンショット 2022-12-19 17.30.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/1da4d717-1160-fbc5-fd33-9881f7e6ab39.png)

## 終わりに

既存のAPIをStripe Appsから呼び出す場合にも、署名検証をサポートしたエンドポイントを作成することをお勧めします。

検証に利用するデータを追加することもできますので、ぜひ下記のドキュメントを参考にお試しください。

https://stripe.com/docs/stripe-apps/build-backend


## Stripe Appsひとりアドベントカレンダー 2022

今年ベータリリースされたばかりのStripe Appsは、まだ日本語の情報が多くありません。

そこでQiita Advent Calendar 2022にて、毎日Stripe Appsについての情報を投稿します。

https://qiita.com/advent-calendar/2022/stripe-apps

ノーコードで利用する方法や、開発するためのTipsなども紹介予定ですので、ぜひ購読をお願いします。