microCMSとStripe + Next.jsを利用したJamstackなHeadless ECサイトを作る方法（入門編）

この記事は、[ジャムジャム！！Jamstack_5](https://jamjamjamstack.connpass.com/event/236720/)イベントのフォローアップ記事です。

「microCMSとNext.jsを利用したJamstackなアプリケーションに、Stripeを組み込むことで、簡単に決済機能を実装できる」という内容のセッションで登壇しました。
この記事では、その日のスライドを作成するにあたってテストしたログを元に、実際に組み込む際の作業の進め方や、実案件に採用する際のヒントなどを紹介しています。

# イベント当日のスライド

https://speakerdeck.com/stripehideokamoto/jamjamjamstack-202202


# 商品データをmicroCMSで管理するメリット

Stripeだけでも商品の説明文や画像・メタ情報などを管理することは可能です。
ですが、以下のような制限があるため、実装したいサイト・アプリの要件には一致しないケースが存在します。

- 説明文には、HTMLが利用できない
- 画像は１枚しか登録できない

microCMSなどのCMSを利用することで、「請求に関わるデータはStripe、それ以外はCMS」のような役割分担が可能になります。
また、CMS側にはStripeのIDデータだけを持たせることで、価格改訂などの請求に関わる変更が発生した際のオペレーションを効率化することができます。

# 今回のケースだけでは「解決できない」問題

今回のサンプルだけでは実現が難しいケースを２つ紹介します。
他にも実案件で利用するには、追加の開発やサービスの利用が必要になる可能性がありますので、ご注意ください。

## 厳密な在庫管理が必要な商品の販売
本記事を作成した時点（2022/02）では、StripeにもmicroCMSにも在庫管理システムは存在しません。
そのため、大規模なセールなど、厳密な在庫管理が必要になるケースにおいては、別途在庫を管理するためのデータベースが必要です。

## メンバー管理を伴うサービス

有料メディアサイトやビジネスアプリケーションのような、「ユーザーがログインして操作をする必要のある要件」についても、別途Auth0やFirebase / AWS Amplifyなどが必要です。

# Step1: microCMSアカウントを作成しよう

アカウントを作成するには、メールアドレスとパスワードが必要です。

![スクリーンショット 0004-02-21 16.40.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/97305e48-64f8-840a-f944-7a5d4e88a26b.png)

ログインすると、APIのサブドメインやサービスの名称を入力する画面が出ます。
ここで設定したサービスIDがAPIのドメインになりますので、ご注意ください。

![スクリーンショット 0004-02-21 16.44.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/7c40bcd9-a35d-607e-d7ea-2c0495cff754.png)

サービスを作成すると、管理ページへのURLが表示されます。

![スクリーンショット 0004-02-21 16.45.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/b7c67d07-0178-8c9e-5b23-d8ad0c259669.png)

# Step2: microCMSで、商品一覧APIを作成しよう

![スクリーンショット 0004-02-21 16.53.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/275c636e-2dfc-ccb7-1496-df3c881c9e58.png)

今回はmicroCMSで商品情報を管理しますので、まずは以下のAPIを用意します。

- API名: Product API
- エンドポイント: /products

このAPIは商品の一覧データを返します。そのため、[APIの型を選択]では、[リスト形式]を選びましょう。

![スクリーンショット 0004-02-21 16.56.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/f8da0add-53b3-6a4e-9ef8-2ea8ed6b067e.png)

[APIスキーマ]を定義します。まずはシンプルに、以下の３項目を設定しましょう。

|フィールドID|表示名|種類|必須項目|
|:--|:--|:--|:--|
|name|商品名|テキストフィールド|必須|
|stripe_price_id|Stripe料金ID|テキストフィールド|必須|
|description|商品説明|リッチエディタ||

![スクリーンショット 0004-02-21 16.58.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/c4264ec2-5428-1f0b-9fe8-c2c3f64d8a67.png)

これでAPIの準備ができました。

# Step3: Stripeに商品データを登録する

続いてStripe側に商品データを登録します。

-> [作成ページに直接移動する](https://dashboard.stripe.com/test/products/create)

今回はシンプルにするため、Stripe側には商品名と料金データのみ登録しましょう。

## 商品情報

|名前|税コード(Optional)|
|:--|:--|
|ステッカー|一般 - 有形商品|

## 料金情報

|料金体系モデル|価格|価格に税金を含める|
|:--|:--|:--|
|標準の料金体系|　500　JPY [一括]|はい|

## 入力イメージ

![スクリーンショット 0004-02-21 17.07.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/94022078-16ba-cf20-09f8-70ed357a594e.png)

## 料金IDを取得しよう

[商品を保存]できれば、作成した商品ページに自動的に移動します。

![スクリーンショット 0004-02-21 17.12.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/b1c611f4-c7c6-df5b-06a8-c1091c948f96.png)

ここでは、作成した料金IDを取得しましょう。

# Step4: microCMSに商品データを登録する

登録する商品データが用意できましたので、microCMSに登録しましょう。

作成した[product api]の管理ページに移動し、[追加]ボタンをクリックします。

![スクリーンショット 0004-02-21 17.13.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/0b971740-f6c3-c584-67fd-6e17892c91db.png)

コンテンツ入力画面では、[商品名]に[ステッカー]そして[Stripe料金ID]に[Stripe Dashboardでコピーした料金ID(`price-xxx`)]を入力します。
[説明]部分には、リッチエディタを利用して段落や画像を挿入してみましょう。

![スクリーンショット 0004-02-21 17.16.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/65da0918-4a98-cfa7-96cd-4582684f16bd.png)

ページ上部にある[公開]ボタンをクリックして公開すれば完了です。

公開後、[公開]ボタンしたに表示される[APIプレビュー]をクリックすることで、APIレスポンスのプレビューができます。

![スクリーンショット 0004-02-21 17.18.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/5ddca580-0b10-bcb8-fdd4-41a23847c5e5.png)

# Step5: Next.jsでmicroCMS / Stripeのデータを表示する

ここまでで商品の登録が完了しました。
Next.jsを利用して、登録した商品データを実際に表示させましょう。

```bash
% npx create-next-app microcms-next-jamstack-blog
% cd microcms-next-jamstack-blog/

# microCMS SDKのインストール
% npm i microcms-js-sdk

#　　Stripe　SDKのインストール
% npm i stripe
```

（まとめてインストールしたい方用: `
npm i microcms-js-sdk stripe`）

## 環境変数を設定する

続いてmicroCMS / Stripeで利用するAPIキーを、環境変数に設定します。

```
% touch .env.local
```

**.env.local**

```
# Stripe:シークレットキー
STRIPE_SECRET_KEY=sk_test_xxxxxxx
# microCMS:APIキー
MICROCMS_API_KEY=xxxxx
```

StripeのAPIキーは、Dashboardの[[開発者>APIキー](https://dashboard.stripe.com/test/apikeys)]
から取得できます。

![スクリーンショット 0004-02-21 17.46.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/12a88dec-fa15-ea7d-b626-2ffa8db7a6e0.png)

microCMSは、管理画面の[権限管理>APIキー管理]から取得しましょう。

![スクリーンショット 0004-02-21 17.46.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/3998b634-4877-6cb2-b6b6-d9cc70831fe6.png)

## トップページに、microCMSのデータを表示する

まずは商品データをmicroCMSから取得しましょう。

`pages/index.js`を以下のように変更します。

```jsx
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { createClient } from 'microcms-js-sdk';

export const getStaticProps = async () => {
  /**
   * microCMS　SDKを利用して、商品データを取得する
   **/
  const client = createClient({
    serviceDomain: '作成したmicroCMS APIのドメイン名',
    apiKey: process.env.MICROCMS_API_KEY,
  });
  const { contents } = await client.get({ endpoint: 'products' });
  return {
    props: {
      products: contents,
    },
  };
};

export default function Home(props) {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main>
        <h1 className={styles.title}>
          Welcome to My Store
        </h1>
        <div>
          {props.products.map(product => {
            return (
              <section key={product.id}>
                <h2>{product.name}</h2>
                <div dangerouslySetInnerHTML={{
                  __html: product.description
                }} />
              </section>
            )
          })}
        </div>
      </main>
    </div>
  );
};
```

変更を保存し、`npm run dev`でNext.jsアプリを起動してみましょう。
以下のようにmicroCMSに登録したデータが表示されていればOKです。

![スクリーンショット 0004-02-21 17.59.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/4029d0c6-368a-e407-bf23-8b611c71a14f.png)

## microCMSのデータから、Stripeの料金データを取得する

商品データが取得できたので、続いて料金データを表示させましょう。

`pages/index.js`を以下のように変更します。


```diff_jsx
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { createClient } from 'microcms-js-sdk';
import Stripe from 'stripe';

export const getStaticProps = async () => {
  /**
   * microCMS　SDKを利用して、商品データを取得する
   **/
  const client = createClient({
    serviceDomain: 'demo-stripe-ec',
    apiKey: process.env.MICROCMS_API_KEY,
  });
  const { contents } = await client.get({ endpoint: 'products' });
+  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
+  /**
+   * Stripe　SDKを利用して、料金データを取得し、マージする
+   **/
+  const products = await Promise.all(contents.map(async (content) => {
+    try {
+      const price = await stripe.prices.retrieve(content.stripe_price_id);
+      return {
+        ...content,
+        price: {
+          unit_amount: price.unit_amount,
+          currency: price.currency,
+          id: price.id,
+        },
+      };
+    } catch (e) {
+      return content;
+    }
+  }));
  return {
    props: {
-      products: contents,
+      products,
    },
  };
};

..

        <div>
          {props.products.map(product => {
            return (
              <section key={product.id}>
                <h2>{product.name}</h2>
+                {
+                  product.price ? (
+                    <dl>
+                      <dt>価格</dt>
+                      <dd>{product.price.unit_amount.toLocaleString()} {product.price.currency.toLocaleUpperCase()}</dd>
+                    </dl>
+                  ): null
+                }
                <div dangerouslySetInnerHTML={{
                  __html: product.description
                }} />
              </section>
            )
          })}
        </div>
```

この変更により、Stripe側で管理している料金データもサイト上に表示できるようになりました。

![スクリーンショット 0004-02-21 18.10.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/2d102ed3-a3d6-351d-628a-4cafff7af5e4.png)


# Step6: Stripe Checkoutを利用して、表示している商品を注文できるようにする

最後に注文画面を作りましょう。今回はStripeが用意する決済画面にリンクさせる方法（Stripe Checkout）を利用します。

まずはCheckoutセッションを作成するためのAPIを用意します。

`pages/api/checkout_session.js`を作成し、以下のように実装しましょう。

```js
import Stripe from 'stripe';

export default async function handler(req, res) {
    const priceId = req.body.price_id
    if (!priceId) {
        return res.status(400).json({message: "商品が選択されていません"})
    }
    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
    const session = await stripe.checkout.sessions.create({
        customer_creation: 'if_required',
        line_items: [{
            price: priceId,
            quantity: 1,
        }],
        success_url: 'http://localhost:3000',
        cancel_url: 'http://localhost:3000',
        mode: 'payment',
    });
    res.status(200).json({ url: session.url })
  }
```

続いて、`pages/index.js`を以下のように変更します。

```diff_jsx
                {
                  product.price ? (
                    <dl>
                      <dt>価格</dt>
                      <dd>{product.price.unit_amount.toLocaleString()} {product.price.currency.toLocaleUpperCase()}</dd>
+                      <dt>注文</dt>
+                      <dd>
+                        <button onClick={async () => {
+                          const session = await fetch('/api/checkout_session', {
+                            method: "POST",
+                            headers: {
+                              'content-type': 'application/json',
+                            },
+                            body: JSON.stringify({
+                              price_id: product.price.id
+                            })
+                          }).then(data => data.json())
+                          .catch(e => {
+                            console.log(e)
+                            window.alert(e.message)
+                          })
+                          window.open(session.url, 'checkout-session')
+                        }}>注文する</button>
+                      </dd>
                    </dl>
                  ): null
                }

```

この実装により、「ボタンをクリックすると、API経由で注文のためのセッションが開始される」→「作成されたURLで、新しいウィンドをを開き、決済ページにアクセスできるようにする」フローが実装できます。

これでとてもシンプルな形ですが、microCMSのコンテンツ管理機能と、決済管理のためのStripeを組み合わせたアプリケーションが実装できました。

# [Appendix] より実践的なアーキテクチャにするためのポイント
実際の運用で利用するには、まだまだ考慮しないといけない部分が存在します。
チャレンジのアイディアとして、アイディアをいくつか紹介しますので、ぜひお試しください。

- Stripe Webhookを利用し、Stripeで登録した商品・画像をmicroCMSのデータに自動で追加する
- Stripe Webhookを利用し、Stripe側に登録できる説明文を「商品の簡易説明文」としてmicroCMSに登録する
- microCMSのWebhookを利用し、簡易説明文や商品名の変更をStripe側に反映させる  
**-> microCMSブログで紹介いただけました！: https://blog.microcms.io/sync-microcms-to-stripe/**
- microCMSの繰り返しフィールドを利用し、商品画像を複数登録する
- microCMSの繰り返しフィールドを利用し、商品バリエーションごとにStripeの料金IDや画像を登録できるようにする
- firebaseやAWS Amplifyを利用し、会員管理や在庫DBなどを実装する

# [Appendix] もっと簡単な方法

Checkoutのセッションを作成するのではなく、「microCMSのフィールドに、Stripeの支払いリンク（Payment Links）URLを登録するだけ」でも実装可能です。

https://stripe.com/jp/payments/payment-links

下の例では、microCMSのスキーマとして`payment_links`を追加し、そこに支払いリンクのURLを設定しています。

```diff_jsx

                      <dd>
                        <button onClick={async () => {
-                          const session = await fetch('/api/checkout_session', {
-                            method: "POST",
-                            headers: {
-                              'content-type': 'application/json',
-                            },
-                            body: JSON.stringify({
-                              price_id: product.price.id
-                            })
-                          }).then(data => data.json())
-                          .catch(e => {
-                            console.log(e)
-                            window.alert(e.message)
-                          })
+                          window.open(product.payment_links, 'checkout-session')
                        }}>注文する</button>
                      </dd>
```

この方法であれば、APIを用意する必要がなくなります。
そのため、NetlifyやAWS AmplifyでFunctionsを利用せずにサイトをデプロイできるようになります。

# おわりに

とてもシンプルな例のため、これだけでは２サービスを連携させるメリットは見えにくいかもしれません。
ですが、「コンテンツやデータを管理することに長けたサービス」をStripeと組み合わせることで、サイトの表現力を高めることができます。

また、microCMSでサイト・アプリケーションを開発されている方にとって、この記事がマネタイズのヒントになれば幸いです。

# 参考記事など

https://blog.microcms.io/microcms-next-jamstack-blog/

https://stripe.com/docs/payments/accept-a-payment