Isomorphic JavaScript でコードを仕様書として利用する


# はじめに

Virtual DOM の普及や webpack の環境整備などに伴い，最近では Isomorphic な JS 実装をよく見かけるようになりました．

Isomorphic JavaScript では，サーバ･クライアント間のロジックを同期できるところに強みがあります．バリデーションロジックなどがいい例ですが，クライアントサイドでの入力チェックと，サーバサイドでのリクエストチェックで同一のコードを参照することができ，ロバストな設計を実現することができます．

今回は，これらの共通化されたコードの一部そのものを，さらに __仕様書__ としても利用するアプローチを考えてみます．これにより，サーバ･クライアント間の同期だけでなく，実装･仕様間の同期もおこなうことができます．

# 実装例

## URL

1 つの例として，複数の URL を 1 ファイルに切り出し，仕様書として利用することを考えてみます．

### 共通化するコード

以下が共通化するコードです．大きくサイトのエンドポイントと API のエンドポイントの 2 つに分け，エンドポイントごとにドキュメントを記述しています．

```js:urls.js
export const userPageUrl = {
  site: {
    /**
     * ユーザ一覧ページ
     */
    users: '/users',
    /**
     * 特定のユーザのページ
     * @param {number} id - ユーザの id
     */
    user: '/user/{id}'
  },
  api: {
    /**
     * 特定のユーザの情報を取得する API
     * @param {number} id - ユーザの id
     */
    user: '/api/user/{id}'
  }
}
```

### サーバサイド

以下が Node サーバ (Koa) の実装例です．以下で利用している [koa-router](https://github.com/alexmingoia/koa-router/tree/master/) では，`/user/:id` のような形で URL を登録することでパラメータを取得できるため，`{id}` の部分に `:id` を指定しています．また，変数展開には [string-format](https://www.npmjs.com/package/string-format) というライブラリを利用していますが，ES6 のテンプレートリテラルなどでも同様のことを実現できます．

```js
import format from 'string-format'
import Router from 'koa-router'
import { userPageUrl } from 'urls'

const router = new Router()

router.get(
  userPageUrl.site.users,
  (ctx, next) => { /* ... */ }
)

router.get(
  format(userPageUrl.site.user, { id: ':id' }),  // '/user/:id'
  (ctx, next) => {
    // this.params.id で id を取得
  }
) 
```

### クライアントサイド

以下がクライアントサイドの実装例です．Virtual DOM のレンダリングも API リクエストも，展開の仕方は Node と同様です．

```js
import format from 'string-format'
import axios from 'axios'
import { userPageUrl } from 'urls'

/**
 * Virtual DOM のレンダリング
 */
export const UserLink = ({ user }) => (
  <a href={format(userPageUrl.site.user, { id: user.id })}>
    {user.name}
  </a>
)

/**
 * API リクエスト
 */
export const fetchUserById = id => {
  return dispatch => {
    axios.get(format(userPageUrl.api.user, { id }))
    .then(response => /* ... */)
    .catch(error => /* ... */)
  }
}
```

このようにすることで，例えば `/api/user/{id}` を `/api/users/{id}` に変更したいということがあったとしても，仕様書となる `urls.js` を 1 行書き換えるだけで，サーバ･クライアントのコードは一切変更することなしに，エンドポイント名の変更を実現できます．

`urls.js` は単なるプロパティの定義にすぎないので，このままでも十分ドキュメントの機能を果たしますが，必要であれば html や markdown の簡単なドキュメントを生成してもいいかもしれません．

## ステータスコード

もう 1 つの例として，ステータスコードの共通化を挙げてみます．URL と同様に 1 つのファイルに共通化しておくことで，サーバ･クライアントそれぞれでステータスコードを直接記述する必要がなくなると同時に，ステータスコードの仕様としての役割も果たします．

やっていることは URL と同様なので，細かい説明は省略しています．

### 共通化するコード

```js:status-code.js
export const errorCode = {
  /**
   * ユーザの認証に失敗した場合
   */
  unauthorized: 401,
  /**
   * ユーザにアクセス権がない場合
   */
  forbidden: 403,
  /**
   * リソースが存在しない場合
   */
  notFound: 404
}
```

### サーバサイド

```js
import format from 'string-format'
import Router from 'koa-router'
import { userPageUrl } from 'urls'
import { errorCode } from 'status-code'

const router = new Router()

router.get(
  format(userPageUrl.api.user, { id: ':id' }),
  async (ctx, next) => {
    await userController.fetchUserById(this.params.id)
    .then(response => this.body = response)
    .catch(error => {
      if (error instanceof UserNotFound) {
        this.status = errorCode.notFound  // 404
      } else {
      	 // ...
      }
    })
  }
) 
```

### クライアントサイド

```
import format from 'string-format'
import axios from 'axios'
import { userPageUrl } from 'urls'
import { errorCode } from 'status-code'

export const fetchUserById = id => {
  return dispatch => {
    axios.get(format(userPageUrl.api.user, { id }))
    .then(response => /* ... */)
    .catch(error => {
      if (error.status = errorCode.notFound /* 404 */) {
        // ...
      } else {
      	// ...
      }
    })
  }
}
```

# おわりに

Isomorphic な環境において，サーバ･クライアント･仕様の 3 者間で足並みをそろえてアップデートできる仕組みを実現しました．

実装と仕様書の乖離をなくしたいといったモチベーション自体は， JSON Schema などにも見受けられますが，今回例に挙げた URL 設計などのように，やり方次第で JSON API フォーマットに限らず様々な JS のコードが仕様書を兼ねられるのではないかと思います．