Features
Docs
CLI
Benchmarks
Examples

© 2024 MoroJs

Universal Validation System

Use any validation library you prefer. MoroJS supports Zod, Joi, Yup, class-validator, or custom functions—all with the same consistent API.

Use Any Validation Library

Don't like Zod? Use Joi. Prefer Yup? That works too.Same API, your choice of library.

Supported Libraries

Zod

TypeScript-first, zero dependencies (default)

Joi

Mature, feature-rich validation

Yup

Simple, lightweight validation

Class Validator

Decorator-based validation

Or write your own custom validation functions

Why Universal Validation Matters

Don't rewrite your validation logic. Use what you already know.

Without universal support, you're locked into one library or forced to rewrite everything.

Without Universal Support

  • Locked into one validation library
  • Rewriting schemas when switching
  • Team members forced to learn new syntax
  • No flexibility for different use cases

With MoroJS

  • Use any validation library you prefer
  • Same consistent API across all libraries
  • Team uses what they already know
  • Mix and match as needed

It's This Easy

Use your preferred library. Same API, different syntax.

Zod (default) - works directly

typescript

1import { createApp } from '@morojs/moro';
2import { z } from 'zod';
3
4app.post('/users', {
5  body: z.object({
6    name: z.string().min(1),
7    email: z.string().email(),
8    age: z.number().min(18)
9  }),
10  handler: ({ body }) => {
11    return { success: true, user: body };
12  }
13});

Why It Makes Sense

Flexible

Use the validation library your team knows best. No forced migrations.

Consistent

Same API regardless of library. Learn once, use everywhere.

Powerful

Mix libraries or write custom validators. Full control.

How It Works

MoroJS uses universal adapters to support any validation library. Define your schema using your preferred library's syntax, and MoroJS handles the rest—validation, error handling, and type inference.

Validation Libraries

Zod (Default) - Works Directly

typescript

1import { createApp } from '@morojs/moro';
2import { z } from 'zod';
3
4app.post('/users', {
5  body: z.object({
6    name: z.string().min(1),
7    email: z.string().email(),
8    age: z.number().min(18)
9  }),
10  handler: ({ body }) => {
11    return { success: true, user: body };
12  }
13});

Joi - Via Adapter

typescript

1import { createApp } from '@morojs/moro';
2import { joi } from '@morojs/moro';
3import Joi from 'joi';
4
5app.post('/users', {
6  body: joi(Joi.object({
7    name: Joi.string().required(),
8    email: Joi.string().email().required(),
9    age: Joi.number().min(18)
10  })),
11  handler: ({ body }) => {
12    return { success: true, user: body };
13  }
14});

Yup - Via Adapter

typescript

1import { createApp } from '@morojs/moro';
2import { yup } from '@morojs/moro';
3import * as yupLib from 'yup';
4
5app.post('/users', {
6  body: yup(yupLib.object({
7    name: yupLib.string().required(),
8    email: yupLib.string().email().required(),
9    age: yupLib.number().min(18)
10  })),
11  handler: ({ body }) => {
12    return { success: true, user: body };
13  }
14  });

Class Validator - Via Adapter

typescript

1import { createApp } from '@morojs/moro';
2import { classValidator } from '@morojs/moro';
3import { IsString, IsEmail, IsNumber, Min } from 'class-validator';
4
5class CreateUserDto {
6  @IsString()
7  name!: string;
8
9  @IsEmail()
10  email!: string;
11
12  @IsNumber()
13  @Min(18)
14  age!: number;
15}
16
17app.post('/users', {
18  body: classValidator(CreateUserDto),
19  handler: ({ body }) => {
20    return { success: true, user: body };
21  }
22});

Custom Validation Functions

typescript

1import { createApp } from '@morojs/moro';
2import { customValidator } from '@morojs/moro';
3
4const validateUser = customValidator(async (data: any) => {
5  if (!data.name || typeof data.name !== 'string') {
6    throw new Error('Name is required and must be a string');
7  }
8  if (!data.email || !data.email.includes('@')) {
9    throw new Error('Valid email is required');
10  }
11  return {
12    name: data.name.trim(),
13    email: data.email.toLowerCase(),
14    validated: true
15  };
16}, 'user-validator');
17
18app.post('/users', {
19  body: validateUser,
20  handler: ({ body }) => {
21    return { success: true, user: body };
22  }
23  });

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 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 with Refinements

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

Schema Composition and Reuse

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

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

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