2年ほど前にiOS, Android アプリをリニューアルすることになり。それと同時にAPIも完全に作り直すことになりました。
その際に API の設計について考えてたこと、解決してきたこと、解決してないことを公開できる範囲でまとめてみました。
方針的なところ
ドキュメントの信頼性をある程度担保できるようにする
- Swagger UI を利用してその場でAPIを実行できるドキュメントにした
- 仮に書いてあるコメントが間違ってたとしてもその場で API を実行できるので 少なくともレスポンスは信用できる。
- 現在となってはAPIの様々なリクエストパターンを網羅するぐらいたくさんAPIがあるので、既存のAPIの設定をコピーしてちょっといじるぐらいでできる。手間が全然かからない。
- APIの実装者以外の人が簡単にAPIを実行できるので、APIが悪いのかアプリの実装が悪いのか問題を切り分けるのが簡単
- APIの仕様を伝えるコストが軽減した
- Swagger は OAuth も対応しているので認証周りも安心
外部に公開しているAPIであれば頻繁に変えることはないので別途ドキュメントページ(例えば github pages など)などを使っていても問題ないですが、頻繁に変更することがあるAPIの場合はSwagger にちょっとコメントを書いた程度のもののほうが内容との乖離が出にくくてよいと思います。
ドキュメントはあまり詳細に書きすぎているとメンテが大変なのでほどほどに。
アプリのリリースサイクルと合わせる
社外の方も利用するAPIとは以下の理由から分けた方がよいです。
- 社外で使われているが故に仕様変更がしづらいし、時間がかかる。APIの利用者を管理している人がいなければ何が依存しているのか分からなくなりがち。
- 実装が変えづらい・廃止しづらい。ゆえに不要な実装が貯まってよくわからないコードに。
- 変更するのが怖いので場当たり的な対処や継ぎ足しでコードを書くことになりがち。
- アプリは Apple や Google Play ストアの規約などに遵守するために アプリでしか使わないようなAPIや、要件によってはアプリ専用の実装を挟み込んだりすることがある
- レスポンスの形をアプリで利用しやすい & リクエストが少なくて済む形にしたいので、アプリの都合だけでレスポンス形式が変更できたほうがよい
一部のAPIは社外に公開するようなAPIと似たような形にはなったりするので、統合したくなるのも分かるところでありますが、メンテナンスの観点から分けたほうがよいというところです。この分ける作業、かなりの労力が必要でした。
強制・任意アップデート用のダイアログを出せるようにする
- 仕様を永遠に変えないことはできないので、どこかでAPIの互換性を切る必要がある
- アプリの自動更新をオフにしているユーザーもいるので、この機構がないとある日突然アプリが動かなかったみたいなことが発生してしまいます
- いきなり強制アップデートダイアログでアプリ更新するよう誘導してしまうと反感を買うので、閉じることできる任意アップデートダイアログ(更新してくれよダイアログ)を表示して誘導して上げると親切。
実装は単純です。強制・任意アップデート対象になるバージョンを返すAPIを用意しておいて起動時などにそれをチェックするだけです。面倒なら Firebase Remote Config にバージョン番号を突っ込むだけという形で運用する形でもいいと思います。
コードの方針的なところ
Null チェック地獄を避ける
Android Java のコードで null チェックだらけでカオスになっていることがよくありました。例えば以下のような感じ。
String url = null;
if (illust != null && illust.imageUrls != null && illust.imageUrls.medium != null) {
url = illust.medium;
}
この null チェックの嵐がすべてのレスポンスに対して行われていると、コードが読みづらいので以下のような方針にした。
- json のキーは基本的にレスポンスから消さない。もちろんjsonのキーに対する値 '{ natoka_key: "ここ"}' は null になることもある。
- キーがパラメータ関係なく、常に見えるのでレスポンスの構造が分かりやすくなるという別の利点もある。
- 文字列を値として返す場面で null でなくても良い場合は null を返さない
- 例えば値として文字列を返すような部分では null でなく空文字 "" も使える。要件的に問題がなければ使ったほうがよい
- 普段値として配列を返すところでは空のときに '[]' を返す
- ほとんどのケースでこれで大丈夫なはず
- もちろん特殊な要件、「設定されていない」という状態と「空」という状態の2つのパターンを定義する必要がある場合はもちろん無理
Swift の場合はコード的には null チェック地獄にはならないが、どのキーに問題があって null になったか挙動的に分かりづらいので避けたいいと思います
配列をAPIに渡したいケースはフレームワークが対応している配列形式で渡す
以下のようにだいたいの言語のフレームワーク等は配列形式で受け取るのに対応しているので、以下のような形でリクエストするようにしました。
test.php?value[]=1&value[]=2 // php の場合はこの形式
/test?value=1&value=2 // 利用している環境やフレームワークによってはこの形式
実はこのような形式で配列として渡すことができる仕様は標準化されているものではないです。
そこで私はカンマ区切りでデータを渡すような形で最初のAPIを作り運用していましたが、あとからカンマを値として渡す要件が出てきて破綻しました。。
素直に最初からフレームワーク等が対応している配列形式で渡すのがいいですね。
全部 json にして渡すという手もありますが、わかりやすさと Swagger も対応してて使い勝手がいいというところも配列形式にしておいたほうがいいと思います。
レスポンスを返す前にかならずチェックを入れる
これは静的型付けの言語を利用している場合は気にする必要はないです。
私たちは php のような動的型付けの言語を利用したので、リファクタリング等で返すレスポンスが変化してトラブルを起こすことが多々ありました。
このトラブルが起きていることに気づかず放置してしまうと、前述した「Null チェック地獄」への階段をのぼることになってしまうので、レスポンスを返す前に以下のような形にし、値について期待する型をサーバー側でチェックするようにした。
$json_array = [
'moji' => ensureStringOrNull($string_datam)
'is_okay' => ensureBool($mydata)
];
echo json_encode($json_array);
function ensureBool($value) {
if (!is_bool($value)) {
throw new RuntimeException('yokunai!!');
}
}
function ensureStringOrNull($value) {
if ($value !== null && is_string($value)) {
throw new RuntimeException('yokunai!!');
}
}
やってる事自体はただチェックする関数でラップしているだけです。
実際やってみると様々なケースを検知することができました
- 仕様変更、リファクタリング等のミス
- 特殊な条件のときだけに発生するバグによるもの。
- データベースに格納されているデータがおかしいことによるもの
- 入力時のバリデーションに漏れ、または実装ミスによるもの。
また、この形式にしておくとサーバー側から意図しないデータが json に入れてくることを一切想定する必要がなくなるので、アプリのコードをシンプルに保つことができます。(Null チェック地獄にもならない!)
構成を考える上で実際にやってみて途中でボツになったもの
gRPC
- 真っ先に選択肢として上がったが、当時はリリースされてそんなに時間が経過してなかったので iOS(Objective-C) のクライアントをビルドすることができなかったので不採用
- 今はいい感じかもしれない?
Apache Thrift
当時、Android のコードが壊れていたので、pull request を投げたらいい感じに別のコードに変わる形でいい感じに直してくれました。
https://issues.apache.org/jira/browse/THRIFT-3220
- Objective-C のコードを吐き出すものがあったが、特定の条件の書き方をするとコンパイルエラーになるコードが吐き出されることがあった。
- Android(Java) の実装は Apache HttpComponents に依存しているが、okhttp(HTTP2)が使いたい。
- チームの中で Thrift のバージョンを統一するのが面倒くさい。Thrift の学習コストも追加で掛かる。
- ちょっと curl で試す、みたいなことができないので、気軽に動作を見たりすることができない。
RPC について改めて調べなおしてたら良い記事を書いている人がいました。とても分かりやすく RPC の存在を踏まえた上での OpenAPI (Swagger)の良い点がまとめられています。
OpenAPI Specification と Swagger Codegen に至るまで辿った道
JSON Schema を利用したレスポンス形式のバリデーション
Swagger の定義は一部、Json schema という json で定義を書くとそれに基づいてバリデーションを行うための規格があり、これを利用してレスポンス形式のチェックをしてから APIで json のレスポンスを返すことをやってみました。
ライブラリは以下のものを使用しました。
https://github.com/justinrainbow/json-schema
実際にやってみると、
- Swagger 定義と JSON Schema の定義は微妙に異なり、Swagger ではエラーにならない範囲で JSON Schema 的に正しい記述の仕方をする必要があり難しかった。
- 自分自身はうまいことやって使ってたけど、他の人にこれをやってもらうのは厳しいと思った
- 当時は使用してライブラリも若干バグってたのでそれ合わせてつらかった。今は治ってる。
- レスポンスがそれなりに大きい json をこのライブラリでレスポンス時のjsonの型チェックをしていると結構重かった。数倍レスポンスが遅くなることも。php7 を使うとそこまで気になるほどでもなかったけど、当時は php 5.5 だった。
###Swagger Edtior
書いた設定ファイルをリアルタイムにチェックして間違いを指摘してくれるのはありがたかったが、以下の理由でやめました。
- 使っているうちに定義が増えてきて、基本的に既存のものをコピペしてちょっと書き換えるぐらいの作業しかしなくなったので意味がなくなった
- 当時は Swagger UI と Swagger Editor のコードベースがバラバラだったので、Swagger UI でちゃんと表示されても Swagger Editor では表示されないなどがあって混乱を招いた(今はコードベースが共通化されているらしいです)
- Swagger Edtitor は結構バグが多くて疲れた
最終的な実装
紆余曲折を経て最終的に Swagger を使用しつつ普通に json を返すだけのありきたりな実装になりました。
失敗を認識してるけど、そのままになっている方針
以下のような感じで json にページャを実現するために”次にリクエスト”するURLを返す方式で実装しました。
{
data: "aaavvvcc",
next_url: "http://example.com/test?page=2"
}
http://example.com/test に対してリクエストすると上記のようなレスポンスが返ってきて、もっと件数がほしいときは next_url に対してリクエストを投げると次のページのコンテンツを取得できるという形式です。
これをアプリを実装する上では便利だったのですが、サーバー側で以下のような問題がありました。
- 様々な事情で php 側でデータを取り除く処理を(unsetする処理)を挟む必要が出てきたりする。したくはないんだが・・。
- DBが別れていると join できないので、どうしても片方のDBで 30 件と取れてももう片方で 30 件に対応するデータが確保できるとは限らない。
- 30件データを返すAPIで次があるかどうかを厳密に保証するには31件のデータを取得して1件捨てるみたいな対応が必要。このコードは見づらい・・。
なかなか難しいですね。
最後に
- とっても短い期間でやってるので大変だった。大変すぎた。