DEV Community

Abdul Ghofur
Abdul Ghofur

Posted on

2

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. 😉

Image of Timescale

Timescale – the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay