身の丈にあったWebAPI設計ガイドラインを作った話

こんにちは。フリーランスエンジニアの@dayoshixです。

現在、[リンクアンドモチベーションのモチベーションクラウド](http://www.lmi.ne.jp/)の開発に、主にフロントエンドエンジニアとしてお手伝いさせて頂いております。
そのようなご縁もあり[モチベーションクラウドのアドベントカレンダー](https://qiita.com/advent-calendar/2018/mcs)（３日目）に参加させて頂くことになりましたので宜しくお願いします！！

トップバッターの@ishigeさん、２日目の@HayatoKamonoさんお疲れ様でした！！
お二人の記事はこちら。どちらも力作なので宜しくお願いします。

- [1年半取り組んだWebプロジェクトマネジメントを振り返って、やって良かったこと、やっておけば良かったことをすべて書く](https://nekonohitai.hateblo.jp/entry/2018/12/01/091653)
- [Vueを用いた開発プロジェクト用にカスタムジェネレーターを作ってみる](https://qiita.com/HayatoKamono/items/716c062aba8c64777a05)

ということで始めたいと思います。

# 概要

最近モチベーションクラウドのWebAPI設計ガイドラインが作成されたのですが、それはどのような方針で作成されたのか、その結果どのようなガイドラインが出来上がったのかを紹介します。

# 背景

## これまでの状況

これまで、開発チームではWebAPIの設計に関する明文化されたルールがありませんでした。
WebAPI設計に関して専任の担当者がいるわけではなく、最近では状況にもよりますがフロントエンドエンジニアが設計を行う場面も増えてきました。

また、新たな開発メンバーも徐々に増えていき、迷いが生ずる場面が多くなり小さなストレスになっていました。
その結果かどうかわかりませんが、APIとしての一貫性が徐々に失われているように感じていた人も少なくなかったと思います。

## 改善しよう！！

開発チームとして技術的負債返済のための改善活動の一環としてWebAPI設計ガイドラインを作ろうという話になりました。

結果、私がガイドライン作成の音頭取りとベース作成を担当させて頂くことになり、開発メンバー全員にレビューして貰いブラッシュアップするようなフローで進めることになりました。

# 大切にしたこと

ガイドラインを作成するに当たって下記の点を大切にしました。

## 身の丈に合わせる

ネット上に公開されているガイドラインをそのまま模倣するのではなく、それらを参考にしつつも自分達が開発を通じて実際に迷ったことをもとに、**今の自分達に必要な身の丈にあった最小限のルールだけを定義しよう**と考えました。

また、システムの文脈に依存する一般化できないようなルールがこぼれ落ちないことに注意しました。

## 基本を意識する

基本の理解を疎かにした状態で応用から入らないよう、基本を崩す場合の具体的な判断例を示すことを意識しました。
例えば、最初から特段の根拠もなく基本を崩しパフォーマンス最適化した設計にしないことを促したいと考えました。

## べき論よりやり易さを優先する

セオリーや思想的にこうあるべき、という事が自分達にとってやりにくければ執着せずに無視することを意識しました。


# システム概要

システムの特徴によってルールとして考慮する点が異なってくる事から、ガイドラインを紹介する前にモチベーションクラウドのシステム概要をざっくりと説明します。

- SaaS型のBtoBサービス
- バックエンドに`Ruby on Rails`、フロントエンドに`Vue.js`を使用したいわゆるシングルページアプリケーション
- 2018年11月時点でクライアントのプラットフォームはWebのみ
- WebAPIはシステム内で閉じた使われ方をしており、ユーザーに公開するAPIはない

# WebAPI設計ガイドライン

それでは実際に作られたWebAPI設計ガイドラインを紹介します。
ガイドライン中に時折なぜこのような選択をしたのかをコメントしています。

## 概要

本文書はMCSバックエンド〜フロントエンド間のWebAPI設計のガイドラインです。

> MCSはモチベーションクラウドの社内での総称になります。


## 設計指針



### RESTful like

基本的には[REST](https://ja.wikipedia.org/wiki/Representational_State_Transfer)に従い設計して下さい。
但し、RESTを原則的に従うことでアプリケーションが複雑になる、パフォーマンスに問題が生ずるなどの理由がある場合はRESTの原則に逸れても問題ありません。

`RESTful like`な設計を目指して下さい。

RESTの原則に外れる設計パターンについては[こちら](#REST原則に外れる設計パターン)を参照して下さい。


### 画面に最適化されたWebAPIを作るよりもREST APIを作ることを優先する

画面が必要とする情報を取得するための汎用性の無いWebAPIを設計することよりも、その画面に必要なリソースを取得するためのWebAPIを設計することを優先して下さい。

その画面に必要なリソースの種別が多く、API呼び出しのラウンドトリップ過多でパフォーマンスに影響が出た場合に初めて画面向けのAPIを設計することを検討して下さい。


#### なぜ？

画面向けのWebAPIは画面の表示要素が変更されるたびにバックエンド側にも改修が発生します。 　

RESTベースでWebAPIを作っている場合、変更対象の表示要素の情報を取得するためのエンドポイントが既に作られている可能性がありますし、新規に作った場合でも将来的に他の用途で再利用できる可能性があります。


### 画面の文言をバックエンドで管理しない

特別な理由が無い限り、画面に表示する文言リソースをバックエンドで管理しWebAPI経由でフロントエンドに提供するような設計にせず、フロントエンドで持つようにして下さい。

🙅 Bad

`GET /menu_items`

```json
{
  "meta": null,
  "data": [{
    "id": 1000,
    "title": "属性検索",
    "description": "登録済みの属性の一覧を検索することができます。"
  },{
  "meta": null,
  "data": [{
    "id": 1001,
    "title": "ユーザー検索",
    "description": "登録済みのユーザーの一覧を検索することができます。"
  }]
}
```

🙆 good

`GET /menu_items`

```json
{
  "meta": null,
  "data": [{
    "id": 1000,
    "type": 1
  },{
  "meta": null,
  "data": [{
    "id": 1001,
    "type": 2
  }]
}
```


## 共通ルール




### 命名



#### endpoint（URL）

##### path

snake caseであること

🙅 Bad

`/surveySettings/1`

`/survey-settings/1`

🙆 good

`/survey_settings/1`

> railsとの親和性を考慮しました

##### query paramter

lower camel caseであること

🙅 Bad

`/survey_settings?suvey_id=1000`

🙆 good

`/survey_settings?suveyId=1000`

> クライアントサイドJSで触るデータに関してはクライアントサイドJSの命名規則と同じlower camel caseに統一しました。

#### response body

##### property名

lower camel caseであること

🙅 Bad

```json
{
  "meta": null,
  "data": {
    "id": 1000,
    "first_name": "foo"
  }
}
```

🙆 good

```json
{
  "meta": null,
  "data": {
    "id": 1000,
    "firstName": "foo"
  }
}
```

#### ページング

ページング操作に対応したエンドポイントにおいてはページング情報は下記の命名に従って下さい。

request:

| フィールド名 |                         意味                          |   制約   |                                       備考                                       |
| ------------ | ----------------------------------------------------- | -------- | -------------------------------------------------------------------------------- |
| page         | ページ番号。開始番号は`1`です。                       | 必須指定 |                                                                                  |
| limit        | 1ページあたりの件数。                                 | 任意指定 | １度に全件取得されると問題がある場合は必ずサーバーサイドで制限チェックを行うこと |
| sort         | ソート対象のフィール名です。                          | 任意指定 |                                                                                  |
| direction    | ソート順です。`asc`の場合は昇順、`desc`の場合は降順。 | 任意指定 |                                                                                  |

response:

| フィールド名 |          意味          |   制約   |                         備考                         |
| ------------ | ---------------------- | -------- | ---------------------------------------------------- |
| total        | 検索結果の総件数です。 | 必須指定 | リソースとしてではなくmeta情報として設定して下さい。 |

> 既存に暗黙の命名ルールがありましたがたまに外れた命名もあったため明文化しました。

### HTTP method

`POST`、`GET`、`PUT`、`DELETE`のみ使って下さい。

各methodは[`CRUD`](https://ja.wikipedia.org/wiki/CRUD)のそれぞれに対応します。



### responseのsatus code


#### 正常系

一律 `200` を設定して下さい。


#### 異常系

[エラーの表現](#エラーの表現)を参照のこと。


### response bodyのフォーマット



#### 基本

リソースとリソース以外の情報（リストデータのページング情報、セキュリティトークン等）を分けるために、リソースを`data`プロパティに、リソース以外の情報を`meta`プロパティに分ける下記を基本のフォーマットとする。

```json
{
  "meta": { // リソース以外の情報をここに定義
    "totalCount": 5000,
    "securityToken" "trHqrxxwK8mJhY"
  }, 
  "data": [
    // リソースはここに定義
    {
      "id": 100,
      "name": "foo"
    }
  ]
}
```

> このルールは意見の別れるところかと思います。
> response bodyはあくまでも純粋なリソースとし、meta情報はHTTP Responseの拡張ヘッダーに設定すべきという考え方もあります。
> RESTを前提に扱ったツールと相性が悪いなどデメリットもありますが、情報がJSONに集約されていることによる開発・デバッグ時の分かりやすさを優先しました。

#### HTTP method: GET

フォーマットはendpointごとの任意の形式にして下さい。


#### HTTP method: POST

フォーマットはリクエストされたデータにID、生成日が設定されているデータにして下さい。

例）ユーザー情報の新規登録が正常に行われた場合のresponse

- 新規登録のrequest情報:
  - endpoint:
    - `/users`
  - requst body:
    ```json
    {
      "name": "foo",
      "gender": 1
    }
    ```

🙅 Bad

`status code: 200`のみを返却し、response bodyを返却しない。

🙆 good

リソースのIDと生成日が含まれる下記のresponse bodyを返却する。

```json
{
  "id": 1001,
  "name": "foo",
  "gender": 1,
  "createdAt": "2018-11-12T06:41:58.898+0900"
}
```

#### HTTP method: PUT

フォーマットはリクエストされたデータに更新日が更新されているデータにして下さい。

例）ユーザー情報の更新が正常に行われた場合のresponse

- 更新のrequest情報:
  - endpoint:
    - `/users/1001`
  - requst body:
    ```json
    {
      "id": 1001,
      "name": "bar",
      "gender": 0
    }
    ```

🙅 Bad

`status code: 200`のみを返却し、response bodyを返却しない。

🙆 good

リソースのIDと更新日が含まれる下記のresponse bodyを返却する。

```json
{
  "id": 1001,
  "name": "bar",
  "gender": 0,
  "updatedAt": "2018-11-12T06:44:34.894+0900"
}
```

#### HTTP method: DELETE

bodyを設定せず、status codeのみ返却して下さい。


#### 非同期操作（※）

フォーマットはendpointごとの任意の形式にして下さい。

※ 非同期操作とは、`バッチ処理を起動する`などの最終的な結果が即時に出ない操作を意味します。



### エラーの表現

#### 種類

エラーはシステム仕様上起こり得る業務エラーと、システム仕様上想定していないシステムエラーでエラーを区別し、適したエラーを定義して下さい。


##### 業務エラー・システムエラーの具体例

業務エラーの例

- ユーザーの入力内容に誤りがあった。
  - 所謂バリデーションエラー
- 他のユーザーが削除したデータを参照した。
- 閲覧権限の無いリソースへのアクスセスを検出した。

システムエラーの例

- バグが原因でサーバーサイドで例外が発生した。
- 外部連携システムがダウンした。


#### responseのstatus code

##### 業務エラー

エラーの種別により下記のstatus codeを設定して下さい。

| status code |               意味               |                                               発生ケース                                               |
| ----------- | -------------------------------- | ------------------------------------------------------------------------------------------------------ |
| 400         | リクエストの内容に誤りがある     | 入力内容のバリデーションに引っかかった。<br>パラメータ改竄されている、等                               |
| 401         | 認証がなされていない             | 認証が必要なendpointに認証情報を付加せずにアクセスした。                                               |
| 403         | リソースに対してアクセス権がない | 閲覧権限のない情報にアクセスした、等 |

> 上記は一例になります

##### システムエラー

エラーの種別により下記のstatus codeを設定して下さい。

| status code |               意味               |                                               発生ケース                                               |
| ----------- | -------------------------------- | ------------------------------------------------------------------------------------------------------ |
| 500         | システム内で想定外のエラーが発生した     | システムがダウンしている、<br>サーバーサイドアプリケーションにバグがある、等                               |

##### status codeの追加について

扱うエラーに対して本ガイドラインで定めているstatus codeに適したものが無い場合、適したstatus codeをガイドラインに追加して下さい。
また、扱わないエラーのstatus codeをあらかじめガイドラインに追加しないようにして下さい。

参考：[HTTPステータスコード](https://ja.wikipedia.org/wiki/HTTP%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89)

#### response bodyのフォーマット

エラーを表現する場合、基本的にはreponseのstatus codeで表しますが、クライアント側でより詳細なレベルのエラー情報が必要な場合にのみ、reponse bodyで詳細なエラー情報を表現して下さい。

##### 基本

エラーの詳細情報を表す場合、下記の基本フォーマットに従って下さい。  

```json
{
  "meta": null,
  "data": [
    {
      "code": 400000,      // ※ 詳細エラーコード。必須。
      "message": "error!!" // 任意
    },
    // ...
  ]
}
```

※ 詳細エラーコードの採番については[こちら](#詳細エラーコードの採番)で説明します。

基本フォーマットで表現できない詳細情報を付加したい場合は基本フォーマットを変更しないことを前提に拡張して構いません。


```json
{
  "meta": null,
  "data": [
    {
      "code": 400002,
      "message": null,
      "rowNo": 1,       // 拡張部分
      "columnNo": 10    // 拡張部分
    },
    {
      "code": 400003,
      "message": null,
      "rowNo" 1,
      "columnNo": 15
    }
  ]
}
```


##### 詳細エラーコードの採番規則

詳細エラーコードは数値型で、HTTPレスポンスのstatus codeと連番を組み合わせて採番して下さい。
フォーマットは６桁の数値で先頭３桁はstatus code、後続の３桁はエラー種別を表す`0`から始まる連番になります。

例）status codeが`400`の`Bad request`で、エラー種別が `3` の場合

```json
{
  "meta": null,
  "data": [
    {
      "code": 400003
    }
  ]
}
```


###### 詳細エラーコード

詳細エラーコードは下記を使用して下さい。

| エラーコード | status code |                        意味                         | 備考 |
| ------------ | ----------- | --------------------------------------------------- | ---- |
| 400000       | 400         | 既に指定されたemailアドレスがデータベースに存在する |      |

適したエラーコードが無い場合、上記のテーブルに追加して下さい。  

##### Formのバリデーションエラー

Formのバリデーションエラーにおいて、エラー発生元フィールドとエラー情報を関連付ける必要がある場合は下記のルールに従って下さい。

- エラーレスポンスの詳細エラー情報ごとに`fieldName`という名前のフィールドを追加し、リクエストに含まれているバリデーション対象のフィールド名を設定する

例）メールアドレスに重複エラーとクレジットカード番号に与信エラーのバリデーションエラーが発生した場合

- メールアドレスのフィールド名は`email`、クレジットカードの番号のフィールド名は`cardNo`とします。
- メールアドレスの重複エラーの詳細エラーコードは`400001`、クレジットカードの番号の与信エラーの詳細エラーコードを`400002`とします。

request:

```json
{
  "meta": null,
  "data": {
    "email": "osumi_kumamon@ggmail.com",
    "cardNo": "378282246310005"
  }
}
```

response:

（status codeは`400`:bad request）

```json
{
  "meta": null,
  "data": [
    {
      "code": 400001,
      "fieldName": "email"
    },
    {
      "code": 400002,
      "fieldName": "cardNo"
    }
  ]
}

```




### データが無い場合の表現

#### リソース自体が無い

リソースのデータ形式が配列の場合は空配列（`[]`）を設定して下さい。

```json
{
  "meta": null,
  "data": []
}
```

リソースのデータ形式がObjectの場合は`null`を設定して下さい。

```json
{
  "meta": null,
  "data": null
}
```

#### リソース内の一部のpropertyが無い

[リソース自体が無い](#リソース自体が無い)場合のルールと同じです。

リソースのデータ形式が配列の場合は空配列（`[]`）を設定して下さい。

```json
{
  "meta": null,
  "data": {
    "id": 1,
    "children": []
  }
}
```

リソースのデータ形式がObjectの場合は`null`を設定して下さい。

```json
{
  "meta": null,
  "data": {
    "id": 1,
    "firstName": "Thet Win Aung",
    "lastName": null
  }
}
```


### データのフォーマット

#### データ型

基本的にデータに適したデータ型で設定して下さい。

🙅 Bad

```json
{
  "meta": null,
  "data": {
    "id": 1,
    "score": "60.5" ‼️ 
  }
}
```

🙆 Good

```json
{
  "meta": null,
  "data": {
    "id": 1,
    "score": 60.5 👍 
  }
}
```

#### 日時型

[ISO8601拡張形式](https://ja.wikipedia.org/wiki/ISO_8601)のタイムゾーン`JST`で表現して下さい。

🙅 Bad

```json
{
  "meta": null,
  "data": {
    "id": 1,
    "createdAt": "2018/10/29 05:38:24"
  }
}
```

🙆 Good

```json
{
  "meta": null,
  "data": {
    "id": 1,
    "createdAt": "2018-10-29T05:38:24.486+09:00"
  }
}
```


#### 区分値

区分を表すコード値は数値で指定して下さい。

区分値の例

🙅 Bad

```json
{
  "meta": null,
  "data": {
    "id": 1,
    "status": "in_progress"
  }
}
```

🙆 Good

```json
{
  "meta": null,
  "data": {
    "id": 1,
    "status": 1
  }
}
```

#### 真偽値

真偽を表すフラグ値は`Boolean`で指定して下さい。

フラグ値の例

🙅 Bad

```json
{
  "meta": null,
  "data": {
    "id": 1,
    "published": 1
  }
}
```

🙆 Good

```json
{
  "meta": null,
  "data": {
    "id": 1,
    "published": true
  }
}
```


### サニタイズ

responce bodyをXSS対策のためにサニタイズせず、rawデータで指定して下さい。

🙅 Bad

```json
{
  "meta": null,
  "data": {
    "id": 1,
    "name": "&lt;script&gt;alert(&quot;foo&quot;)&lt;/script&gt;"
  }
}
```

🙆 Good

```json
{
  "meta": null,
  "data": {
    "id": 1,
    "published": "<script>alert(\"foo\")</script>"
  }
}
```

> サニタイズはクライアントサイドのテンプレートシステムで行なっているためです。


### query parameter


#### 配列

配列データは`<フィールド名>[]`で表現して下さい。

🙅 Bad

`GET /suveys?id=1,2,3,4,5`

🙆 Good

`GET /suveys?id[]=1&id[]=2&id[]=3&id[]=4&id[]=5`


> railsとの親和性を考慮しました。

### 構造を持つデータ表現

複数の異なるリソースを含むリソースはリソース単位でネスト構造を持たせて表現して下さい。

🙅 bad

```json
{
  "meta": null,
  "data": {
    "id": 1,
    "name": "foo"
    "barId": 1,
    "barName": "foo"
  }
}
```

🙆 good

```json
{
  "meta": null,
  "data": {
    "id": 1,
    "name": "foo"
    "bar": {
      "id": 1,
      "name": "foo"
    }
  }
}
```



### エンドポイントのネスト

エンドポイントのネストは基本的にせず、`path parameter`より`query parameter`で表現することを優先して下さい。

🙅 bad

`/companies/:company_id/survys/:survey_id/foos/:foo_id`

🙆 good

`/foos/:foo_id?companyId=:company_id&surveyId=:survey_id`



### REST原則に外れる設計パターン

REST原則に従うことによってアプリケーションとして問題が生ずる場合があります。

下記に示すパターンに沿って設計を行って下さい。


#### WebAPIコールのN+1問題

##### 問題

複数のリソースで構成されたリソースの一覧を取得する場合、単純にRESTらしい設計を行うと`N+1問題`が発生します。

下記に例を示します。

###### 前提

- 画面表示対象の回答者は100人いる
- 回答者として表示する情報は回答数、ユーザー名、性別、生年月日
- 用意されているリソースは下記の２つ
  - 回答者リソース
      - Endpoint: `/answers`
      - 回答数とユーザーIDを保持する
  - ユーザーリソース
      - Endpoint: `/users`
      - ユーザー名、性別、生年月日を保持する

###### 制御フロー

1. 回答者リソースを全件（100件）取得する
  - `/answers`のAPIコール数：1回
2. 回答者に紐づくユーザーリソースを取得する
  - `/users/:id`のAPIコール数：100回

😱回答者の数だけAPIコールが発生しクライアント側のパフォーマンスやサーバー負荷の問題が発生しうる!!

##### 設計パターン

本問題に対する設計パターンは２つあり、それぞれの状況によって使い分けて下さい。

###### パターンA: リソースの検索条件として複数のIDを指定できるようにする

対象のリソースのEndpointのフィルタ条件として複数のIDを指定できるように設計します。
これにより複数のリソースで構成された情報を取得する際のAPIコール回数がリソースの種別数分に抑えることができます。

先の例ではユーザーリソースのフィルタ条件に配列型のIDを定義できるようにします。

1. 回答者リソースを全件（100件）取得する
    - `/answers`のAPIコール数：1回
2. 回答者に紐づくユーザーリソースを取得する
    - `/users?id[]=1&id[]=2&id[]=3....`のAPIコール数：1回

😀合計２回のAPIコールで済みました。

基本的にはパターンBよりパターンAを優先します。

###### パターンB: リソースに異なるリソースを含める

REST原則に違反する形にはなりますが、リソースに異なるリソースを含めます。
これにより複数のリソースで構成された情報を１度のAPIコールで取得することができます。

先の例では回答者のリソースにユーザーのリソースを含めるように拡張します。

```json
{
  "meta": null
  "data": [
    {
      "id": 1,
      "answerCount": 50,
      "user": {
        "id": 1,
        "name": "foo",
        "gender": 1,
        "birthDate": "1994-07-30T00:00:00.000+0900"
      }
    },
    {
      "id": 2,
      "answerCount": 45,
      "user": {
        "id": 2,
        "name": "bar",
        "gender": 0,
        "birthDate": "1974-10-02T00:00:00.000+0900"
      }
    },
    ...
  ]
}
```

パターンAとの使い分けの観点としては、**このリソースが拡張した異なるリソースとともに複数個所でよく使われるのかどうか**になります。

よく使われる場合はパターンAよりパターンBを採用した方が良いでしょう。
そうでなければ、拡張したリソースは多くのケースで余計な情報になるためパターンAを採用すべきでしょう。

また、本パターンを採用する場合、拡張するリソースが3種類以上、及び3階層以上にならないようにして下さい。


🙅 Bad

```json
{
  "meta": null
  "data": [
    {
      "id": 1,
      "answerCount": 50,
      "user": {
        "id": 1,
        "name": "foo",
        "gender": 1,
        "birthDate": "1994-07-30T00:00:00.000+0900",
        "company": {  // 🙆 2階層目のリソース
          "id": 100,
          "name": "bar",
          "address": { // 🙅 3階層目のリソース
            "id": 30,
            "location": "東京都中央区銀座6丁目10-1"
          }
        }
      },
      "answerdQuestion": [{ // 🙅 3種類以上のリソース
        "no": 1,
        "title": "第一問 あなたの性別は？",
        "answer": 1
      }]
    },
    ...
  ]
}
```

🙆 good

パターンAを使用し複数のリソースを複数回のAPIコールで取得する、もしくは画面に最適化した専用のリソースとして新たに定義しましょう。

# 最後に

紹介したガイドラインを適用した本格運用をまだ始めたばかりで、今後足り無い部分や余計な部分など色々な問題が出て来ると思います。
しかしこの点に関しては心配はしていません。

ガイドラインはシステムの改変、チームの成長、文化の変化などにより成長していく生き物のようなものです。
チームでこのガイドラインを放置せず大切に育て行くことにこそ意義があります。  

ということでレビューをしてくれた全ての開発メンバーの皆様ありがとうございました！！
一緒にガイドラインを大切に育てて行きましょう！！😀



# 最後の最後に

組織改善にご興味のある方は[全ての組織がこれで変わるモチベーションクラウド](https://www.motivation-cloud.com/)を是非ご検討して頂ければと思います！！