Skip to content

Custom error responses

In Express, pass an onInvalid(issues, req, res, next) function to guard() and send whatever response you want. In web frameworks, the guard returns the issues and you build the response yourself. The default is a 400 with { "errors": issues }.

guard(User, {
allow: ["name", "email"],
onInvalid: (issues, req, res) => {
res.status(422).json({
message: "Validation failed",
fields: Object.fromEntries(issues.map((issue) => [issue.path, issue.message])),
});
},
});

onInvalid replaces the default response completely. Your handler doesn’t run unless you call next() yourself.

Write it once and pass it to every guard:

import type { InvalidHandler } from "mongoose-guard/express";
export const sendValidationError: InvalidHandler = (issues, req, res) => {
res.status(400).json({ error: "VALIDATION_FAILED", issues });
};
router.post("/users", guard(User, { allow: ["name"], onInvalid: sendValidationError }), createUser);

Or wrap guard so nobody can forget:

import { guard as baseGuard, type ExpressGuardOptions } from "mongoose-guard/express";
export const guard = (model: Parameters<typeof baseGuard>[0], options: ExpressGuardOptions) =>
baseGuard(model, { onInvalid: sendValidationError, ...options });

If your app already has a central error handler, turn the issues into an error and pass it on:

class ValidationError extends Error {
constructor(readonly issues: Issue[]) {
super("Validation failed");
}
}
const forwardToErrorHandler: InvalidHandler = (issues, req, res, next) => {
next(new ValidationError(issues));
};

Some APIs use the standard application/problem+json format:

const problemDetails: InvalidHandler = (issues, req, res) => {
res
.status(400)
.type("application/problem+json")
.json({
type: "https://example.com/problems/validation",
title: "Your request body is invalid",
status: 400,
instance: req.originalUrl,
errors: issues,
});
};

The web guard never sends a response itself. Build one from result.issues:

const result = await validateUser(request);
if (!result.ok) {
return Response.json(
{ message: "Validation failed", fields: result.issues.map(({ path, message }) => ({ path, message })) },
{ status: 422 },
);
}

invalidResponse(issues, init) is a shortcut for the default shape. It accepts a status and headers in init.

message is written for developers, and Mongoose’s messages mention internal names (Path `name` is required.). For an API used by your own frontend, mapping code, path and rule to friendly text in the frontend usually gives the best result. Results and issues has an example. You can also put friendly messages straight into the Mongoose schema, and they come through unchanged.