ユーザー新規登録処理を書いてみた(zod , node.js , mysql ) [備忘録] 

## zod でのvalidation check

[前提]
next.jsでツイッターlikeなmusicpostアプリケーションを作成している。
今回tsでログインフォーム、新規登録フォームの処理を書いている。
色々苦労したのでまとめる。
tsのライブラリーであるzod , dbはmysqlを用いてユーザーの入力のチェック、新規登録を行う。mysqlの操作を少しでも覚えたいのでORMなどは使わない。
<hr>

先に、完成版
以下からコードについて詳しくみていく。

```typescript
import { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import  { formSchema } from '../../../validations/schema';
import { ZodError } from "zod";
import bcrypt from 'bcrypt';
import { db } from '../../../lib/db';

export async function POST(request : NextRequest){
    try {
        //jsのオブジェクト形式にする処理
        const body = await request.json();
        console.log("リクエストをオブジェクト変換したもの" , body);
        //validationをここでチェック
        const validation = formSchema.parse(body);

        //重複しているユーザーがいないかチェックする処理
        //検索を早めるためにlimit 1をつける
        const [exitUser]  = await db.query(
            'SELECT * FROM users WHERE username = ? limit 1',
            [validation.username]
        );

        if(Array.isArray(exitUser) && exitUser.length > 0){
            return NextResponse.json(
                {error : 'ユーザーはすでに存在しています'},
                {status : 409}
            )
        }

        //パスワードのハッシュ
        const hashedPassword = await bcrypt.hash(validation.password , 10);
        console.log('hash化したパス' , hashedPassword);

        const [result] = await db.query(
            'insert into users (name , password , email) value (? , ? ,?)',
            [validation.username , hashedPassword , validation.email]
        );
        if(result && 'affectedRows' in result && result.affectedRows > 0){
            return NextResponse.json(
                {message : 'ユーザーが正しく作成されました'},
                {status : 200}
            );
        } else {
            return NextResponse.json(
                { error: 'ユーザーの作成に失敗しました' },
                { status: 500 }
            );
        }
    } catch (error) {
        //validation checkが通らなかった時にここに入る。zodはエラーになるとzodErrorインスタンスを返す
        if(error instanceof ZodError){
            //NextResponseでjsonの型を持つレスポンスを作成
            // zodのerrorsバリデーションエラーの配列.path,messageなどが入っている
            return NextResponse.json({
                error : 'validation error',
                errors : error.errors.map(err => ({
                    path : err.path,
                    message : err.message
                }))
                },
                {status : 400}
            )
        } else if(error instanceof SyntaxError){
            return NextResponse.json(
                {error : 'InValid json error'},
                {status : 400}
            )
        } else {
            console.error('Unexpected error' , error);
            return NextResponse.json(
                {error : 'Unexpected error'},
                {status : 500}
            )
        }
    }
}
```

まず,最初のリクエストを受け取る処理だが、json()メゾットを用いてjsのオブジェクトとして受け取っている。ここではjson形式にしているのではないので注意！
ここで行っているのは後のデータ操作(validation）、データの加工を行うため

```typescript
const body = await request.json();
```

https://developer.mozilla.org/ja/docs/Web/API/Response/json

次にvalidationチェックを行う処理

```typescript
const validation = formSchema.parse(body);
```
ここでのparse()は文字列を数値やオブジェクトに変換するperseではなく、zodの標準関数になる。もしエラーがあればzodErrorをthrowする。

```typescript
import { z } from 'zod';

export const formSchema = z.object({
    username: z.string()
        .min(1, { message: '最低でも１文字以上である必要があります' })
        .max(32, { message: 'ユーザー名が３２文字を超えています' }),
    email: z.string()
        .email('有効なメールアドレスを入力してください'),
    password: z.string()
        .min(6, { message: '最低でも６文字以上である必要があります' })
        .regex(/[0-9]+/, { message: '最低でも数字を1文字含める必要があります' }),
    confPass : z.string()
}).refine((val) => val.password === val.confPass , {
    message : 'パスワードが一致しません',
    path : ['confPass'],
});

export type FormSchemaData = z.infer<typeof formSchema>;
```
上記が作成したzodのスキーマ。
基本的な内容だが、今回触ったこととしてregex , refine , そしてinferによる型定義である。

まずregexだが、エラーによってメッセージを出し分けたいと思い、

https://zenn.dev/kysnrm/articles/21e5ed72ee1f34

上記のサイトを参考にした。

次にrefineだが、zodはrefineを用いて、独自ロジックを作成することができる

https://zod.dev/?id=refine

引数としてオプションを設定する。

次にdbからユーザーがあるかどうか調べる。
```typescript
//重複しているユーザーがいないかチェックする処理
        //検索を早めるためにlimit 1をつける
        const [exitUser]  = await db.query(
            'SELECT * FROM users WHERE username = ? limit 1',
            [validation.username]
        );

        if(Array.isArray(exitUser) && exitUser.length > 0){
            return NextResponse.json(
                {error : 'ユーザーはすでに存在しています'},
                {status : 409}
            )
        }
```
ここでexitUserの配列が０より大きければ、NextResponseでエラーを返す。

https://nextjs.org/docs/app/api-reference/functions/next-response

NextResponse ではjson形式で返したり、redirect()を使うことができる
<br>

次に行っている処理は,パスワードをハッシュ化して、dbにユーザー情報を登録する処理。

```typescript
const hashedPassword = await bcrypt.hash(validation.password , 10);
        console.log('hash化したパス' , hashedPassword);

        const [result] = await db.query(
            'insert into users (name , password , email) value (? , ? ,?)',
            [validation.username , hashedPassword , validation.email]
        );
        if(result && 'affectedRows' in result && result.affectedRows > 0){
            return NextResponse.json(
                {message : 'ユーザーが正しく作成されました'},
                {status : 200}
            );
        } else {
            return NextResponse.json(
                { error: 'ユーザーの作成に失敗しました' },
                { status: 500 }
            );
        }
```
パスワードのハッシュ化処理を行う(bcyrpt)

https://zenn.dev/417/scraps/43f1ffbe90132c

そして実際に登録されたかは affectedRows in result で実際に登録されたかみている。

そして今回時間のかかったvalidationのチェックした結果をフロントに表示させる処理だが、ここではerroors配列を展開してそれぞれ、path , message をオブジェクトとして変換している。

```typescript
 catch (error) {
        //validation checkが通らなかった時にここに入る。zodはエラーになるとzodErrorインスタンスを返す
        if(error instanceof ZodError){
            //NextResponseでjsonの型を持つレスポンスを作成
            // zodのerrorsバリデーションエラーの配列.path,messageなどが入っている
            return NextResponse.json({
                error : 'validation error',
                errors : error.errors.map(err => ({
                    path : err.path,
                    message : err.message
                }))
                },
                {status : 400}
            )
        } else if(error instanceof SyntaxError){
            return NextResponse.json(
                {error : 'InValid json error'},
                {status : 400}
            )
        } else {
            console.error('Unexpected error' , error);
            return NextResponse.json(
                {error : 'Unexpected error'},
                {status : 500}
            )
        }
```
サーバーサイドでの変換した状態とフロントで扱う状態が正しく連携しておらず、エラーになってしまっていた。なのでZodErrorの中身を理解してどのようなデータ構造なのかを把握する必要があった。

zodErrorの構造としては、
ZodErrorの主な構造:

1 : errors: エラーの詳細情報を含む配列
2 : issues: errors と同じ内容を持つ別名のプロパティ
3 : その他のメタデータ

が挙げられ、errorsの構造は配下になる。今回はmessageを扱う必要があった
```typescript
interface ZodIssue {
  code: string;           // エラーの種類を示すコード
  path: (string | number)[];  // エラーが発生したフィールドのパス
  message: string;        // 人間が読めるエラーメッセージ
  expected?: string;      // 期待される値の型（オプショナル）
  received?: string;      // 受け取った値の型（オプショナル）
}
```

以上上記によって正しくフロントの方でvalidationを表示できた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679572/88f9fa47-9d61-5b0c-bf2a-6a91166d2d22.png)