Validation Errors

Control exactly how validation failures are formatted and returned to the client. Every validation library in MoroJS normalizes to a single shape, so you write one error handler and it works everywhere.

How Error Handling Works

When a validation schema rejects a request, MoroJS runs the failures through a single pipeline:

  1. Normalize — raw errors from Zod, Joi, Yup, class-validator, or custom functions are converted to a shared ValidationErrorDetailType[].
  2. Resolve handler — the per-route onValidationError wins if present, then the global handler passed to createApp(), then the built-in default.
  3. Respond — the handler returns a ValidationErrorResponse ({ status, body, headers? }) which MoroJS writes to the client.

Because the errors array is always the same shape, you never have to branch on which validation library produced a given failure.

1

Default Response Format

With no custom handler, MoroJS returns a 400 Bad Request with a consistent JSON body listing each failing field.

Default 400 Response (development)

json

1{
2  "success": false,
3  "error": "Validation failed for body",
4  "details": [
5    { "field": "email", "message": "Invalid email", "code": "invalid_string" },
6    { "field": "age",   "message": "Expected number, received string" }
7  ],
8  "requestId": "..."
9}

The error string names which part of the request failed (body, query, params, or headers). The details array is omitted when NODE_ENV=production to avoid leaking schema internals.

2

Per-Route Custom Handler

Pass onValidationError on any route to override the response shape for that route only.

Custom Handler on a Route (chainable API)

typescript

1import type { ValidationErrorHandler } from '@morojs/moro';
2
3const rfc7807: ValidationErrorHandler = (errors, ctx) => ({
4  status: 422,
5  headers: { 'Content-Type': 'application/problem+json' },
6  body: {
7    type: 'https://example.com/errors/validation',
8    title: 'Validation Failed',
9    status: 422,
10    instance: ctx.request.url,
11    field: ctx.field,
12    errors: errors.map(e => ({ field: e.field, message: e.message, code: e.code })),
13  },
14});
15
16// Pass onValidationError via .validate() — the route builder
17// takes a ValidationConfig, not a separate chainable method.
18app.post('/users')
19  .validate({ body: CreateUserSchema, onValidationError: rfc7807 })
20  .handler(createUser);

On a Module Route

typescript

1// In defineModule(), set onValidationError on the route object:
2defineModule({
3  name: 'users',
4  version: '1.0.0',
5  routes: [
6    {
7      method: 'POST',
8      path: '/users',
9      validation: { body: CreateUserSchema },
10      onValidationError: rfc7807,
11      handler: createUser,
12    },
13  ],
14});

What you get in the handler

  • errors — normalized failure list, always the same shape regardless of validation library
  • context.field — which request part failed (body, query, etc.)
  • context.requestmethod, url, path, headers
  • context.route — matched route method/path when available
3

Global Handler

Set one handler at app construction and every unvoiced route inherits it. Individual routes can still override with onValidationError.

App-Wide Validation Handler

typescript

1import { createApp, type ValidationErrorHandler } from '@morojs/moro';
2
3const handler: ValidationErrorHandler = (errors, ctx) => ({
4  status: 422,
5  body: {
6    ok: false,
7    path: ctx.request.path,
8    in: ctx.field,
9    errors: errors.map(e => ({ field: e.field, reason: e.message })),
10  },
11});
12
13// Note: the key on createApp() is 'onError', not 'onValidationError'.
14const app = createApp({
15  validation: { onError: handler },
16});

Resolution Order

  1. Per-route onValidationError
  2. Global handler from createApp({ validation })
  3. Built-in default (400 + normalized body)

Type Reference

ValidationErrorHandler

typescript

1type ValidationErrorHandler = (
2  errors: ValidationErrorDetailType[],
3  context: ValidationErrorContext,
4) => ValidationErrorResponse;

ValidationErrorDetailType

typescript

1interface ValidationErrorDetailType {
2  field: string;
3  message: string;
4  code?: string;
5  value?: unknown;
6  path?: (string | number)[];
7}

ValidationErrorContext

typescript

1interface ValidationErrorContext {
2  request: { method: string; url: string; path: string; headers: Record<string, any> };
3  route?:  { method: string; path: string };
4  field:   'params' | 'query' | 'body' | 'headers';
5}

ValidationErrorResponse

typescript

1interface ValidationErrorResponse {
2  status: number;
3  body: any;
4  headers?: Record<string, string>;
5}