skip to content
abdulghofurme

Transform zod error into readable error response

/ 3 min read

Perspective

Sebagai Frontend Engineer, beberapa kali menjumpai error validation yang diterima kurang mudah diolah.

Well, yah memang aku bukan expert sih, tapi menurutku setidaknya lebih baik apabila validation error yang diterima mudah untuk diolah.

Purpose

Mengubah zod validation error seperti berikut

{
	"issues": [
		{
			"code": "too_small",
			"minimum": 3,
			"type": "string",
			"inclusive": true,
			"exact": false,
			"message": "Panjang nama kategori minimal 3 karakter",
			"path": ["name"]
		}
	],
	"name": "ZodError"
}

menjadi error response dengan format key: message seperti berikut

{
	"name": "Panjang nama kategori minimal 3 karakter"
}

Dengan format key: message seperti di atas, setidaknya menurutku lebih mudah dibaca & diolah.

Sekali lagi, bukan berarti yang pertama susah. Tentu semua bisa diolah, hanya saja diperlukan satu lagi proses untuk mengambil pesan error dari masing-masing input yang ada.

The Code

Kita akan menggunakan Error/Exception Filter, yang merupakan standard yang digunakan untuk menangai error yang terjadi, secara terpusat.

Kita bisa generate filter dengan command

nest g f error
# nest generate filter [filter_name]
// ./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);
	}
}

Selanjutnya implementasikan filter tersebut dalam application

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

Explanation

HTTPException

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

Menangkap error HttpException error dan meneruskannya, sehingga

...
throw new HttpException(
  'Kategori tidak ditemukan',
  HttpStatus.NOT_FOUND,
);
...

akan mengembalikan response

{
	"status": 404,
	"data": {
		"message": "Kategori tidak ditemukan"
	}
}

ZodError

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

Akan menangkap ZodError, dan

...
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>,
    );
}
...

akan mengubah ZodError menjadi key: message object error seperti tujuan kita di atas.

Sehingga response yang diterima seperti berikut

{
	"status": 404,
	"data": {
		"message": "Validation error",
		"errors": {
			"name": "error message field specific"
		}
	}
}

Else

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

Well, ya itu akan mengambalikan error message internal server error 500.

Conclusion

Aku kira semua punya pendapat & standard masing-masing.

Namun dari sudut pandang FE noob ini, error validation akan lebih mudah diolah apabila berbentuk object key: message. Sehingga langsung bisa diambil & ditampilkan di masing-masing inputnya.

Terlepas dari itu, apabila team sudah punya standard yang berbeda. Pastikan menerima, bertanya & diskusikan dengan baik. Barangkali ada alasan lebih bagus dari hal tersebut.

Bolehlah dibagi pertimbangan-pertimbangan lain tersebut, ;)