マインスイーパーの RESTful な API サーバーをつくる

以前、[Vue.js を使ってマインスイーパーを実装しました](https://qiita.com/kosuke-sugimori/items/894277a9d50fe7bd7661)という記事を書きました。そちらでは、ビジネスロジックは Vue に依存しないように作っていたので、ふと思い立って今回はそれをサーバーサイドに移植してみました。

本記事では、実装の細かいところは書ききれないので設計のポイントになる部分を説明します。

### 作ったもの

API サーバ

https://github.com/ksugimori/minesweeper-server

API サーバに対応したクライアントプログラム

https://github.com/ksugimori/minesweeper-client-vue

### 環境

```
node 14.16.0
express 4.16.4
MySQL 8.0
```

## 1. 基本方針

[Vue.js で作成したときの Model](https://qiita.com/kosuke-sugimori/items/894277a9d50fe7bd7661#%E3%83%AC%E3%82%A4%E3%83%A4%E3%83%BC%E6%A7%8B%E9%80%A0) はそのまま再利用します。前回の記事で書きましたが、ビジネスロジックは Vue.js に依存しない POJO なクラス群で作成しているため、ほとんどそのまま移植しています。

全体の構成は、大雑把に次のような構成。

![ms-server_arch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/771456/096e9d61-7b93-9536-9cfb-efa93365e536.png)


||説明|
|:--|:--|
|Controller|HTTPリクエストのハンドリング。HTTPに関する知識はここだけが持つ。|
|Repository|DBとのやりとり。テーブルレイアウトに関する知識はここだけが持つ。|
|Model|Vue で作ったマインスイーパーから移植した部分。ビジネスロジック。<br>Controller や Repository には依存しない。|

## 2. データ設計

基本となるデータ構造は Cell, Game の２つ。
データベース上も２つのテーブルを作成。

### Cell

一つのマスを表すデータ。
セルの位置座標 (x, y) と、画面表示に必要な情報を保持。

```js:Cell
{
  "x": 0,
  "y": 0,
  "count": 2,      // 周囲のセルの地雷数
  "isMine": false, // 地雷があるか？
  "isOpen": false, // 開かれているか？
  "isFlag": false  // フラグが立てられているか？
}
```

### Game

ゲーム全体を表すデータ。
盤面の広さや地雷数などの設定値、ゲームのステータスや開始・終了時刻などを持ちます。

```js:Game
{
  "id": 999,      // ID
  "width": 9,     // 盤面の横幅
  "height": 9,    // 盤面の高さ
  "numMines": 10, // 地雷数
  "status": "PLAY", // ステータス
  "startTime": "2021-06-27T08:22:34.470Z", // 開始日時
  "endTime": null, // 終了日時
  "cells": [ /* 盤面上すべてのセルが入った配列 */ ]
}
```


## 3. URL設計

### ボツ案

まずは、はじめに考えたもの。
データ構造をそのままURLにした。

|HTTP メソッド|パス|説明|
|:---|:---|:---|
|POST|/api/games|Game の作成|
|GET|/api/games/{id}|Game の取得|
|GET|/api/games/{gameId}/cells|Cell 一覧の取得|
|PATCH|/api/games/{gameId}/cells/{id}|Cell の更新|

#### イマイチなところ

- なんか REST っぽくない
- セルを開く、フラグを立てる、フラグを外す、という操作がすべて Cell に対する PATCH で行われている
- ゲーム上あり得ないリクエストをどう扱うのか悩むところが増える

このままでも機能的には実現できるんですが、うまいやり方はないかと試行錯誤して次の形に落ち着きました。

### 最終的なURL設計

ポイントは「セルを開く」という操作を「『開いたセル』を作る」として扱うように変更したことです。これによって、HTTPメソッドとURLの組み合わせで、何に対するどういう操作なのか、を表現できるようになりました。

|HTTP メソッド|パス|説明|
|:---|:---|:---|
|POST|/api/games|Game の作成|
|GET|/api/games/{id}|Game の取得|
|POST|/api/games/{gameId}/open-cells|Cell を開く|
|POST|/api/games/{gameId}/flags|フラグを立てる|
|DELETE|/api/games/{gameId}/flags/{id}|フラグを外す|


## 4. チート対策

ゲームのAPIサーバーなので一応チート対策として、ゲームが終了するまではどのセルに地雷がセットされているのか、という情報をクライアントに渡さないようにしています。

Express のドキュメントによると `res.json()` メソッドは内部で `JSON.stringify()` を使っています[^json]。また、オブジェクトに `toJSON` メソッドを実装すると `JSON.stringify()` の挙動を変更することが可能です[^stringify]。このことを利用して、ステータスがプレイ中の場合は `isMine` フラグを表示しない、などの制御を入れています。

[^json]: https://expressjs.com/ja/api.html#res.json

[^stringify]: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#tojson_behavior

```javascript:toJSONの使用例
const obj = {
  value: 0,
  toJSON () {
    return {
      value: this.value,
      text: this.value % 2 === 0 ? 'even' : 'odd'
    }
  }
}

obj.value = 1
JSON.stringify(obj) // "{\"value\":2,\"text\":\"odd\"}"

obj.value = 2
JSON.stringify(obj) // "{\"value\":2,\"text\":\"even\"}"
```

## さいごに

きっかけは「AWSの勉強をするために簡単な Web アプリケーションを作ってみよう」ということだったんですが、
やってみたら色々学ぶことが多く非常に楽しかったです。

ただやっぱり型が無いのは辛いですね…次は TypeScript に手を出してみようかな…