Make組ブログ

Python、Webサービスや製品開発、ライブラリー開発についてhirokikyが書きます

JavaScript (ES6) でAPIから受け取ったデータをモデルに入れるプラクティス

フロントエンドのJavaScriptで、バックエンドのAPIからの結果をモデルに入れるプラクティスについてまとめます (より良い方法があったら教えてください)。

まず、以下のようなレスポンスがAPIからあると想定します。

{
    "id": 1,
    "username": "hirokiky",
    "last_name": "Kiyohara",
    "first_name": "Hiroki"
}

データをそのまま使いまわすのはイマイチ

このデータを受け取ったままの状態でJavaScript上で使うのはイマイチです。

import axios from 'axios'

async function initialize () {
  let res = await axios.get("/user")
  let user = res.data

  ...  // 以降、userを使いまわす
}

理由としてはいくつかあります。

  • APIが snake_case で返すと、JavaScript内でも user.last_name とかく必要がある
    • user.lastName のように camelCase にしたい
  • APIの実装が変わるとアプリケーションの深い部分で影響を受けることがある
    • データを受け取った段階で検知したい
  • user などのデータに振る舞いを持たせられない
    • user.fullName のような処理を持たせたい

モデルを定義する

モデルを定義しておくと良いでしょう。 一旦、このように定義しました(models.js に書く想定です)。

export class User {
  constructor (id, username, lastName="", firstName="") {
    this.id = id
    this.username = username
    this.lastName = lastName
    this.firstName = firstName
  }

  get fullName () {
    return this.lastName + this.firstName
  }
}

これをAPIからデータを受け取ったときに使います。 ついでに、APIアクセスをまとめたJavaScriptファイルにまとめておいたほうが良いです (api.js に書く想定です。バックエンドのAPIの名前が明確ならその名前でも良いでしょう)。

import axios from 'axios'

import { User } from '@/models'

export async function getAuth () {
  let res = await axios.get("/user")
  return new User(
    id=res.data.id,
    username=res.data.username,
    lastName=res.data.last_name,
    firstName=res.data.first_name
  )
}

元の処理では上記の getAuth を使うと良いです。

import { getAuth } from '@/api'

async function initialize () {
  let user = await getAuth()

  ...  // 以下 user を使いまわす
}

ここで少し面倒な点がいくつかあります

  • new User(...) 時に、APIからのレスポンスを1つ1つ入れるのが面倒
  • constructor の定義が面倒くさい
    • Pythonのdataclassのように書きたい

簡単にモデルを書けるようにする

実験的な実装なので信用しないでください

以下のようにモデルの基底クラスを定義しておきます。

class Model {
  static  fields () { return {} }

  constructor(options={}) {
    var v
    for (let [key, value] of Object.entries(this.constructor.fields())) {
      if (options.hasOwnProperty(key)) {
        v = options[key]
      } else {
        v = value
      }
      this[key] = v
    }
  }
}

各モデルのクラスでは以下のように使います。 fields メソッドには各フィールドの定義を書きます。

export class User {
  static fields () {
    return {
      id: null,
      username: "",
      lastName: "",
      firstName: ""
    }
  }
}

fields からは {フィールド名: デフォルト値} というオブジェクトを返します。 fields がstaticメソッドなのは、ES6にはクラス変数が無いから、仕方なくこう定義しています。

User は以下のように作ります。

new User({id: 1, username: "hirokiky"})

APIのデータからモデルを作る

ここで、 Model にメソッドを追加して、APIから受け取ったデータを使ってインスタンスを簡単に作れるようにしましょう。

function makeSnake (s) {
  return s.replace(/(?:^|\.?)([A-Z])/g, function (x,y){return "_" + y.toLowerCase()}).replace(/^_/, "")
}

function copyFields (fromObj, toObj, fields, fromSnake=false, toSnake=false) {
  for (var field of fields) {
    var value, fromFieldName

    if (fromSnake) {
      fromFieldName = makeSnake(field)
    } else {
      fromFieldName = field
    }

    if (fromObj.hasOwnProperty(fromFieldName)) {
      value = fromObj[fromFieldName]
    } else {
      value = null
    }

    if (toSnake) {
      toObj[makeSnake(field)] = value
    } else {
      toObj[field] = value
    }
  }
  return toObj
}


class Model {
  // ...

  static fromAPIData (options, initial={}) {
    return new this(copyFields(options, initial, Object.keys(this.fields()), true, false))
  }
}

こうしておくと、以下のように snake_case でUserのインスタンスを作成できます。

User.fromAPIData({id: 1, username: "hirokiky", 'last_name': "Kiyohara"})

API呼び出し時にも簡単にインスタンスを作れるようになりました。

import axios from 'axios'

import { User } from '@/models'

export async function getAuth () {
  let res = await axios.get("/user")
  return User.fromAPIData(res.data)
}

まぁ、 getAuth 関数内で snake_case から camelCase に変換して new User(toCamelCase(res.data)) としたほうが良いかもしれませんが。

まとめ

  • フロントエンドのJavaScriptを書くときもモデルを定義したほうが良いよ
  • モデルの定義を毎度書くのは面倒だから、楽できると良いよね

今後、各ModelクラスをJsonSchemaから定義したり、バリデーションを追加できるようにすると良さそうです。 でもそこまでいくとライブラリーがありそうなので探すと良さそうです。良いのがあったら教えてください。

他のオススメ記事

blog.hirokiky.org

blog.hirokiky.org