まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

「REST API の標準化」を考える機会がありました。実際に格納したり取得したりするデータ自体の構造は当然ケース・バイ・ケースになるわけですが、その呼び出し方とか、パラメータの指定方法とか、結果のフォーマットとかを社内や団体内で共通化すると、単に便利なだけでなく、一度使った後に新しい別の API を利用する際にも理解を早めることができます。

で、具体的にはどのように共通化すべきで、そこにはどういった要素が考慮されるべきか、といった内容を自分なりに考えてみました。

ここには色んな流派というか、考え方の基本となるパターンがあるのですが、今回は自分の経験を元に、自分はこういう API にしている、こういう API だとわかりやすい/覚えやすい、という基準で、自分なりに標準化したものを紹介します。


【考慮すべき要素】
詳しくは後述しますが、以下の6つについては標準化時に抑えておく必要がある要素であると思っています:

0. id と日付時刻の扱い
1. API をリクエストする際のメソッドと URI
2. API をリクエストする際のパラメータ
3. API からのレスポンス
4. セキュリティ
5. テスト
6. 公開方法



また、以下で説明する内容については、下図のような商品マスターデータ(items)を対象とした REST API を作ることを想定した例として紹介します:
idcategory_idnamemaker_idpriceimage_urlcreatedupdated
x100x051○○シャンプーx10001700http://xxxxx/100.jpg16054028361605402836
x101x051××シャンプーx10002800http://xxxxx/101.png16054038361605403836
x102x052□□コンディショナーx100101000http://xxxxx/102.jpg16054048361605404836
x103x053▲▲ヘアオイルx10001500http://xxxxx/103.jpg16054058361605405836
x104x061◎◎ボディソープx100211000http://xxxxx/104.png16054068361605406836
x105x071◆◆天然化粧水x100102000http://xxxxx/105.png16054078361605408836

※数字は全て integer 。category_id や maker_id は別途 category や maker のマスターデータが存在していて、外部参照キーだけが格納されているイメージです。


【0. id と日付時刻の扱い】
まずは API の考慮点というよりも、その API で対象とするデータにおける考慮点です。既に存在しているデータベース等を対象とする場合は今からの変更は難しいかもしれませんが、新たにデータベースから作成できるのであれば API 利用を想定した設計にしておくべき、という考慮点です。

まずいわゆる id 値について。基本的にはテーブル内でユニークな値であればよいのですが、ここを integer 型とすべきか string 型とすべきか、という考慮点があります。メリット・デメリットを考慮した上で選ぶべきであると思っています:
 メリットデメリット
integer 型・データ量が少ない
・データベース側に自動生成機能があることが多い
・推測しやすい、推測される可能性がある
string 型・存在する id の推測が困難
・値そのものに意味をもたせることも可能(例:メールアドレス)
・1件あたりのデータ量が増える(といっても現代では大したことはないかも)
・生成の仕組みが必要


どちらのケースも存在しているし、どちらか一方に不利な要素があるわけではないのですが、個人的には「今から新規に作るなら string 型」だと思っています。理由は「今では string 型のデメリットが大したことない」のと「 ID が自動生成されるミドルウェアでは string 型になることが多い」ので、結果的に「ID は string 型にした方が統一しやすい」と思っています。

ID に加えて、データレコードの作成日時(created)と最終更新日時(updated)は全てのレコードに加えるべきだと思っています。ソフトデリートを有効にする場合は削除日時(deleted、初期値は 0 か null)も加えます。


なお日付時刻については後述する特別な理由がない限りは UNIX タイムスタンプ値を使うべきです。YYYY-MM-DD のような特定の文字列フォーマットで格納してしまうとタイムゾーンを考慮することができなくなってしまうため、データとしてはタイムスタンプ値で格納し、表示する際に変換する、という方法が理想的です。

※ただし「日本からしか使わない」ことに加えて「特定日だけのデータを取り出す」といった用途がある場合などはあらかじめ日付フォーマット変換しておいた方が便利になることもあります。日付フォーマットを利用する場合は、そのフォーマットもあらかじめ標準化しておくべきです(YYYY-MM-DD とか YYYY-MM-DD hh:mm:ss など)。なお、このあたりは個別事情を考慮して対応する必要があるため、標準化ルールの対象外とする場合もあります。


【1. API をリクエストする際のメソッドと URI】
リクエスト時の考慮ポイントが1番多くあると思っています。順に説明します。

「メソッド」はいわゆる HTTP メソッドのことで、一般的には GET(取得・検索)、POST(作成)、PUT(更新)、DELETE(削除)が用いられます。API の用途や目的に合わせてメソッドを選びます。

より多くの考慮が必要なのは URI 部分となります。まず前提として以下の条件を逸脱しないよう注意してください:
  • 文字コードは UTF-8
  • リクエストデータに外字は使わない
  • URI ではキャメルケース(userData)ではなくスネークケース(user_data)を使う※
  • %(パーセント)でエンコーディングする必要があるデータは URI に含めない
※ホスト名部分が大文字・小文字の区別をしないためです

次にホスト名ですが、理想的にはホスト名の一部に "api" という文字が含まれていることが挙げられます。これが難しい場合はパス名のどこかに "api" を含めて、この URI が API のための URI であることを明示します。以下では api.mycompany.com というホスト名を使う想定で説明を続けます。

パス名の中には今後の仕様変更に対応できるよう、 API のバージョン("v1" など)を含めるようにします。

データの種類(この場合は items)も URI の一部に含まれている必要があります。このあたりから流派によって扱いに違いが出てくる部分ですが、自分は「単数を扱う API なら単数形(item)」、「複数を扱う API なら複数形(items)」としています。そして単数を対象とする場合はその id をパス内で指定するようにします。

例えば以下のようにする、ということです:
  • GET https://api.mycompany.com/v1/items (複数の items を取り出す
  • GET https://api.mycompany.com/v1/item/x100 (id = "x100" の item を取り出す
  • POST https://api.mycompany.com/v1/item (item を1件新規登録する)
  • PUT https://api.mycompany.com/v1/item/x101 (id = "x101" の item を更新する)
  • DELETE https://api.mycompany.com/v1/item/x102 (id = "x102" の item を削除する)
  • POST https://api.mycompany.com/v1/items (items をまとめてバルクインサートする)
  • DELETE https://api.mycompany.com/v1/items (items をまとめてバルクデリートする)
  •   :
リクエストした結果をどのようなフォーマットで取得するか、というフォーマットの指定も(特定フォーマットで固定、ということでなければ)リクエストに含めることができるべきです。一般的には URI の最後に拡張子の形で指定できるようにすることが多いようです:
  • GET https://api.mycompany.com/v1/items.json (複数の items を JSON で取り出す)
  • GET https://api.mycompany.com/v1/items.xml (複数の items を XML で取り出す)
  • GET https://api.mycompany.com/v1/item/x100.json (id = "x100" の item を JSON で取り出す)
  • GET https://api.mycompany.com/v1/item/x100.csv (id = "x100" の item を CSV で取り出す)
  •   :
拡張子が指定されていなかった場合は、エラー扱いとするか、またはあらかじめ決められたデフォルトフォーマットのリクエストに転送されて処理されるようにするか、を決めておきます。

なお、そもそも CSV で取り出すことができないようなデータ構造のオブジェクトを対象とする場合は無理に CSV に対応する必要はないと思っています。

また「検索」や「アップロード」といった処理については別の URI を用意することになります。



【2. API をリクエストする際のパラメータ】
一部 3. で考慮する内容も含まれるのですが、リクエスト時の URL パラメータをどのように標準化するべきかを記載します。

複数のデータが返ってくる可能性のある API を実行する場合、全データ量が膨大にならないようパラメータで制御することがあります。この例を使って以下を紹介します。

一般的には limit パラメータと offset パラメータを用いて取得範囲を指定します。limit は取得件数、offset は「何件目から」を指定するパラメータです。一般的に offset のデフォルト値は 0(1件目から)で、limit のデフォルト値は 100 以下が推奨されています:
  • GET https://api.mycompany.com/v1/items.json?limit=20&offset=100 (items を 100 件目から 20 件、JSON で取り出す)
  •   :

これら以外によく使われるパラメータとしては以下があります。どのようなパラメータに対応すべきかを事前に決めておきます:
パラメータ用途
sortソートキーを指定
since指定タイムスタンプ以降のデータを取り出す
until指定タイムスタンプ以前のデータを取り出す
fields実行結果に含まれるフィールド値を指定したものだけにする(カンマ区切り)


【3. API からのレスポンス】
API の(実行できなかったケースも含めた)実行結果にもある程度の共通化・標準化がされていると、実行後の処理ハンドリングに手間取ることが少なくなります。このレスポンスは (1) 成功した時と、(2) 失敗した時 の両方を考慮した上で標準化する必要があります。ただし KML や iCal, GeoJSON など、フォーマットが規定済みである場合は、そのフォーマットに従うものとします。

考慮点を以下に挙げますが、多くの場合で開発者はまず API を実行し、その結果を見て開発していく、という流れになります。そのため返却された内容で実行結果を判断できるようになっていることが望ましいと考えられます。

まず (1) 成功時のレスポンスデータには以下のような内容が含まれているべきと考えられます:
パラメータ用途
statusHTTP ステータス
resultset全データ件数、offset 値、limit 値
result実行結果(配列)


{
  status: 200,
  resultset: {
    count: 3,
    offset: 10,
    limit: 20
  },
  result: []
}


また (2) エラー時のレスポンスデータには以下の情報を返して原因追求できるようにするべきです:
パラメータ用途
statusHTTP ステータス
typeエラー種別
errorエラー内容


{
  status: 401,
  type: "not authorized",
  error: {
    message: "Not authorized to perform this operation."
  }
}

【4. セキュリティ】
フォーマットの取り決め、という意味での標準化は上述のような感じですが、フォーマット以外にも標準化の対象はあります。その1つがセキュリティ項目です。代表的なものを挙げてみました:
観点対策説明
通信の改ざん、盗聴通信暗号化SSL(https)通信に対応することで送受信データの信頼性担保および情報漏えいの防止を行う。
情報漏えいAPI キーなどによる認証API 利用者に認証をかけることで API を利用できる人を制限する。また利用者毎の利用頻度を確認できるようにすることで API キーが漏洩している可能性を早めに特定できるようになる。アクセストークンを利用する場合はアクセストークンの有効期間を適切に設定して、トークン漏洩時の2次被害を最小にする。
負荷対策利用制限、キャッシュAPI に DDoS 攻撃が行われることを想定した管理が必要。コール数に対する利用制限をかけたり、同一の GET リクエストに対してキャッシュレスポンスで対応する、など
クロスドメイン間の通信CORS 対応ウェブブラウザではクロスサイトスクリプティング防止の観点からクロスドメイン間通信は行わない仕様となっている。場合によってはクロスドメイン間通信を許可する必要が生じ、そのための対応が必要となる




【5. テスト】
API を実際に動かしてテストする上での、テスト環境やテスト方法を標準化することで、テスター担当者の負担を軽減することができます。

一般的には動作を確認できるようなフォームやサンプルプログラムを用意して実際に実行し、そのレスポンスを確認することになります。

なお、6. で後述する Swagger ドキュメントを利用することでテスト用フォームを自動生成することができます。


【6. 公開方法】
API をドキュメント化してテスターや開発者に公開するまでの流れを標準化する、という項目です。このための各種ツールも存在していますが、以下では Swagger ドキュメントを紹介します。

Swagger ドキュメントは Open API Initiative が提供するオープン企画 "OAS" で採用されている REST API のドキュメント化規格です。

Swagger Editor や、YAML による特定フォーマットで API を記述することで、インタラクティブなドキュメント UI を生成することができます。単に URI やパラメータの説明をするだけでなく、実際にAPI を動かすことができる、という点が最大の特徴となっています。開発者視点では実際に API の動作を確認しながら仕様を確認できるのでとても有用な API ドキュメントといえます:
2020111801



【参考】
 内閣官房情報通信技術(IT)総合戦略室  API テクニカルガイドブック

そろそろ、その時はやって来たかな?
時は来た!


これまでメインのプログラミング開発環境を Windows10 の WSL(Windows Subsystem for Linux)Ubuntu 18.04 にしていました。WSL2 のことはもちろん知っていましたが、メイン機のスペックが特別高いわけでもなく(メモリ 8GB)、WSL で困らないであろう範囲のプログラミングをメインにしていました(例えばコンテナ環境が必要になった場合は別環境に用意して、そこにアクセスする、など)。

一方で WSL2 をサポートする開発周辺環境も整ってきました。Docker Desktop for Windows もリリースされ、もう mac と比較しても「アプリ開発環境は Windows 機 + WSL2 でいいんじゃないか?」と思える状況になってきました。スペック面での不安がないわけじゃないけど、環境的な意味では「時は来た」かな、と。

というわけで、自分の WSL 環境を WSL2 環境へ移行することにしました。以下その手順を備忘録として残しておきます。


【Hyper-V を有効化】
まず Windows の Hyper-V を有効化する必要があります。既に Hyper-V が有効になっている環境であればこの手順は不要ですが、今回はここも含めて紹介します。

その手順で利用するため Windows で PowerShell を起動します。この際に管理者権限で起動しておくようにします(バージョン確認だけなら管理者権限は不要ですが、WSL を WSL2 に変更する際に管理者権限が必要になります)。

そして以下のコマンドを実行します:
> Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All

そしてこのコマンドを実行後に再起動をかけると Hyper-V が有効化されます。ここから以下の作業は再起動後に行ってください。


(↓2020/11/22 追記)
【Windows ハイパーバイザープラットフォームの有効化】
VirtualBox など、他のハイパーバイザー環境との共存のための機能を有効にします。

Windowsの機能の有効化または無効化から Windows ハイパーバイザープラットフォームを選択して、チェックを入れて OK をクリックします:
2020112201
(↑2020/11/22 追記)


【WSL のバージョンを確認】
ここから下の PowerShell での操作については(管理者権限の)コマンドプロンプトでも構いません。

Hyper-V を有効にして再起動後、まず現在利用中の WSL のバージョン(1のはず)を確認しておきます。PowerShell で以下のコマンドを実行します:
> wsl --list --verbose

すると以下のような結果になり、WSL(1) を使っていることが確認できます:
2020112101


【WSL の Linux カーネルを更新】
この WSL を WSL2 に変更するのですが、その前に WSL の Linux カーネルを最新版に更新しておきます。

まず以下へアクセスします:
https://docs.microsoft.com/ja-jp/windows/wsl/wsl2-kernel


「最新の WSL2 Linux カーネル更新プログラムパッケージをダウンロード」と書かれたリンクから更新パッケージをダウンロードします:
2020112102


ダウンロード後に実行して、WSL の Linux カーネルを更新します:
2020112103


【WSL を WSL2 に変更】
ここまでの準備ができていれば WSL を WSL2 に変更する準備が整いました。管理者権限の PowerShell で以下を実行して、まずは WSL のデフォルトバージョンを 2 に変更します(一瞬で終わります):
> wsl --set-default-version 2

続けて以下のコマンドも実行し、現在 WSL で動作中の Ubuntu 18.04 を WSL2 に変更します(少し時間がかかります):
> wsl --set-version Ubuntu-18.04 2

2020112101


これで WSL2 になった、はず。。


【WSL のバージョンを再確認】
最後に WSL のバージョンが2になっていることを確認します。再度先程と同じコマンドを実行し、バージョンが更新されていることを確認します:
> wsl --list --verbose

2020112102


WSL1 から WSL2 への環境移行に成功しました。

この記事の続きです。PayPay API を自分のアプリケーションで使うための準備やサンプルコード、使い方などはこちらを参照してください:
PayPay の API でアプリケーションに日本円での決済機能を実装する

こちらのエントリでは応用編というか、実際のアプリケーションの中で PayPay API を使って決済する際に意識することになるであろう事柄3点について考慮すべきポイントを紹介します。


1 作成した QR コードで実際に決済されたかどうかをアプリケーションから知る方法

前回の記事では簡単にウェブアプリケーションに PayPay による決済機能を組み込んで QR コードによる決済を実現することができる、という内容を紹介しました。ここまではよい、と。 ただ実際には QR コードを作成する所まではトラッキングできますが、作成した QR コードを使って決済が行われても、それは元のウェブアプリケーションの画面内で行われる決済ではないため「本当に決済までが行われた」のか、それとも「QRコードは表示されたけど、PayPay での決済は行われなかった」のかを識別する方法は別に考慮する必要があります。それをどうすればよいか、というのが実運用における最初のポイントです。

これは実は PayPay API 側で用意された機能を使うことで実現できます。前回の記事を参照いただくとわかるのですが、生成された QR コードを表示する際に以下のような JavaScript コードが実行されていました:
          :
          :
      success: function( result ){
        console.log( result );
        if( result && result.status && result.status == 201 && result.body && result.body.data ){
          var merchantPaymentId = result.body.data.merchantPaymentId; //. 支払いID(キャンセル時に必要)
          var codeId = result.body.data.codeId; //. QRコードID(QRコード削除時に必要)
          var url = result.body.data.url;  //. QRコードが表示されるページの URL

          if( url ){
            //. QRコードが表示されるページを別ウィンドウで開く
            window.open( url, 'PayPayWindow' );
          }
        }
      },
          :
          :

QR コードの表示は result オブジェクトの result.body.data.url の値を参照して、この値で示される URL を別ウィンドウで開くことで実現していました(別ウィンドウで開くため、これ以降の直接のトラッキングはできなくなっていました)。この時に取得できる result.body.data.merchantPaymentId という値を使って PayPay APIGetCodePaymentDetails 関数を実行すると、その QR コードのステータスを確認することができるのでした。ステータスを確認し、COMPLETED となっていればその決済は完了しており、そうでなければ完了していない、と判断できることになります。

より具体的にはこの merchantPaymentId をデータベースなどに保存しておいて、Webhook と使ったり、cron で定期的に確認したり、いずれも難しい環境下であっても例えば次回ログイン時などにステータスをチェックすることで決済の済/未済の判断ができるようになります。


2 作成した QR コードを「どのユーザーが」決済したかを判断する方法

1で QR コードが実際に決済されているかどうかを識別する方法を紹介しましたが、実運用においてはもう1点、その QR コードを「どのユーザーが」決済したのかを知る必要があります。これも決済そのものは簡単に実現してしまうのですが、別ウィンドウで開くページ内での決済のため、自分が作ったウェブアプリケーション内で決済ユーザーを識別するのが難しい、という問題を解決する必要があります。

こちらは少しコードを修正する必要がありますが可能です。現在のソースコードでは上述の merchantPaymentId をサーバーサイドで一意になるよう生成していますが、ここを変更し、クライアントサイドで「ユーザー名やユーザーIDを用いて merchantPaymentId を生成する(あるいはサーバーにユーザー ID を送信して、サーバーサイドでユーザー ID を用いた merchantPaymentId を生成する)ことで解決できそうです。つまり merchantPaymentId をただのランダムな文字列ではなく、(例えば頭にユーザーIDを付与するなどして)ここを見ることで誰が作成した QR コードなのかがわかれば、(1と合わせることで)誰が作成した QR コードが決済されたのか、も識別することができるようになります。


3 1回の QR コード決済で定期的な支払いをする方法

例えば月額課金のサブスクリプション型サービスと契約すると、毎月決まった額が引き落とされることになります。これを PayPay API の QR コード決済でも1回で実現できると、自分の作ったウェブアプリケーションのサブスク化ができるようになって便利だと思っています。

が、実はこの内容だけは現時点で実現の目処が立っていません。そもそも QR コードで分割払い契約って可能なんだろうか・・・

PayPay API や SDK のドキュメントも見ているのですが、なんとなく「現時点では未対応」なような気もしています。サブスクだと「解約」処理も必要になると思うのですが、そういう API も見当たらないような・・・ 今の時点においては毎月 QR コードを作って、その都度払ってもらう以外の方法はないのかもしれません(情報/アイデア求む)



というわけで、PayPay API を使うことでウェブアプリケーションに決済機能を簡単に付与することができるのですが、実運用までを意識すると、ここに挙げた3点についても考慮して設計・実装する必要があると思っています。ただ3番目の定期支払いはなんとか1回の処理で実現したり、それを途中で解約するような具体的な方法ないかなあ。。。

このページのトップヘ