GoogleのWebAPI設計とWebAPI設計のベストプラクティスを比較してみる

追記
GoogleがAPIのデザインガイドを作ったのでリンクを記載しておきます。
https://cloud.google.com/apis/design/

---

[翻訳: WebAPI 設計のベストプラクティス](http://qiita.com/mserizawa/items/b833e407d89abd21ee72)というWebAPIに関する記事の内容と筆者が普段使っているGoogleのWebAPIはどうなっているかを比較してみました。
GoogleのWebAPIと言ってもたくさんあるため本記事ではCalendarAPIの予定情報をベースに比較していきます。
こんな感じなのかという雰囲気だけでも掴んでもらえれば幸いです。


# TL;DR
WebAPI設計のベストプラクティスを満たす項目には○を記載

|項目|GoogleWebAPI|補足|
|------|:---:|---|
|RESTful な URL にしよう|○||
|いつ何時も、SSL 通信を使おう|○||
|かっこいい仕様書を作ろう|○||
|バージョンは URL に含めよう|○||
|フィルタ・ソート・検索はリクエストパラメータでやろう|○||
|レスポンスのフィールドを絞れるようにしよう|○||
|作成・更新の後は変更後の情報をフルで返そう|○||
|HATEOAS を採用するのは待とう|○||
|可能な限り JSON で返そう|○||
|フィールドの命名規則を考えよう|△|キャメルケース|
|JSON はデフォルトで整形しよう|○||
|要素はラップせずに返そう|○||
|追加・更新時のリクエストボディには JSON を使おう|○||
|ページング情報はレスポンスヘッダに入れよう|×|レスポンスボディ内のpageTokenを使用|
|関連データを埋め込む手段を作ろう|×|Google内のデータの持ち方的に難しいと思われる|
|HTTP メソッドを上書きしよう|○||
|リクエスト制限情報はレスポンスヘッダに入れよう|△|Google Developer Consoleを使用|
|トークン認証を使おう|○||
|キャッシュの情報をレスポンスヘッダに入れよう|○||
|ちゃんとしたエラーメッセージを返そう|○||
|HTTP ステータスコードを有効活用しよう|○||


# RESTful な URL にしよう

**元記事**
>GET /tickets - チケットのリストを取得する
GET /tickets/12 - 指定したチケットの情報を取得する
POST /tickets - 新しいチケットを作成する
PUT /tickets/12 - チケット #12 を更新する
PATCH /tickets/12 - チケット #12 を部分的に更新する
DELETE /tickets/12 - チケット #12 を削除する


**Google**
GET /events - 予定のリストを取得する
GET /events/12 - 指定した予定の情報を取得する
POST /events - 新しい予定を作成する
PUT /events/12 - 予定 #12 を更新する
PATCH /events/12 - 予定 #12 を部分的に更新する
DELETE /events/12 - 予定 #12 を削除する

完全に同じですね。



# いつ何時も、SSL 通信を使おう
SSL必須です。
以下は予定取得をhttpsではなくhttpで実行してみた例です。


```javascript:httpで実行した時のレスポンス
{
    "error": {
        "errors": [
            {
                "domain": "global",
                "reason": "sslRequired",
                "message": "SSL is required to perform this operation."
            }
        ],
        "code": 403,
        "message": "SSL is required to perform this operation."
    }
}
```


# かっこいい仕様書を作ろう
イケてる仕様書だと思います。

[Googleの予定取得の仕様書](https://developers.google.com/google-apps/calendar/v3/reference/events/get)を見てみましょう。
リクエスト情報やサンプルコード、APIを実行するデモ環境まで用意されています。

またGoogleではDiscoveryAPIというAPIの設計書を取得するAPIまで用意しています。
以下のURLにアクセスしてExecuteを押すとCalendarAPIの仕様を見ることができます。
https://developers.google.com/apis-explorer/#p/discovery/v1/discovery.apis.getRest?api=calendar&version=v3&_h=1&



# バージョンは URL に含めよう
含めています。
予定の取得は以下のURLになります。

```url
GET https://www.googleapis.com/calendar/v3/calendars/{calendarId}/events/{eventId}
```

v3とバージョンが含まれています。



# フィルタ・ソート・検索はリクエストパラメータでやろう
リクエストパラメータで行っています。

[予定の一覧取得](https://developers.google.com/google-apps/calendar/v3/reference/events/list)を見てみましょう。
以下の様なリクエストパラメータが定義されています。
* フィルタ：showDeleted, showHiddenInvitations, timeMaxなど
* ソート：orderBy
* 検索：q




# レスポンスのフィールドを絞れるようにしよう
絞ることが可能です。
fieldsパラメータを使用します。
``?fields=items(id,start,end,summary)``のように指定します。

fieldsパラメータを詳しく知りたい方はこちらの[記事](http://qiita.com/howdy39/items/2eca13609901a22fcbb8#fields%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%82%92%E6%8C%87%E5%AE%9A)をどうぞ


# 作成・更新の後は変更後の情報をフルで返そう
フルで返しています。
以下に予定作成時の例を載せときます。

```js:request：予定の作成のRequest
POST https://www.googleapis.com/calendar/v3/calendars/admin@howdylikes.jp/events

{
 "end": {
  "date": "2016-07-15"
 },
 "start": {
  "date": "2016-07-15"
 },
 "summary": "テスト予定です"
}
```

```json:response
{
    "kind": "calendar#event",
    "etag": "\"2937059431494000\"",
    "id": "lqpeq5ih89phe1gmr8l55s2qfk",
    "status": "confirmed",
    "htmlLink": "https://www.google.com/calendar/event?eid=bHFwZXE1aWg4OXBoZTFnbXI4bDU1czJxZmsgYWRtaW5AaG93ZHlsaWtlcy5qcA",
    "created": "2016-07-14T20:55:15.000Z",
    "updated": "2016-07-14T20:55:15.781Z",
    "summary": "テスト予定です",
    "creator": {
        "email": "admin@howdylikes.jp",
        "displayName": "Tatsuya Nakano",
        "self": true
    },
    "organizer": {
        "email": "admin@howdylikes.jp",
        "displayName": "Tatsuya Nakano",
        "self": true
    },
    "start": {
        "date": "2016-07-15"
    },
    "end": {
        "date": "2016-07-15"
    },
    "iCalUID": "lqpeq5ih89phe1gmr8l55s2qfk@google.com",
    "sequence": 0,
    "hangoutLink": "https://plus.google.com/hangouts/_/howdylikes.jp/admin?hceid=YWRtaW5AaG93ZHlsaWtlcy5qcA.lqpeq5ih89phe1gmr8l55s2qfk",
    "reminders": {
        "useDefault": false,
        "overrides": [
            {
                "method": "popup",
                "minutes": 10
            }
        ]
    }
}
```


# HATEOAS を採用するのは待とう
未採用です。
CalendarAPIに限らずHATEOASは筆者の観測範囲では見ていません。


# 可能な限り JSON で返そう
CalendarAPIなど最近のAPIはJSONを標準で返します。

デフォルトでXMLを返す古いAPIもありますが、GoogleAPIは`alt`リクエストパラメータでレスポンスを切り替えることができます。
`?alt=json`を指定してあげることでレスポンスがJSONになります。


※逆にCalendarAPIのような新しいAPIの場合は`?alt=xml`は指定できません。
GoogleAPIはXMLからJSONへのシフトが完了しつつあります。


# フィールドの命名規則を考えよう
GoogleAPIはキャメルケースが基本です。

~~**ここはベストプラクティスとずれましたね。**~~
ベストプラクティスの筆者の考えは、JSONなので原則キャメルケースであるべきだけど、世の中のAPIとしてはスネークケースも多いよとのこと。


# JSON はデフォルトで整形しよう
整形されています。

また、GoogleAPIは`prettyPrint`リクエストパラメータで整形の有無を切り替えることができます。
`?prettyPrint=true`がデフォルトで、切りたい場合は`?prettyPrint=false`を指定します。


# 要素はラップせずに返そう
通常はラップしません。エラー時はラップします。

>JSONP のリクエストではヘッダを参照する術がありません。しかしながら、世の中のスタンダードは CORS になりつつありますし、RFC 5988 では Link というレスポンスヘッダの仕様が提案されています。つまり、要素をラップする必要はなくなりつつあるわけです。
将来的には、デフォルトの状態ではラップをしていなく、特殊なケースに対応するときのみラップするようになるでしょう。

GoogleAPIは`CORS`に対応しています。
そのためJSONPにするケースは思いつかない（なにかあるのかな？）のですが`callback`リクエストパラメータでJSONPの対応が可能です。

以下は`?callback=hogehoge`を指定した時のエラーレスポンスです。
`error`でラップされています。

```javascript:エラー時のJSONP
// API callback
hogehoge({
 "error": {
  "errors": [
   {
    "domain": "global",
    "reason": "authError",
    "message": "Invalid Credentials",
    "locationType": "header",
    "location": "Authorization"
   }
  ],
  "code": 401,
  "message": "Invalid Credentials"
 }
}
);
```

>どんな状態であれステータスコード 200 を返し、本来のステータスコードはラップした内側に埋め込みます。さらに、従来レスポンスヘッダに入れていたデータも内包させるのです。

GoogleAPIの場合、ステータスコードはエラー時でも200は返しておらず本来のステータスコードと同じステータスコードをレスポンスヘッダに入れているようです。
なので上記の場合、401をレスポンスヘッダに設定されています。

**ベストプラクティスとちょっとずれましたね。**


# 追加・更新時のリクエストボディには JSON を使おう
以下のようにJSONを使っています。

```js:予定の作成
POST https://www.googleapis.com/calendar/v3/calendars/admin@howdylikes.jp/events

{
 "end": {
  "date": "2016-07-15"
 },
 "start": {
  "date": "2016-07-15"
 },
 "summary": "テスト予定です"
}
```


# ページング情報はレスポンスヘッダに入れよう
ページングはレスポンスヘッダには入れてきません。

レスポンス内のnextPageTokenという値を使用します。

```json:一覧取得時に次のページが有る場合のレスポンス
{
    "kind": "calendar#events",
    "nextPageToken": "CigKGnZmMzdnazR1OW1lbjVta2I0dGxnMDExcHYwGAEggIDAv7Lf15wUGg0IABIAGND_ocrr880C"
    // 省略
}
```

次頁が欲しい場合、`?pageToken={nextPageToken}`といった形でリクエストパラメータで取得します。


# 関連データを埋め込む手段を作ろう
CalendarAPIの予定情報の場合、関連データは全て入った状態になっています。

その他のAPIでも関連データが有る場合でも埋め込む手段はありません。
以下のように別でAPIを発行する必要があります。

例)
Googleドライブで複数ファイルのコメントが欲しい時

1. [ファイルの一覧を取得するAPIを実行](https://developers.google.com/drive/v3/reference/files/list) -> ファイルIDが取れる
1. [ファイルIDからコメント一覧を取得するAPIを実行](https://developers.google.com/drive/v3/reference/comments/list) -> コメントが取れる


# HTTP メソッドを上書きしよう
可能です。

POSTで送って`X-HTTP-Method-Override`ヘッダに`PUT`や`DELETE`などを指定します。


# リクエスト制限情報はレスポンスヘッダに入れよう
これは行っていません。

制限情報やリクエスト数は[Google Developer Console](https://console.developers.google.com)を見る必要があります。

## 使用量
![image](https://qiita-image-store.s3.amazonaws.com/0/35595/7efd77d3-af10-8166-8778-da9678657909.png)

## 割り当て
![image](https://qiita-image-store.s3.amazonaws.com/0/35595/8ec5aa0c-8d53-66d8-f3db-36003c625c3e.png)


# トークン認証を使おう
トークン認証で行います。
※昔はアカパス認証があったのですが今はないはず。

`Authorization`ヘッダに`Bearer {Access Token}`という形で指定します。
また`access_token`や`oauth_token`をリクエストパラメータにすることも可能です。
`?access_token={Access Token}`
`?oauth_token={Access Token}`



# キャッシュの情報をレスポンスヘッダに入れよう
>これについて、ETag を使うやり方と Last-Modified を使うやり方があります。

Etagを使用しています。
※ダブルクォートが含まれるのに注意

```json:response例
{
    "kind": "calendar#event",
    "etag": "\"2937059431494000\"",
    "id": "lqpeq5ih89phe1gmr8l55s2qfk",
    "status": "confirmed",
// 省略
}
```

また元記事（翻訳前の記事）ではキャッシュしか触れられていませんが、ここに触れるなら排他制御も含めたほうがいいと思われるので記載しておきます。

## キャッシュ
**取得時**にEtagを`If-None-Match`で指定することで **変更されてない**場合に`304 Not Modified`を受け取ることができます。

## 排他制御
**更新・削除時**にEtagを`If-Match`で指定することで **変更されていた**場合に`412 Precondition Failed`を受け取ることができます。



# ちゃんとしたエラーメッセージを返そう
これはちゃんと返していると思います。

```json:エラー時のレスポンス
{
    "error": {
        "errors": [
            {
                "domain": "global",
                "reason": "conditionNotMet",
                "message": "Precondition Failed",
                "locationType": "header",
                "location": "If-Match"
            }
        ],
        "code": 412,
        "message": "Precondition Failed"
    }
}
```

まれに`500 Internal Server Error`を返します（笑

## 余談
ごくまれ（Google側の障害とか）に **ロボットの画面を表示するhtml**を返します。
Twitterでいうクジラ、Githubでいうユニコーンですかね。

その場合パースするライブラリが`Json Parse Error`みたいなのを吐くので注意。

ちなみにこんなやつです。
![image](https://qiita-image-store.s3.amazonaws.com/0/35595/435d4edb-edc3-26e9-dc29-67b1adfd49db.png)


# HTTP ステータスコードを有効活用しよう
そこそこ活用していると思います。

見たことあるやつを緑色で塗っときます。
201は使っておらず200で代用していますね。

```diff:見たことあるやつを緑色で塗る
+ 200 OK - GET, PUT, PATCH, DELETE リクエストが成功した場合に応答。もしくは、POST リクエストが結果的に何もリソースを作らなかった場合に応答。
201 Created - POST リクエストがリソース作成に成功した場合に応答。なお、そのリソースへのリンクを Location ヘッダに含める必要がある。
+ 204 No Content - 成功したDELETE リクエストで、ボディを返したくない場合に応答
+ 304 Not Modified - HTTP キャッシュが有効な場合に応答
+ 400 Bad Request - パース不可能なリクエストボディが来た場合に応答
+ 401 Unauthorized - 認証がされていない、もしくは不正なトークンの場合に応答
+ 403 Forbidden - 認証はされているが、認可されていないリソースへのリクエストに応答
+ 404 Not Found - 存在しないリソースへのリクエストに応答
405 Method Not Allowed - 認可されていないメソッドでのリクエストに応答
410 Gone - 今は存在しないリソース（廃止されたAPIなど）で空要素を返す場合などに応答
415 Unsupported Media Type - 対応していない MediaType が指定された場合に応答
422 Unprocessable Entity - バリデーションエラーに対して応答
+ 429 Too Many Requests - 回数制限をオーバーしたリクエストに対して応答
```


# あとがき
大体満たしていると言っていいのではないでしょうか。
TwitterやFacebookなどの有名どころはどうなっているんでしょうね。使ったことある人に書いて欲しいなぁ。
わたし、気になります。