Universal Validation System

MoroJS supports any validation library through universal adapters. Use Zod, Joi, Yup, class-validator, or custom functions with the same consistent API.

Universal Validation Support

MoroJS v1.4.0 introduces universal validation support - use any validation library you prefer:

  • Zod - TypeScript-first with zero dependencies (default)
  • Joi - Mature, feature-rich validation library
  • Yup - Simple, lightweight object validation
  • Class Validator - Decorator-based validation
  • Custom Functions - Write your own validation logic

Universal Validation Examples

Multiple Validation Libraries

typescript

1import { createApp } from '@morojs/moro';
2import { z, joi, yup, classValidator, customValidator } from '@morojs/moro';
3import Joi from 'joi';
4import * as yupLib from 'yup';
5
6const app = createApp();
7
8// 1. Zod (default - works directly)
9app.post('/users/zod')
10  .body(z.object({
11    name: z.string().min(1),
12    email: z.string().email(),
13    age: z.number().min(18)
14  }))
15  .handler(({ body }) => {
16    return { success: true, user: body, library: 'zod' };
17  });
18
19// 2. Joi (via adapter)
20app.post('/users/joi')
21  .body(joi(Joi.object({
22    name: Joi.string().required(),
23    email: Joi.string().email().required(),
24    age: Joi.number().min(18)
25  })))
26  .handler(({ body }) => {
27    return { success: true, user: body, library: 'joi' };
28  });
29
30// 3. Yup (via adapter)
31app.post('/users/yup')
32  .body(yup(yupLib.object({
33    name: yupLib.string().required(),
34    email: yupLib.string().email().required(),
35    age: yupLib.number().min(18)
36  })))
37  .handler(({ body }) => {
38    return { success: true, user: body, library: 'yup' };
39  });

Class Validator & Custom Functions

typescript

1import { IsString, IsEmail, IsNumber, Min } from 'class-validator';
2
3// 4. Class Validator (via adapter)
4class CreateUserDto {
5  @IsString()
6  name!: string;
7
8  @IsEmail()
9  email!: string;
10
11  @IsNumber()
12  @Min(18)
13  age!: number;
14}
15
16app.post('/users/class')
17  .body(classValidator(CreateUserDto))
18  .handler(({ body }) => {
19    return { success: true, user: body, library: 'class-validator' };
20  });
21
22// 5. Custom validation functions
23const validateUser = customValidator(async (data: any) => {
24  if (!data.name || typeof data.name !== 'string') {
25    throw new Error('Name is required and must be a string');
26  }
27  if (!data.email || !data.email.includes('@')) {
28    throw new Error('Valid email is required');
29  }
30  return {
31    name: data.name.trim(),
32    email: data.email.toLowerCase(),
33    validated: true
34  };
35}, 'user-validator');
36
37app.post('/users/custom')
38  .body(validateUser)
39  .handler(({ body }) => {
40    return { success: true, user: body, library: 'custom' };
41  });

Query Parameter Validation

typescript

1const SearchQuerySchema = z.object({
2  q: z.string().min(1, "Search query is required"),
3  limit: z.coerce.number().min(1).max(100).default(10),
4  page: z.coerce.number().min(1).default(1)
5});
6
7app.get('/search', {
8  query: SearchQuerySchema,
9  handler: ({ query }) => {
10    // All query params are validated and have proper types
11    // query: { q: string, limit: number, page: number }
12    
13    return {
14      query: query.q,
15      limit: query.limit,
16      page: query.page,
17      results: []
18    };
19  }
20});

Advanced Schema Patterns

Nested Objects and Arrays

typescript

1const AddressSchema = z.object({
2  street: z.string(),
3  city: z.string(),
4  country: z.string(),
5  postalCode: z.string().regex(/^\d{5}$/, "Invalid postal code")
6});
7
8const CreateUserSchema = z.object({
9  name: z.string().min(1),
10  email: z.string().email(),
11  addresses: z.array(AddressSchema).min(1, "At least one address required"),
12  tags: z.array(z.string()).optional(),
13  preferences: z.object({
14    newsletter: z.boolean().default(false),
15    theme: z.enum(['light', 'dark']).default('light')
16  }).optional()
17});

Conditional Validation

typescript

1const PaymentSchema = z.discriminatedUnion('type', [
2  z.object({
3    type: z.literal('credit_card'),
4    cardNumber: z.string().regex(/^\d{16}$/, "Invalid card number"),
5    expiryDate: z.string().regex(/^\d{2}\/\d{2}$/, "Invalid expiry date"),
6    cvv: z.string().regex(/^\d{3,4}$/, "Invalid CVV")
7  }),
8  z.object({
9    type: z.literal('bank_transfer'),
10    accountNumber: z.string(),
11    routingNumber: z.string()
12  }),
13  z.object({
14    type: z.literal('paypal'),
15    paypalEmail: z.string().email()
16  })
17]);

Custom Validation

typescript

1const PasswordSchema = z.string()
2  .min(8, "Password must be at least 8 characters")
3  .regex(/[A-Z]/, "Password must contain at least one uppercase letter")
4  .regex(/[a-z]/, "Password must contain at least one lowercase letter")
5  .regex(/\d/, "Password must contain at least one number")
6  .regex(/[^\w\s]/, "Password must contain at least one special character");
7
8const RegisterSchema = z.object({
9  email: z.string().email(),
10  password: PasswordSchema,
11  confirmPassword: z.string()
12}).refine(data => data.password === data.confirmPassword, {
13  message: "Passwords don't match",
14  path: ["confirmPassword"]
15});

Error Handling

MoroJS automatically handles validation errors and returns structured error responses:

Automatic Error Responses

json

1// When validation fails, MoroJS automatically returns:
2{
3  "error": "VALIDATION_ERROR",
4  "message": "Request validation failed",
5  "details": [
6    {
7      "field": "email",
8      "message": "Invalid email format"
9    },
10    {
11      "field": "age",
12      "message": "Must be at least 18 years old"
13    }
14  ]
15}

Custom Error Handling

typescript

1app.post('/users', {
2  body: CreateUserSchema,
3  handler: ({ body }) => {
4    try {
5      // Your logic here
6      return { success: true };
7    } catch (error) {
8      return {
9        status: 400,
10        body: {
11          error: 'BUSINESS_LOGIC_ERROR',
12          message: error.message
13        }
14      };
15    }
16  },
17  
18  // Custom validation error handler
19  onValidationError: (errors) => {
20    return {
21      status: 422,
22      body: {
23        error: 'CUSTOM_VALIDATION_ERROR',
24        message: 'Your request is invalid',
25        fields: errors.map(err => ({
26          field: err.path.join('.'),
27          issue: err.message
28        }))
29      }
30    };
31  }
32});

Schema Composition and Reuse

Reusable Base Schemas

typescript

1// Base schemas
2const BaseEntitySchema = z.object({
3  id: z.string().uuid(),
4  createdAt: z.string().datetime(),
5  updatedAt: z.string().datetime()
6});
7
8const PaginationSchema = z.object({
9  page: z.coerce.number().min(1).default(1),
10  limit: z.coerce.number().min(1).max(100).default(20)
11});
12
13// Composed schemas
14const UserSchema = BaseEntitySchema.extend({
15  name: z.string(),
16  email: z.string().email(),
17  role: z.enum(['user', 'admin'])
18});
19
20const CreateUserSchema = UserSchema.omit({
21  id: true,
22  createdAt: true,
23  updatedAt: true
24});
25
26const UpdateUserSchema = CreateUserSchema.partial();
27
28const ListUsersQuerySchema = PaginationSchema.extend({
29  role: z.enum(['user', 'admin']).optional(),
30  search: z.string().optional()
31});

Schema Transformations

typescript

1const ProcessedUserSchema = z.object({
2  name: z.string().transform(val => val.trim().toLowerCase()),
3  email: z.string().email().transform(val => val.toLowerCase()),
4  age: z.coerce.number(), // Automatically converts strings to numbers
5  tags: z.string().transform(val => val.split(',').map(s => s.trim())),
6  isActive: z.union([z.boolean(), z.string()])
7    .transform(val => val === true || val === 'true')
8});
9
10// Usage
11app.post('/users', {
12  body: ProcessedUserSchema,
13  handler: ({ body }) => {
14    // body.name is trimmed and lowercase
15    // body.email is lowercase
16    // body.age is a number
17    // body.tags is an array of strings
18    // body.isActive is a boolean
19    
20    return { user: body };
21  }
22});

Best Practices

Do

  • • Use descriptive error messages
  • • Create reusable base schemas
  • • Validate all external inputs
  • • Use transformations for data cleaning
  • • Define response schemas for documentation
  • • Use z.infer for type extraction

Don't

  • • Skip validation for "trusted" inputs
  • • Use overly complex nested schemas
  • • Ignore validation error details
  • • Validate the same data multiple times
  • • Use any types instead of proper schemas
  • • Forget to handle edge cases

Next Steps