zod でのvalidation check
[前提]
next.jsでツイッターlikeなmusicpostアプリケーションを作成している。
今回tsでログインフォーム、新規登録フォームの処理を書いている。
色々苦労したのでまとめる。
tsのライブラリーであるzod , dbはmysqlを用いてユーザーの入力のチェック、新規登録を行う。mysqlの操作を少しでも覚えたいのでORMなどは使わない。
先に、完成版
以下からコードについて詳しくみていく。
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)、データの加工を行うため
const body = await request.json();
次にvalidationチェックを行う処理
const validation = formSchema.parse(body);
ここでのparse()は文字列を数値やオブジェクトに変換するperseではなく、zodの標準関数になる。もしエラーがあればzodErrorをthrowする。
import { z } from 'zod';
export const formSchema = z.object({
username: z.string()
.min(1, { message: '最低でも1文字以上である必要があります' })
.max(32, { message: 'ユーザー名が32文字を超えています' }),
email: z.string()
.email('有効なメールアドレスを入力してください'),
password: z.string()
.min(6, { message: '最低でも6文字以上である必要があります' })
.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だが、エラーによってメッセージを出し分けたいと思い、
上記のサイトを参考にした。
次にrefineだが、zodはrefineを用いて、独自ロジックを作成することができる
引数としてオプションを設定する。
次にdbからユーザーがあるかどうか調べる。
//重複しているユーザーがいないかチェックする処理
//検索を早めるために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の配列が0より大きければ、NextResponseでエラーを返す。
NextResponse ではjson形式で返したり、redirect()を使うことができる
次に行っている処理は,パスワードをハッシュ化して、dbにユーザー情報を登録する処理。
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)
そして実際に登録されたかは affectedRows in result で実際に登録されたかみている。
そして今回時間のかかったvalidationのチェックした結果をフロントに表示させる処理だが、ここではerroors配列を展開してそれぞれ、path , message をオブジェクトとして変換している。
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を扱う必要があった
interface ZodIssue {
code: string; // エラーの種類を示すコード
path: (string | number)[]; // エラーが発生したフィールドのパス
message: string; // 人間が読めるエラーメッセージ
expected?: string; // 期待される値の型(オプショナル)
received?: string; // 受け取った値の型(オプショナル)
}
以上上記によって正しくフロントの方でvalidationを表示できた。