ReactからExpress側のWebAPIを呼び出してみた（その2）

[前回](https://qiita.com/betarium/items/d01ef5325d5767af840d)までで画面からAPIを呼び出す処理を作成したが、RESTに準拠したAPIに修正する。

:::note
[前回：ReactからExpress側のWebAPIを呼び出してみた（その1）](https://qiita.com/betarium/items/d01ef5325d5767af840d)
:::

https://qiita.com/betarium/items/d01ef5325d5767af840d

## シリーズ記事一覧

https://qiita.com/betarium/items/f692b6221ece484cefce
# RESTに準拠したAPIの作成

前回はブラウザからAPIを動作確認しやすいように`get`メソッドに応答するAPIを作成してきたが、RESTではCRUDの処理に合わせたメソッドを使用したほうが良いとされているので、パスやメソッドをRESTっぽく変えてみる。

（RESTに厳密に準拠すると説明が長くなるので部分的に修正する）

## API修正

まずはユーザー一覧と詳細のAPIを追加する。

### users.ts

以下のコードを`users.ts`に追加する。
APIのパスは`/api/users`と`/api/users/:id`と短くしている。
また、パスワードをAPIで送信しないよう`getUserOutput`で変換している。

```routes/users.ts
function getUserOutput(user: UserModel): UserModel {
  const output = { id: user.id, account: user.account } as UserModel
  return output
}

router.get('/', function (req, res, next) {
  const destList = testUserList.map(user => getUserOutput(user))
  res.json(destList)
})

router.get('/:id', function (req, res, next) {
  const id = parseInt(req.params.id)

  const user = testUserList.find(p => p.id === id)

  if (user === undefined) {
    res.sendStatus(404)
    return
  }

  const dest = getUserOutput(user)

  res.json(dest)
})
```

同様に、ユーザー作成APIを追加する。
データの作成や更新を行う場合は`post`や`put`メソッドを使用し、データ取得系のAPIと区別しやすくするのが一般的である。
`routes.post()`を使用すると`post`メソッドで応答するAPIとなる。

```routes/users.ts
router.post('/', function (req, res, next) {
  const user = req.body as UserModel

  const oldId = testUserList.map(p => p.id).reduce((a, b) => Math.max(a, b), 0)

  user.id = oldId + 1

  testUserList.push(user)

  const dest = getUserOutput(user)

  res.json(dest);
});
```

更に、ユーザー更新と削除APIを追加する。
メソッドは`put`と`delete`にしている。
メソッドが異なる場合は同じパスを使用できるので、`/api/users/:id`と`/api/users`の短いパスにしている。

```routes/users.ts
router.put('/:id', function (req, res, next) {
  const id = parseInt(req.params.id)
  const userInput = req.body as UserModel

  const user = testUserList.find(p => p.id === id)

  if (user === undefined) {
    res.sendStatus(404)
    return
  }

  user.account = userInput.account
  if (userInput.password !== undefined && userInput.password.length > 0) {
    user.password = userInput.password
  }

  res.json({ success: true });
});

router.delete('/:id', function (req, res, next) {
  const id = parseInt(req.params.id)

  const index = testUserList.findIndex(p => p.id === id)
  if (index < 0) {
    res.sendStatus(404)
    return
  }

  testUserList.splice(index, 1)

  res.json({ success: true });
});
```

ここまででサーバ側のAPIの追加は完了である。次はAPIを呼び出す画面側を修正する。

## 画面修正

ユーザー一覧画面とユーザー詳細画面を修正し、ユーザーの追加・削除も行えるようにする。

### main.tsx

ユーザー詳細画面をユーザー登録にも流用するため、`/users/new`のパスで画面の`Route`を追加する。注意点として、`Route`のパスは上から順に判定されるので、`/users/new`を上に追加しないと`/users/new`のパスに一致しなくなる。

```main.tsx
        <Route path="/users/new" element={<UsersDetailPage />} />
        <Route path="/users/:id" element={<UsersDetailPage />} />
```

### UsersListPage.tsx

ユーザー一覧APIのURLを修正する。

```pages/UsersListPage.tsx
    // const apiPath = "/api/users/list"
    const apiPath = "/api/users"
```

createボタンが押されたらユーザー詳細画面に遷移するよう修正。

```pages/UsersListPage.tsx
  const onClickUserCreateLink = useCallback((e: MouseEvent) => {
    e.preventDefault()
    // alert("create!!")
    navigate("/users/new")
  }, [navigate])
```

deleteボタンが押されたら削除APIを呼び出すよう修正。
`fetch`メソッドは、`RequestInit.method`でHTTPメソッドを指定して呼び出すことができる。
ここでは削除処理なので`delete`メソッドを指定している。
削除処理が完了したら`loadUserList`を呼び出してユーザー一覧を更新している。

```pages/UsersListPage.tsx
  const onDelete = useCallback(async (user: UserModel) => {
    if (!confirm(`delete user [${user.account}]?`)) {
      return
    }

    const req = {
      method: "delete"
    } as RequestInit
    const res = await fetch("/api/users/" + user.id, req)
    const data = await res.json()
    if (data.success) {
      alert("delete complete");
    }

    loadUserList()
  }, [loadUserList])

  const onClickUserDelete = useCallback((e: MouseEvent, user: UserModel) => {
    e.preventDefault()
    // alert(`${user.account} delete!!`)
    onDelete(user)
  }, [onDelete])
```

### UsersDetailPage.tsx

ユーザーの登録・更新に対応する。

ユーザー登録の場合はユーザーIDが`undefined`になるので、判別できるよう`-1`を設定しておく。

```pages/UsersDetailPage.tsx
  // const userId = parseInt(params["id"]!!)
  const userId = parseInt(params["id"] ?? "-1")
```

ユーザー詳細APIのパスを修正する。
```pages/UsersDetailPage.tsx
    // const apiPath = "/api/users/detail/" + user.id
    const apiPath = "/api/users/" + user.id
```

ユーザー登録APIの呼び出し処理を追加する。
ユーザー登録APIは`post`メソッドで呼び出す。
ヘッダに`Content-Type`を指定してJSON形式で送信することを指定する。
送信するデータはJSON形式の文字列にして`body`に指定する。
ユーザー登録後はAPIのレスポンスからユーザー情報を取得し画面を更新している。

```pages/UsersDetailPage.tsx
  const createUser = useCallback(async () => {
    const req = {
      method: "post",
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(user)
    } as RequestInit
    const res = await fetch("/api/users", req)
    if (res.status >= 400) {
      alert("save error")
      return
    }
    const data = await res.json() as UserModel
    setUser(data)
    alert("save complete!!")
  }, [user, setUser])
```

同様に、ユーザー更新APIの呼び出し処理を追加する。
ユーザー更新APIは`put`メソッドで呼び出している。
更新後はユーザー一覧画面に遷移している。
（※登録・更新後に画面の表示を更新する場合と、一覧画面に戻るシステムのどちらもありうるので、ここでは更新後は一覧画面に戻る処理にしている）

```pages/UsersDetailPage.tsx
  const updateUser = useCallback(async () => {
    const req = {
      method: "put",
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(user)
    } as RequestInit
    const res = await fetch("/api/users/" + user.id, req)
    if (res.status >= 400) {
      alert("save error")
      return
    }
    navigate("/users")
  }, [user, navigate])
```

保存ボタンを押された時の処理を修正し、新規登録の場合(user.id === -1)なら登録APIを、それ以外なら更新APIを呼び出す。

```pages/UsersDetailPage.tsx
  const onSave = useCallback((e: MouseEvent) => {
    e.preventDefault()
    // alert("save!!")
    if (user.id === -1) {
      createUser()
    }
    else {
      updateUser()
    }
  }, [createUser, updateUser, user])
```

入力が更新された場合の処理を追加する。
Reactでは随時画面が更新されるので、入力内容は状態変数に保存しておく。
`{ ...user, account: e.target.value }`の箇所は変数の要素を展開する[スプレッド構文](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax)を利用して変数のコピーを作成しているもので、`user`変数のコピーに`account`プロパティを設定している。
`setUser`に指定する変数は更新したことを知らせるため`user`とは別の変数を指定する必要があり、このような処理が必要になる。

```pages/UsersDetailPage.tsx
  const onChangeAccount = useCallback((e: ChangeEvent<HTMLInputElement>) => {
    const newUser = { ...user, account: e.target.value }
    setUser(newUser)
  }, [user, setUser])

  const onChangePassword = useCallback((e: ChangeEvent<HTMLInputElement>) => {
    const newUser = { ...user, password: e.target.value }
    setUser(newUser)
  }, [user, setUser])
```

入力欄を修正する。
入力内容が変更されるとonChangeが呼ばれるので、そこで変数に値を保存している。

```pages/UsersDetailPage.tsx
            <input type="text" value={user.account} onChange={onChangeAccount} autoComplete="off" />
            <input type="password" value={user.password} onChange={onChangePassword} autoComplete="new-password" />
```

# 動作確認

フロントとサーバの両方を起動しユーザー一覧画面を表示する。
http://localhost:3000/users

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/372771/a257e452-4a81-d853-5bd1-4fdf050ba5ce.png)

ユーザーの追加・削除・更新が行えれば成功である。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/372771/8bf91e36-c5bd-618c-1c62-5dfb2a211a73.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/372771/662bfd72-4217-3d35-54dc-ca98805c8518.png)

2回に分けてもだいぶ長い記事になってしまったが、これでフロント・サーバでのAPIの実装について解説できたのではないかと思う。

# サンプルコード
[https://github.com/betarium/sample-react-express/tree/0.5.1](https://github.com/betarium/sample-react-express/tree/0.5.1)