Go + frourio + gRPC + PAYJP

2021-01-01

Go + frourio + gRPC + PAYJP で決済ができるサンプルアプリを作りました。

MicroServices とセットで登場する gRPC を触ってみたかったのと、Go で何か作ってみたかった時に見つけた記事と、同時期に読んだフロントエンドの記事で知った frourio を組み合わせて全部同時にやろうというモチベーションのやつです。

成果物は GitHub にあります。

ここからは領域ごとにざっくりやることと、ハマったポイントなどをメモしていきます。

決済まわり

gRPCのサーバサイドはGoで実装しますが、ほとんど参考にした記事と同じなので、適宜進めます。 差分があるとすればオーソリ、キャプチャを一応分けてみたところくらいでしょうか。

func (s *server) Charge(ctx context.Context, req *gpay.ChargeRequest) (*gpay.ChargeResponse, error) {
    pay := payjp.New(os.Getenv("PAYJP_SECRET_KEY"), nil)

    charge, err := pay.Charge.Create(int(req.Amount), payjp.Charge{
        Currency:    "jpy",
        CardToken:   req.Token,
        Capture:     false,
        Description: req.Name + " " + req.Desc,
    })
    if err != nil {
        return nil, err
    }

    res := &gpay.ChargeResponse{
        Id:       charge.ID,
        Paid:     charge.Paid,
        Refunded: charge.Refunded,
        Captured: charge.Captured,
        Amount:   int64(charge.Amount),
    }

    return res, nil
}

func (s *server) Capture(ctx context.Context, req *gpay.ChargeRequest) (*gpay.ChargeResponse, error) {
    pay := payjp.New(os.Getenv("PAYJP_SECRET_KEY"), nil)

    charge, err := pay.Charge.Capture(req.ChargeId)
    if err != nil {
        return nil, err
    }

    res := &gpay.ChargeResponse{
        Id:       charge.ID,
        Paid:     charge.Paid,
        Refunded: charge.Refunded,
        Captured: charge.Captured,
        Amount:   int64(charge.Amount),
    }

    return res, nil
}

protobuf からクライアント側のコードを生成

必要なパッケージをインストールします。

% yarn add -D grpc-tools grpc_tools_node_protoc_ts

インストールしたプラグインを指定してコンパイルします。長いので適当なスクリプトや Makefile にまとめるやり方が多いみたいです(最後に参考にした記事貼っておきます)。

PLUGIN_TS=../frontend/node_modules/.bin/protoc-gen-ts
PLUGIN_GRPC=../frontend/node_modules/.bin/grpc_tools_node_protoc_plugin
DIST_DIR=./client

protoc \
--js_out=import_style=commonjs,binary:"${DIST_DIR}"/ \
--ts_out=import_style=commonjs,binary:"${DIST_DIR}"/ \
--grpc_out="${DIST_DIR}"/ \
--plugin=protoc-gen-grpc="${PLUGIN_GRPC}" \
--plugin=protoc-gen-ts="${PLUGIN_TS}" \
--proto_path=./proto \
-I $DIST_DIR \
./proto/*.proto

基本的に npm 以外は docker に包んでいた関係でサーバ側を生成する時はコンテナ上の protoc を使っていましたが、クライアント側のコードを生成したくなった時に npm がホスト側にしかないので結局 GitHub からホスト上に protoc のバイナリを持ってきてそちらを使いました。

HTTPとgRPCの間に proxy を置く前提の grpc-web というものもあり、一度それと混同してハマりました。 生成したコードはあとで frourio の server 側のディレクトリに移動させます。

アプリケーションのバックエンド

参考にした記事では Go でしたが、今回はバックエンドも TypeScript で実装してみます。 frourio 内部では nodeフレームワークに Fastify、 ORMに Prisma を選択しました。

create-frourio-app した段階で作られるサンプルを真似してそれぞれのレイヤーを実装していきました。

まずはモデルをつくります。 今回は商品を表す Item と、購入の履歴を入れる Purchase を作ることにして、Prisma のマイグレーションファイルを書きます。 全体を動かすことを優先して顧客情報などは考えないことにしました。

// schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Item {
  id Int @id @default(autoincrement())
  name String
  description String?
  amount Int
}

model Purchase {
  id Int @id @default(autoincrement())
  item Item @relation(fields: [itemId], references: [id])
  itemId Int
  amount Int
}

当初普通に SQL で書く migration を先に作ってたんですが、そこは frourio が面倒見てくれるっぽい。(リポジトリにコミットされてる中身はちょっとおかしい)

次は Prisma の client を使って実際に操作を行う service を書きます。 Rails の Service層 とほとんど同じノリなので理解しやすいです。レコードを作る時の relation の書き方がちょっと変わっていて(主観)、操作に応じて異なる型を要求されます。 ActiveRecord に慣れているとちょっとつまづきそうです。

ドキュメントはこの辺りです。

// belongs to item な purchase を作る例
prisma.purchase.create({
  data: {
    amount: item.amount,
    item: {
      connect: {
        id: item.id
      }
    }
  }
});

先ほど生成した gRPC のクライアントもこの service から呼びます。生成したコードが要求するので grpc パッケージが別途必要になります。 フロントエンドのディレクトリの中にサーバサイドのディレクトリが同居しているので、独立した node_modules がそれぞれあることを忘れて、親の階層にパッケージを追加してここでもハマりました。

最後に Controller を定義します。 ディレクトリでリソースが表現されていて、それぞれの配下の index.ts に action ごとの型定義を、 controller.ts に実際の処理を書きます。 この controller は相当に薄いので、あくまで service に詰めたロジックを呼び出して結果に応じたレスポンスを返すことに徹します。

アプリケーションのフロントエンド

ここは普通に Next.js のアプリを作るだけですが、 React も初めてだったので個人的には苦戦しました。 商品の一覧が見られるページと、購入フォーム付きの詳細ページをつくりました。

クレジットカード用のフォームは PAY.JP が出している Checkout というスクリプトがあり、さらに先人が React のコンポーネント化したものを公開してくださっていますが、今回は Function Component を使っていたので移植を諦めて、 自前の form と payjp.js(v1) を使ってカード情報をトークン化しています。

カードのトークン化には PAY.JP の公開鍵が必要で、それをハードコードしたくなかったので、 next.config.js にマッピングを書いてサーバサイドから環境変数の値をもらうようにしました。Next.js に慣れている人からすれば当たり前なのかもしれませんが、個人的には なるほど!となりました。

module.exports = {
  env: {
    payjpPublicKey: process.env.PAYJP_PUBLIC_KEY,
  },
}

ここまででようやく一通り動く状態になりました。なんだかんだで3-4日くらいかかりました。

雑感

いろんなことを同時にやったので浅〜い感じになりましたが、けっこう面白かったです。

今社内では Angular や Nuxt を使ってますが、2021年は React 使いたいみたいな動きもあったりして、キャッチアップの足がかりとしては良かったかなと思います。バックエンドに軸足を置いて仕事している人間的には API についても型定義をフロントと共有できると幸せとか、 REST でフロントが欲しい情報が不足してる時の手間とかを考えて GraphQL を部分的に導入したいとか、 MicroServices を考える上で何となくハードルになってた gRPC って何ぞやとか、いろんな試みへの手がかりをつかめたような気がしたのも良かったです。

もう少し手を動かしてみて、ちょっとずつ業務にも取り入れていきたいです。

node + gRPC については 最近 mercari の人も記事を書いてて、結構ナウいんだなと思いました。

参考