読者です 読者をやめる 読者になる 読者になる

WEB API The Good Parts 3章の自分的まとめ

前回からの続きで、今回は以下の本の3章を自分的にまとめたいと思う。

Web API: The Good Parts

Web API: The Good Parts

3. レスポンスデータの設計

各サブの章を一言でまとめられた。
エラー部分については重要だなぁ、と感じたため、しっかり解説。

  • データフォーマット
    JSONをサポートすべき。それ以外(xml、MessagePackなど)はオプション。
    データフォーマットの指定方法は以下の3つ。記号はおすすめ度。

    • ○ クエリパラメータ
    • × 拡張子
    • ○ Acceptヘッダ

  • レスポンスの内容をユーザが選べるようにする
    そのままの意味。選べる値はフィールドごとか、グループ単位かのいずれかで、その情報をクエリパラメータに付与する。

http://api.example.com/v1/users/12345?fields=name,age
  • エンベロープは必要か
    メタデータを含んだ形ですべてのAPIが同じデータ構造を返すために実際のデータをくるむための構造をエンベロープと呼ぶ。
    HTTPヘッダがメタ情報を持っているため、エンベロープは不要。

  • データはフラットにすべきか
    不要な階層化はするべきではないが、階層化すべきものはする。
    たぶん開発時は普通にオブジェクトをつくれば、それで階層化すべきものはされ、不要に階層化していない形になると思う。

  • 配列とフォーマット
    トップレベルに配列を包んだオブジェクトを置くか、直接配列を置くか。
    筆者としてはオブジェクトで包んだほう推し。理由は以下のよう。

    • レスポンスデータが何を示しているものかがわかりやすくなる
    • レスポンスデータをオブジェクトに統一することができる
    • JSONインジェクションを防ぎ、セキュリティ上のリスクを避けることができる

  • 各データの名前
    前回と同じ考え方。

    • 多くのAPIで同じ意味に利用されている一般的な単語を持ちいる
    • なるべく少ない単語数で表現する
    • 複数の単語を連結する場合、その連結方法はAPI 全体を通して統一する
    • 変な省略形は極力利用しない
    • 単数形/複数形に気を付ける

  • 大きな整数とJSON
    32bit を超える数値は丸め込み誤差が発生するため、文字列を使うべき。

3.6 エラーの表現

3.6.1 ステータスコードでエラーを表現する

HTTP のステータスコードを正しく使って表現すること。

ステータスコード 意味
100番台 情報
200番台 成功
300番台 リダイレクト
400番台 クライアントサイドに起因するエラー
500番台 サーバサイドに起因するエラー

3.6.2 エラーの詳細をクライアントに返す

上記のステータスコードだけではわからないので、レスポンスボディにエラー専用のデータ構造を用意して、エラーの詳細を返す。

{
  "error" : {
    "code": 2013,
    "message": "Error Description",
    "info": "Detail Description or its url"
  }
}

3.6.4 エラーの際にHTMLが返ることを防ぐ

例えば存在しないURL を叩いた場合にWEBサーバ側の設定でHTML が返ってしまうことがないようにしっかり設定する。
つまり、API だけでなく、その周りのミドルウェアの設定も必要。

3.6.5 メンテナンスとステータスコード

メンテナンスでAPI を停止する場合は503 のHTTPステータスコードを返却し、Retry-After のヘッダで再開の時刻を知らせるべき。

じゃあどうやって実装するの?

レスポンスの内容をユーザが選べるようにする

今回の部分であれ?どうやって実装するんだろう?って思ったのは、
とくに「レスポンスの内容をユーザが選べるようにする」部分だ。

実装イメージがわかなかったので調べたみた。
実際に実装するのはJava、Spring Boot。

こういうのはやっぱりStack OverFlow に書いてあるね!
参考にしたのは以下。

json - Spring rest api filter fields in the response - Stack Overflow

Model クラスの作成

ここではlombok を使用している。

@JsonFilter(RestControllerAdvice.PROFILE_FILTER)
@Data
public class Profile {

    private String firstName;
    private String lastName;
    private LocalDate birthDay;
    private String gender;
}

ここで重要なのはJsonFilter アノテーションを付与していること!
これでfilter 対象であることを表している。

Controller クラスの作成

Profile クラスを作成して、値を全部つめて返却する簡単なController クラス。

@RestController
public class ProfileController {

    @RequestMapping(value = "/api/v1/users/me/profile", method = RequestMethod.GET)
    public Profile getV1Profile() {
        Profile profile = new Profile();
        profile.setFirstName("太郎");
        profile.setLastName("山田");
        profile.setBirthDay(LocalDate.of(1986, 1, 1));
        profile.setGender("MALE");

        return profile;
    }
}

Advice クラスの作成

リクエストに詰められたfields は一括してAdviceクラスで処理するようにする。

@ControllerAdvice(annotations = RestController.class)
public class RestControllerFilterAdvice extends AbstractMappingJacksonResponseBodyAdvice {

    public static final String PROFILE_FILTER = "PROFILE_FILTER";

    @Override
    protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType,
            MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {

        // fields パラメータの取得
        String fieldsParamter = ((ServletServerHttpRequest) request).getServletRequest().getParameter("fields");

        String[] fields = fieldsParamter == null ? new String[0] : fieldsParamter.split(",");
        // Filter の設定
        FilterProvider filters = new SimpleFilterProvider().addFilter(PROFILE_FILTER,
                fields.length == 0 ? SimpleBeanPropertyFilter.serializeAllExcept(fields)
                        : SimpleBeanPropertyFilter.filterOutAllExcept(fields));
        bodyContainer.setFilters(filters);
    }
}

まずAbstractMappingJacksonResponseBodyAdvice クラスをextends する。
このクラスを継承することでレスポンスボディの変更が楽に行える。
beforeBodyWriteInternal メソッドだけコーディングすればよくなるのだ!

レスポンスのフィールドを指定するのはFilterProvider クラスを作成して、 MappingJacksonValue のフィルターに指定すればよい。

リクエストにfieldsパラメータがあるかないかでfilter のメソッドを変更している。
これはパラメータがない場合には、全フィールドを返却するためである。

GitHub

今回作ったものはGitHub にあげておくので、参考にしてくださいねー。

github.com

所感

今回学んだ、こうあるべきって内容については普通に身についていた。

一つどうやって実装するんだろうってイメージがつかめない部分があったが、
実際にコーディングしてみて意外と簡単に実装できるものだなぁ、と感じた。

重要なのは概念を理解すること、そして実装イメージをもつこと!