やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

【API設計】jsonを返す時はできる限りフィールドのkeyを統一してあげよう

APIの仕様書が送られてきた

外部連携させて頂く企業様から、新規に追加されるAPIの仕様書を頂きました。ユーザーが登録しているカテゴリの総数と、その内訳を取得することが出来るAPIだそうで、新たに追加されたとのことです。しかしながら、蓋を開けてみると、何とも言えない残念なレスポンスとなっていました。まずは、以下をご確認ください。

(情報は原文より書き換えてあります)

以下はユーザーが何かしらのカテゴリーを1件以上登録している場合のレスポンスになります。

{
  "total_category": 2,
  "category": [
    {
      "id": 1,
      "name": "Python"
    },
    {
      "id": 2,
      "name": "JavaScript"
    }
  ]
}

そして、こちらがユーザーが1件もカテゴリーを登録していない場合のレスポンスです。

{
  "total_category": 0
}

お分かりいただけただろうか

カテゴリーの登録があるかどうかでjsonのレスポンスのフィールドのkeyの構造に違いがあります。1件でもカテゴリーの登録がされていれば、categoryというkeyが存在して、valueにはカテゴリー情報を持つオブジェクトの配列が入っています。しかしながら、1件もカテゴリーの登録がされていなければ、categoryというkeyが存在しないのです。

発生する2つの問題

実際には2つの問題があろうとも、動くコードを記述することは出来ます。ただ、それは対応としては複雑さを生み出し、保守性と品質を低下させます。APIのレスポンスだけを修正すれば済む問題を、呼び出し元でカバーしなければならないのです。

keyが存在しない場合の挙動

例えば、フロントエンドでユーザーが登録しているカテゴリーを一覧表示しようとした場合に以下のようなコードを書きます。

const generateCategoryDOM = () => {
  const resp = getCategory(); // 架空の関数
  const category = resp.category;
  return (
    <div>
      {category.map((detail) => {
        return (
          <p>{`ID: ${detail.id}`}</p>
          <p>{`Name: ${detail.name}`}</p>
        )
      })}
    </div>
  )
}

カテゴリーの登録が1件以上ある場合には正常に動作するでしょうが、カテゴリーが1件も登録されていない場合はどうでしょうか。const category = resp.category;の評価の結果、categoryにはundefinedが束縛されてしまいます。undefinedに対してmapを呼び出すことは出来ないため、このコードはエラーを引き起こします。

const obj = { "a": 1 };
const exist = obj.a;
console.log(exist); // 1

const notExist = obj.b;
console.log(notExist); // undefined

この問題を回避するためにコードに手を加えなければなりません。無駄なコードが増えてしまいました。

const category = resp.category || [];

「なぜcategoryが存在しない場合に空配列を代入してるんだろう?」ということをコードを見る度に考える必要がある可能性があるので、このような変更はできる限りしたくありませんが、仕方ないですね😭

型定義の煩雑化

またtypescriptでレスポンスの型を定義する場合にcategory?としなければなりません。毎回、categoryが存在するかどうかを確認するコードを書かないといけなくなる可能性があります。型定義も煩雑になってしまいました。

interface CategoryResp {
    total_category: number;
    category?: CategoryDetail;
}

interface CategoryDetail {
    id: number;
    name: string;
}

あるべき姿

カテゴリーの登録がない場合にでも、categoryというフィールドを返してくれれば、上記2つの問題は発生しません。値としては空配列が適切ですね。

{
  "total_category": 0,
  "category": []
}

カテゴリーの登録がない場合にcategoryというフィールドが返ってこないというのは、感覚としては戻り値をbooleanと定義している関数から、nilが返ってくるようなものです。実際に呼び出し元では今回、紹介した問題が発生します。他にも問題はあるかもしれません。

APIというのはプロトコル経由で呼び出せる、ただの関数だ」と考えると、戻り値の型を揃えるのはごく自然なことかと思います。プリミティブな型であれば関数の戻り値の型を統一するというのが自然だと思います(言語によっては関数の戻り値の型が不一致でエラーになっているはず👀)。
jsonであっても、可能な限りレスポンスの型を統一してあげるべきです。

APIの設計については、この書籍が大変役立っています。今回紹介したレスポンスに関するテーマは第3章で扱っていますのでご参考までに。