Expressでjoiを使ってバリデーションを実装する

前回の記事では、Reactでバリデーションを実装してみました。

https://qiita.com/ngq299/items/47fbe64bd7201dc01a66

今回は、バックエンドでExpressにおいてJoiライブラリを使ったバリデーションの実装をしてみたいと思います。

### Joiライブラリの基本的な使い方

Joiライブラリは、データのバリデーションを簡単かつ強力に行うことができるツールです。
Joiのドキュメントを参照して、基本的な使い方を見てみます。

https://joi.dev/api/?v=17.13.3

まず、Joiライブラリを下記コマンドでインストールします。

```
npm i joi
```

下記コードを作成します。

```javascript:index.js
const Joi = require('joi')

const schema = Joi.object({
  name: Joi.string().required(),
  age: Joi.number().min(0),
  location: Joi.string()
})

const { value, error } = schema.validate({name: '鈴木', age: 12, location: '大阪'})
console.log("value: ", value)
console.log("error: ", error)
```

ここでは、Joiのオブジェクト`schema`を定義し、`schema`には`name`, `age`, `location`という三つのプロパティがあります。`name`プロパティは文字列型(`string`)、かつ省略不可(`required`)である。`age`プロパティは数字(`number`)で、かつ0以上でなければならない(`min(0)`)。`location`プロパティは文字列型(`string`)である。ドキュメントを見ると、ほかにもたくさんの型や数字の範囲を限定するメソッドがあります。これらがバリデーションチェックを楽にしてくれます。
さらに`validate`メソッドを用いて、引数にオブジェクトを代入し、結果を返します。

このコードを実行すると、下記出力になります。

```
value:  { name: '鈴木', age: 12, location: '大阪' }
error:  undefined
```

この結果からわかる通り、`value`には引数で代入したオブジェクトが格納されています。また、入力したオブジェクトはすべて条件を満たしている為エラーも発生していないようです。

次に、`schema`の`name`プロパティには`required`、`age`プロパティには`min(0)`がそれぞれ指定されていますので、`name`プロパティを省略した場合と`age`プロパティを負の値にした場合をそれぞれ試してみましょう。

#### nameプロパティを省略した場合

```javascript:index.js
const Joi = require('joi')

const schema = Joi.object({
  name: Joi.string().required(),
  age: Joi.number().min(0),
  location: Joi.string()
})

const { value, error } = schema.validate({ age: 12, location: '大阪' })
console.log("value: ", value)
console.log("error: ", error)
```

これを実行すると、下記結果が出力されました。

```
value:  { age: 12, location: '大阪' }
error:  [Error [ValidationError]: "name" is required] {
  _original: { age: 12, location: '大阪' },
  details: [
    {
      message: '"name" is required',
      path: [Array],
      type: 'any.required',
      context: [Object]
    }
  ]
}
```

想定通り`Error [ValidationError]: "name" is required`というエラーが出ました。

#### ageプロパティを負の値にした場合

```javascript:index.js
const Joi = require('joi')

const schema = Joi.object({
  name: Joi.string().required(),
  age: Joi.number().min(0),
  location: Joi.string()
})

const { value, error } = schema.validate({ name: '鈴木', age: -12, location: '大阪' })
console.log("value: ", value)
console.log("error: ", error)
```

これを実行すると下記結果が出力されました。

```
value:  { name: '鈴木', age: -12, location: '大阪' }
error:  [Error [ValidationError]: "age" must be greater than or equal to 0] {
  _original: { name: '鈴木', age: -12, location: '大阪' },
  details: [
    {
      message: '"age" must be greater than or equal to 0',
      path: [Array],
      type: 'number.min',
      context: [Object]
    }
  ]
}
```

これも想定通り`Error [ValidationError]: "age" must be greater than or equal to 0`というエラーが出ました。

### サーバーサイドにおけるバリデーションチェック

では、htmlファイルで簡単にログインフォームを作成して、express内でバリデーションチェックを実装してみたいと思います。

まず、`login.html`ファイルを作成します。

```javascript:login.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ログイン</title>
  <!-- axiosパッケージをインポートする -->
  <script src="https://unpkg.com/axios@1.6.7/dist/axios.min.js"></script>
</head>
<body>
    <div>
      <label>
        username: 
        <input type="text" class="username">
      </label>
    </div>
    <div>
      <label>
        password: 
        <input type="password" class="password">
      </label>
    </div>
    <button>ログイン</button>
    <p class="errorMessage"></p>

    <script>
      const uname = document.querySelector('.username')
      const pw = document.querySelector('.password')
      const button = document.querySelector('button')
      const errorMessage = document.querySelector('.errorMessage')

      button.addEventListener('click', async () => {
        await axios.post('http://localhost:3000/login', {
          username: uname.value,
          password: pw.value
        }).then(res => {
          errorMessage.style.color = 'blue'
          errorMessage.innerHTML = res.data
        }).catch(error => {
          errorMessage.style.color = 'red'
          errorMessage.innerHTML = error.response.data.message
        })
      })
    </script>
</body>
</html>
```

`login.html`は、`username`と`password`の値をサーバーに投げかけ、返ってきた結果を受け取って画面に表示するようになっています。

次にサーバーサイドのindex.jsファイルを作成します。

```javascript:index.js
const express = require('express')
const Joi = require('joi')
const cors = require('cors')
const app = express()

const loginSchema = Joi.object({
  // username: 文字列、アルファベットor数字、3文字以上、省略不可
  username: Joi.string().alphanum().min(3).required(),
  // password: 文字列、6文字以上、省略不可
  password: Joi.string().min(6).required()
})

app.use(express.json())
app.use(cors())

app.post('/login', (req, res) => {
  // req.bodyがloginSchemaの定義に沿っていなければ、変数errorにエラーが入る
  const { error } = loginSchema.validate(req.body)
  if (error) {
    return res.status(400).json({ message: error.details[0].message })
  }
  res.send('ログイン成功！')
})

app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000')
})
```

`index.js`では、`loginSchema`を定義し、`app.post('/login', ...)`APIで`validate`メソッドを使って`req.body`の形式が定義した通りになっているかをチェックしている。`error`が存在する場合はエラー内容を返し、存在しない場合は'ログイン成功！'を返す。

完成したものは下記のようになります。

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3532036/fb185c9d-c145-503a-adfb-a6eb5f337cc2.gif)