LoginSignup
0
2

More than 1 year has passed since last update.

TypeGraphql+Class-validatorで作成されるエラー(Argument Validation Error)をフォーマットして使いやすくする方法

Posted at

はじめに

個人的にNodeJsでGraphQLを実装する時にはTypeGraphQLを使用していますが、@InputTypeclass-validatorを使用したバリデーションは便利な反面返ってくるエラーの形式がそのままだとフロントエンドでは使いにくいため、エラーをフォーマットして使いやすくする方法を共有します。

TypeGraphql+class-validatorのバリデーション

TypeGraphqlでInputを実装する際はデコレータ@InputTypeを付与した上でクラスを定義し、クラス内に入力項目として使いたい値を定義していきます。その際にclass-validatorと併用することで入力項目のバリデーションができるようになり、非常に便利です。

UserInput.ts

//例:新規ユーザーを作成する際の入力項目とバリデーションを定義
@InputType()
export class CreateUserInput {
    @Field()
    @IsNotEmpty({ message: 'ユーザー名は必須項目です!' })
    @MaxLength(15, { message: '15文字以内に収めて下さい!' })
    username: string;

    @Field()
    @IsNotEmpty({ message: 'Eメールは必須項目です!' })
    @IsEmail({}, { message: '無効なEメールの形式です!' })
    email: string;

    @Field()
    @IsNotEmpty({ message: 'パスワードは必須項目です!' })
    @MaxLength(15, { message: '15文字以内に収めて下さい!' })
    @MinLength(2, { message: '2文字以上に設定して下さい!' })
    password: string;

    @Field()
    @IsNotEmpty({ message: '確認用パスワードは必須項目です!' })
    @Match('password', { message: 'パスワードが一致しません!' })
    confirmPassword: string;
}

例では新規ユーザーを作成する時の入力項目とバリデーション項目を定義しています。
こうすることで、入力項目に対してどのようなバリデーションが適用されるのかが一目瞭然になり、コードの見通しが非常に良くなります。
では実際にどのようにエラーが返ってくるか確認しましょう。

{
  "errors": [
    {
      "message": "Argument Validation Error",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "createUser"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "validationErrors": [
            {
              "target": {
                "username": "",
                "email": "",
                "password": "1234",
                "confirmPassword": "1234"
              },
              "value": "",
              "property": "username",
              "children": [],
              "constraints": {
                "isNotEmpty": "ユーザー名は必須項目です!"
              }
            },
            {
              "target": {
                "username": "",
                "email": "",
                "password": "1234",
                "confirmPassword": "1234"
              },
              "value": "",
              "property": "email",
              "children": [],
              "constraints": {
                "isEmail": "無効なEメールの形式です!",
                "isNotEmpty": "Eメールは必須項目です!"
              }
            }
          ],
・・・・//以下省略
}

このようにとても長いエラーオブジェクトが返されてきました・・・・
このエラーオブジェクトはそのままでは使えなさそうです。
理由としては
1・エラー形式が「INTERNAL_SERVER_ERROR」となっていること(GraphQLErrorの形式ではない)
2・varidationErrors.constraintsオブジェクトのkeyがclass-validatorのバリデータの名称になっているため、複数ヶ所同じ名前のエラーが発生したらどの入力項目で発生したエラーなのか判定するのはこのままでは難しい(例ではisNotEmptyが複数で発生しています)
なので、エラーをフロントエンドで扱いやすくなるようにフォーマットする必要がありそうです。
↑のエラーの理想としてはこんな形式ではないでしょうか↓。

//理想のエラーオブジェクト(例:入力項目「username」「email」が空白だった場合)
"errors": [
    "validationErrors": [
         {"username": "ユーザー名は必須項目です!"},
         {"email": "Eメールは必須項目です!"}
    ]
]

このようにすると、どの項目でどのようなエラーが発生したのか一目瞭然となり、フロントエンドでも扱いやすいオブジェクトとなるのではないでしょうか。
ではApolloServerの機能を使ってこのエラーをフォーマットしてみましょう。

ApolloServerのformatError関数を使用してエラーをフォーマットする

ApolloServerにはformatError関数が用意されており、この関数を使用することでエラーをフォーマットすることができます。

server.ts
const apolloServer = new ApolloServer({
        schema: await buildSchema({
            resolvers: [UserResolver],
        }),
        formatError: (err: GraphQLError) => {
            return err;
        },
    });

formatErrorで受け取ったエラー(↑の例でいうと「err」)にはリゾルバーがスローしたエラーであるoriginalErrorが含まれています。
このoriginalErrorのインスタンスの種類によってエラー処理を変えることや、エラーを使いやすい形にフォーマットすることが可能になります。

server.ts
const apolloServer = new ApolloServer({
        schema: await buildSchema({
            resolvers: [UserResolver],
        }),
        formatError: (err: GraphQLError) => {
            if (err.originalError instanceof ArgumentValidationError) {
                const errorMessage = err.extensions?.exception.validationErrors;
                //validationErrors.propertyのvalueをkeyに、validationErrors.constraintsの     
                //valueをvalueに持つ新たな配列を作成
                const object = errorMessage.map(
                    (e: { property: string; constraints: string }) => ({
                        [e.property]: Object.values(e.constraints),
                    })
                );
                //変数「object」は配列のため、reduceで値を取り出しオブジェクトにする
                const formattedErrors = object.reduce(
                    (result: string[], current: string[]) => {
                        let key: any = Object.keys(current);
                        result[key] = current[key];
                        return result;
                    }
                );
                throw new UserInputError('Errors', { formattedErrors });
               }
             return err;
        },
    });

先程のエラーをフォーマットするコードはこのようになります。
まず、class-validatorでスローされたエラーはArgumentValidationErrorインスタンスが含まれているため、その場合の処理をif文で分岐させます。
その後validationErrors.propertyのvalueをkeyに、validationErrors.constraintsのvalueをvalueに持つ新たな配列を作成し、さらにreduceにより配列→オブジェクトへと変換しています。
最後に作成したオブジェクトをUserInputErrorでスローすれば完成です。
フォーマットされたエラーがどのようになったか確認してみましょう。

{
  "errors": [
    {
      "message": "Errors",
      "extensions": {
        "formattedErrors": {
          "username": [
            "ユーザー名は必須項目です!"
          ],
          "email": [
            "無効なEメールの形式です!",
            "Eメールは必須項目です!"
          ]
        },
        "code": "BAD_USER_INPUT",
  //以下省略
}

大分扱いやすいデータになったのではないかと思います。
フロントエンドでこのエラーを使用する時は以下のようにすれば取得できます。

//例:React+ApolloClientでフォーマットしたエラーを取得する場合

const [error, setError] = useState<Record<string, string>>({});

const [createUser, { loading }] = useMutation(CREATE_USER,{
        onError: (err) =>
            setError(err.graphQLErrors[0].extensions?.formattedErrors),
        onCompleted: () => router.push('/login'),
    });

まとめ

ApolloServerformatErrorを使用すれば扱いやすいデータへとエラーをフォーマットできることがわかったと思います。
今後も新しいことが分かり次第随時記事を更新していこうと思います。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2