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

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

タグ:csv

「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 テクニカルガイドブック

MVC 型の PHP アプリケーション開発フレームワークである cakePHP を使ってます。単なるテンプレートエンジンである以上に、日本に利用者が多い(つまり日本語の情報を見つけやすい)こともあって、とても便利です。
cake-logo


そんな cakePHP を使って作っているアプリケーションで、「TSV(CSV) ファイルをダウンロードさせたい」という機能を実装する方法を紹介します。なまじ View/Controller の組み合わせに慣れてしまうと、TSV(CSV) のような View を無視する機能を cakePHP でどう実装するか、という点に迷いますよね。TSV ファイルを作成する前提で調査結果をまとめました。

結論として View(***.ctp) は用意しません。そして Controller 側に以下のような記述を行います:
function download(){
  $this->autoRender = false; //. Viewを使わない
  Configure::write('debug', 0); //. debugコードを出さない

  //. HTTP ヘッダを TSV 用に変更
  header ("Content-disposition: attachment; filename=test.tsv");
  header ("Content-type: application/octet-stream; name=test.tsv");

  //. TSV 本体を生成する部分
  $tsv = "";
  $items = $this->Item->find('all');
  foreach( $items as $item ){
    //. 日本語部分をシフトJISに変換しながら TSV の一行を作成
    $line = $item['Item']['id'] 
      . "\t" . mb_convert_encoding( $item['Item']['name'], "SJIS" )
      . "\t" . mb_convert_encoding( $item['Item']['body'], "SJIS" )
      . "\n";
    $tsv .= $line;
  }

  print($tsv); //. 生成した TSV を出力
  return;
}

肝として、まず View を使わずに処理を終えたいので、autoRender プロパティを false に変更します。また出力結果に debug コードを出したくないこともあるので、debug レベルを 0 に設定しておきます。 更に TSV ファイルをそのまま(選択肢なしで)ダウンロードさせたいので、HTTP ヘッダをダウンロード用のものに変更しています。

TSV 本体部分のコーディングは出力したい内容によって異なりますが、普通に出力したい内容をタブ("\t")区切りにして1行ずつ生成します。これだけでもいいのですが、もし出力した TSV をそのままエクセルで読み込みたい場合などは、文字コードをシフト JIS にしておく必要がある点に注意が必要です。

生成した TSV を最後に出力して処理終了、です。


cakePHP はとても便利なフレームワークですが、今回のような(ビューを使わない)特殊な処理を行う場合は少し特殊な手順が必要になる、というサンプルです。


 

このページのトップヘ