JAM Stack + Serverless Framework + Stripeで始めるServerless E-Commerce

この記事はStripe アドベントカレンダー22日目の記事です。 ServerlessやJAM Stackの登場で、スケーラブルなwebサイトやサービスの構築がより簡単になってきました。しかしそんな世の中でも定期的にHT […]

広告ここから
広告ここまで

目次

    この記事はStripe アドベントカレンダー22日目の記事です。

    ServerlessやJAM Stackの登場で、スケーラブルなwebサイトやサービスの構築がより簡単になってきました。しかしそんな世の中でも定期的にHTTP500系のエラーを吐きながらダウンしていく業種があります。

    そう、E-Commerceです。

    リプレイスが容易でないことや、セッション・トランザクションの問題などでなかなかスケーラブルに構築することが難しいというところもありますが、どうせならここもなんとかしたいところです。

    と、いうことでServerlessなバックエンドかつフロントエンドがJAM StackなE-Commerceについて考えてみます。

    構成

    ここからは「サーバーのことを意識する必要がない」ことを「Serverlessである」と考えて進めます。そのためEC-CUBEやWooCommerceなどのCMSについては一旦忘れましょう。

    商品を販売するためには以下のものが必要です。

    • 決済システム(カード情報の取り扱い・決済管理・返金など)
    • 商品管理(商品登録・在庫管理など)
    • 購入フォーム
    • 各種メールの送信
    • 顧客情報管理

    これらの機能を比較的簡単に用意でき、なおかつ「使った分だけ支払う」というスタイルが可能なFaaSとして、今回はStripeを利用します。

    Stripe側で通販の設定をする

    Stripeには商品管理機能がデフォルトで用意されています。

    商品名やカスタム属性、SKUの登録といった通販サイトに最低限必要な機能は用意されています。ただし商品画像の登録についてのみ、「外部サイトへアップロード後にURLを指定する」という形になることにご注意ください。AWSであればS3に画像をアップロードしてURLを指定すれば良いでしょう。CloudFrontを前段にセットするとなおよしです。

    登録した状態

    ちなみにStripeには2種類の「商品」があり、[注文 > 商品]からは買い切りの商品が登録できます。そして[Billing > 商品]からは定期購入商品の登録が可能です。

    Serverless FrameworkでAPIを構築する

    商品の登録が終われば、次はAPIを実装します。StripeのNode.js SDKはブラウザから直接実行することを想定していませんので、必ずAPIを挟みましょう。

    事前にStripeダッシュボードからAPIキーを2種類コピーしておきましょう。[開発者 > APIキー]から取得できます。この際XXX_test_XXXと書かれている方がテスト環境用(ダミーのカードが利用可能)ですので、こちらを使って開発します。

    APIについてはServerless Frameworkでサクッと作ります。

    プロジェクト作成

    Serverless FrameworkはnpmからCLIツールをDLします。globalに追加したくないという方は、npxコマンドからも実行できますので、slsnpx serverlessに適宜読み替えて実行してください。

    # 基本
    $ npm i -g serverless
    $ sls create -t aws-node-js -p stripe-ec-api
    
    # npm i -gしたくない人はこちら
    $ npx serverless create -t aws-nodejs -p stripe-ec-api
    
    # 移動
    $ cd stripe-ec-api

    YAMLでAPI定義

    sls createを実行するとAWS Lambdaをデプロイするための準備が出来上がっています。API Gatewayも必要となりますので、以下のようにserverless.ymlを書き換えておきましょう。

    service: stripe-ec-api 
    
    provider:
      name: aws
      runtime: nodejs8.10
      logRetentionInDays: 3
      environment:
        STRIPE_PROD_KEY: ${env:STRIPE_PROD_KEY}
        STRIPE_DEV_KEY: ${env:STRIPE_DEV_KEY}
    
    functions:
      # 購入のためのAPI
      buyItem:
        handler: handler.buy
        events:
          - http:
              path: products
              method: POST
              cors: true
      # 商品一覧の取得API
      productList:
        handler: handler.products
        events:
          - http:
              path: products
              method: get
              cors: true

    StripeのAPIシークレットキーが必要ですので、環境変数でSTRIPE_PROD_KEYSTRIPE_DEV_KEYをそれぞれ読み込ませています。個人でさっとやるなら${env~}に直接書き込んでも構いませんが、漏れると誰でもそのアカウントのStripeにアクセス・操作できるようになりますので本当に注意してください。

    Stripe SDKを追加

    続いて処理部分を組み込みます。StripeのAPIにはSDK経由でアクセスできますので、npmで追加しましょう。また同時にStripe SDKを呼び出す関数も追加しておきます。

    $ npm i -s stripe
    $ vim handler.js
    
    /* ここから */
    const getStripe = (stage = 'development') => {
      return require('stripe')(
        stage === 'production'
          ? process.env.STRIPE_PROD_KEY
          : process.env.STRIPE_DEV_KEY
      )
    }
    /* ここまでを追加 */

    商品一覧の取得APIを実装する

    まずは商品一覧を取得する実装を書きます。stripe.products.listのAPIの戻り値を返してやるだけでOKなのでシンプルに作れます。Access-Control-Allow-Originを入れ忘れるとCORSで詰むので要注意です。

    module.exports.products = async (event, context) => {
      const stripe = getStripe()
      const { data } = await stripe.products.list({
        type: 'good'
      })
      return {
        statusCode: 200,
        headers: {
          "Access-Control-Allow-Origin" : "*"
        },
        body: JSON.stringify(data),
      };
    };

    購入APIを実装する

    続いて購入のAPIです。stripe.order.createでオーダーを作成後、stripe.order.payで決済を走らせるという流れになります。

    なお、エラー処理や請求・送付先住所が別になった時の考慮が漏れていますので、そのままプロダクションに入れると事故ります。

    module.exports.buy = async (event, context) => {
      if (!event.body) {
        return {
          statusCode: 400,
          headers: {
            "Access-Control-Allow-Origin" : "*" // Required for CORS support to work
          },
          body: JSON.stringify({
            message: 'invalid request'
          }),
        };
      }
      const stripe = getStripe()
      const requestBody = JSON.parse(event.body); 
    
      const { skuId, token, currency } = requestBody
      const { email, id, card } = token
      const order = await stripe.orders.create({
        currency,
        items: [
          {
            type: 'sku',
            parent: skuId
          }
        ],
        shipping: {
          name: card.name,
          address: {
            line1: card.address_line1,
            city: card.address_city,
            state: card.address_state,
            country: card.country,
            postal_code: card.address_zip
          }
        },
        email
      })
      const result = await stripe.orders.pay(order.id, {     
        source: id 
      })
      return {
        statusCode: 201,
        headers: {
          "Access-Control-Allow-Origin" : "*"
        },
        body: JSON.stringify({
          order,
          result,
        }),
      };
    }

    Reactでフロントエンドを構築する

    別にReactである必要はないのですが、自分がよく使うのがReactなのでReactにしました

    $ npx create-react-app stripe-ec-react
    $ cd stripe-ec-react
    $ yarn add react-stripe-checkout axios

    商品一覧を表示する

    まずは商品一覧を取得します。先ほど作成したAPIをコールし、その結果をレンダリングしてやりましょう。

    import React, { Component } from 'react';
    import axios from 'axios';
    import StripeCheckout from 'react-stripe-checkout';
    import './App.css';
    
    class App extends Component {
      state = {
        items: [],
        result: ''
      }
      onToken = (token) => {
        console.log(token)
      }
      async componentDidMount() {
        const { data } = await axios.get('https://YOUR_STRIPE_SERVERLESS_API')
        this.setState({
          items: data || []
        })
      }
      render() {
        return (
          <div className="App">
            <header className="App-header">
              <p>
                Serverless EC
              </p>
            </header>
            <main>
              {this.state.result ? <h1>{this.state.result}</h1> : null}
              {this.state.items.map((item, key) => {
                return (
                  <article key={key} className="list">
                    <img src={item.images[0]} alt="product"/>
                    <h1>{item.name}</h1>
                    <p>{item.description}</p>
                    <section>
                      <h2>サイズ</h2>
                      <dl>
                        <dd>Height</dd>
                        <dt>{item.package_dimensions.height} cm</dt>
                        <dd>Width</dd>
                        <dt>{item.package_dimensions.width} cm</dt>
                        <dd>Length</dd>
                        <dt>{item.package_dimensions.length} cm</dt>
                        <dd>Weight</dd>
                        <dt>{item.package_dimensions.weight} cm</dt>
                      </dl>
                      <h2>Items</h2>
                      <ul>
                        {item.skus.data.map((sku, k) => (
                          <li key={k}>
                            <dl>
                              <dd>Color</dd>
                              <dt>{sku.attributes.color}</dt>
                              <dd>Size</dd>
                              <dt>{sku.attributes.size}</dt>
                            </dl>
                            <dl>
                              <dd>Price</dd>
                              <dt>{sku.price} / {sku.currency.toUpperCase()}</dt>
                              <dd>Stock</dd>
                              <dt>{sku.inventory.type === 'infinite' ? '∞' : sku.inventory.quantity}</dt>
                            </dl>
                            <StripeCheckout
                              token={this.onToken}
                              stripeKey="pk_test_stripekey"
                            />
                          </li>
                        ))}
                      </ul>
                    </section>
                  </article>
                )
              })}
            </main>
          </div>
        );
      }
    }
    
    export default App;
    

    サンプルとしての見通し優先で1ファイルにしていますが、実際にはファイルを分割することをオススメします。

    これで商品一覧ページが作成できました。詳細ページを作りたい場合は、指定した商品のみを取得するAPIを用意し、それを呼び出すページを別途追加してやればOKです。

    決済を実装する

    最後に決済情報を実装します。先ほどのコードからStripeCheckoutコンポーネントの引数を変更します。

    <StripeCheckout
      token={(token) => this.onToken(token, sku.id, sku.currency)}
      name={item.name}
      billingAddress={true}
      shippingAddress={true}
      description={`Color: ${sku.attributes.color} / Size: ${sku.attributes.size}`}
      amount={sku.price}
      currency={sku.currency.toUpperCase()}
      stripeKey="pk_test_stripekey"
    />

     これでお届け先住所や購入商品情報を表示するようになりました。

    続いて購入APIをコールする処理を追加します。onTokenを以下のように書き換えましょう。

      onToken = async (token, skuId, currency) => {
        try {
          await axios.post('https://YOUR_STRIPE_SERVERLESS_API', {
            skuId,
            token,
            currency
          })
          this.setState({
            result: '注文を受け付けました'
          })
        } catch (e) {
          this.setState({
            result: '注文の処理に失敗しました'
          })
        }
      }

    成功していれば、以下のように購入時にメッセージが表示されます。

    また、Stripeのダッシュボードにも注文が表示されるようになります。

    あとはこのReactアプリケーションをNetlifyやAWS Amplifyなどでホストすれば、スケーラブルなServerless ECサイトのデモが完成です。

    最後に

    ここまでServerlessにECサイトを作るデモのステップを紹介しました。

    実際に導入するにはURLの設定やルーティング、画像の管理を含めた管理画面の追加など考慮すべき点は少なくありません。

    ですが、個人的に何かしらのDLコンテンツを提供したい場合などの選択肢として、Serverless & JAM Stackな構成というのはありかなと思います。

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark