DEV Community

abdulghofurme
abdulghofurme

Posted on

Transform zod error into readable error response

Transform zod validation error into FE readable key message object in NestJS

Bahasa Indonesia Version

https://abdulghofurme.github.io/posts/transform-zod-error-into-readable-error-response

Perspective

As a Frontend Engineer,
I have occasionally encountered validation errors that are less easy to process.

Well, I’m not an expert,
but in my opinion, it would be better if the validation errors received were easier to handle.

Purpose

To transform Zod validation errors like this:

{
    "issues": [
        {
            "code": "too_small",
            "minimum": 3,
            "type": "string",
            "inclusive": true,
            "exact": false,
            "message": "The category name must be at least 3 characters long",
            "path": ["name"]
        }
    ],
    "name": "ZodError"
}
Enter fullscreen mode Exit fullscreen mode

into an error response in the key: message format like this:

{
    "name": "The category name must be at least 3 characters long"
}
Enter fullscreen mode Exit fullscreen mode

With the key: message format above,
I think it is easier to read and process.

Once again,
this doesn’t mean the first format is difficult. Of course, it can be processed, but it requires an additional step to extract error messages from each input field.

The Code

We will use an Error/Exception Filter,
a standard approach for handling errors centrally.

You can generate the filter with this command:

nest g f error
# nest generate filter [filter_name]
Enter fullscreen mode Exit fullscreen mode

Here’s the implementation:

// ./error/error.filter.ts

import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from "@nestjs/common";
import { Response } from "express";
import { TWebResponse } from "src/model/web.model";
import { ZodError, ZodIssue } from "zod";

@Catch()
export class ErrorFilter<T> implements ExceptionFilter {
    zodErrorToKeyedObject(error: ZodError): Record<string, string> {
        return error.errors.reduce(
            (acc, issue) => {
                const key = issue.path.join(".");
                acc[key] = issue.message;
                return acc;
            },
            {} as Record<string, string>,
        );
    }

    catch(exception: any, host: ArgumentsHost) {
        const response: Response = host.switchToHttp().getResponse();
        let result: {
            status: HttpStatus;
            json: TWebResponse;
        };

        if (exception instanceof HttpException) {
            result = {
                status: exception.getStatus(),
                json: {
                    message: exception?.message,
                },
            };
        } else if (exception instanceof ZodError) {
            result = {
                status: HttpStatus.BAD_REQUEST,
                json: {
                    message: "Validation error",
                    errors: this.zodErrorToKeyedObject(exception),
                },
            };
        } else {
            result = {
                status: HttpStatus.INTERNAL_SERVER_ERROR,
                json: {
                    message: exception?.message,
                },
            };
        }

        response.status(result.status).json(result.json);
    }
}
Enter fullscreen mode Exit fullscreen mode

Next, apply the filter to your application:

// app/global module
...
import { APP_FILTER } from '@nestjs/core';
import { ErrorFilter } from './error/error.filter';
...
@Module({
...
  providers: [
    ...
    {
      provide: APP_FILTER,
      useClass: ErrorFilter,
    },
  ],
  ...
})
Enter fullscreen mode Exit fullscreen mode

Explanation

HTTPException

...
if (exception instanceof HttpException) {
    result = {
        status: exception.getStatus(),
        json: {
            message: exception?.message,
        },
    };
}
...
Enter fullscreen mode Exit fullscreen mode

Handles the HttpException and forwards it, so that:

...
throw new HttpException(
  'Category not found',
  HttpStatus.NOT_FOUND,
);
...
Enter fullscreen mode Exit fullscreen mode

will return this response:

{
    "status": 404,
    "data": {
        "message": "Category not found"
    }
}
Enter fullscreen mode Exit fullscreen mode

ZodError

...
else if (exception instanceof ZodError) {
    result = {
        status: HttpStatus.BAD_REQUEST,
        json: {
            message: 'Validation error',
            errors: this.zodErrorToKeyedObject(exception),
        },
    };
}
...
Enter fullscreen mode Exit fullscreen mode

Captures a ZodError, and the method:

...
zodErrorToKeyedObject(error: ZodError): Record<string, string> {
    return error.errors.reduce(
        (acc, issue) => {
            const key = issue.path.join('.');
            acc[key] = issue.message;
            return acc;
        },
        {} as Record<string, string>,
    );
}
...
Enter fullscreen mode Exit fullscreen mode

transforms the ZodError into a key: message error object as per our goal.

This results in a response like this:

{
    "status": 400,
    "data": {
        "message": "Validation error",
        "errors": {
            "name": "Field-specific error message"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Else

...
else {
    result = {
        status: HttpStatus.INTERNAL_SERVER_ERROR,
        json: {
            message: exception?.message,
        },
    };
}
...
Enter fullscreen mode Exit fullscreen mode

Handles any other errors, returning an internal server error (500).

Conclusion

I believe everyone has their own opinions and standards.

However, from this noob FE’s perspective,
validation errors are easier to handle if they are in the key: message object format,
allowing them to be directly extracted and displayed for each input field.

That said, if the team already has a different standard,
make sure to accept, ask questions, and discuss it well.
There might be better reasons behind it.

Feel free to share other considerations. 😉

Top comments (0)