Core Concepts
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.
Overview
Don't like Zod? Use Joi. Prefer Yup? That works too. Same API, your choice of library.
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.
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
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 }));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
1import { createApp } from '@morojs/moro';
2import { z } from 'zod';
3
4app.post('/users')
5 .validate({
6 body: z.object({
7 name: z.string().min(1),
8 email: z.string().email(),
9 age: z.number().min(18)
10 })
11 })
12 .handler((req) => {
13 const body = req.body;
14 return { success: true, user: body };
15 });1import { createApp } from '@morojs/moro';
2import { joi } from '@morojs/moro';
3import Joi from 'joi';
4
5app.post('/users')
6 .validate({
7 body: joi(Joi.object({
8 name: Joi.string().required(),
9 email: Joi.string().email().required(),
10 age: Joi.number().min(18)
11 }))
12 })
13 .handler((req) => {
14 return { success: true, user: req.body };
15 });1import { createApp } from '@morojs/moro';
2import { yup } from '@morojs/moro';
3import * as yupLib from 'yup';
4
5app.post('/users')
6 .validate({
7 body: yup(yupLib.object({
8 name: yupLib.string().required(),
9 email: yupLib.string().email().required(),
10 age: yupLib.number().min(18)
11 }))
12 })
13 .handler((req) => {
14 return { success: true, user: req.body };
15 });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 .validate({ body: classValidator(CreateUserDto) })
19 .handler((req) => {
20 return { success: true, user: req.body };
21 });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 }));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 }));Advanced Patterns
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});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]);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});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});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 }));Error Handling
MoroJS automatically handles validation errors and returns structured error responses.
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}1// Pass schemas and onValidationError together in one .validate() call.
2app.post('/users')
3 .validate({
4 body: CreateUserSchema,
5
6 // Shapes the 4xx the client sees when a schema rejects the request
7 onValidationError: (errors) => ({
8 status: 422,
9 body: {
10 error: 'CUSTOM_VALIDATION_ERROR',
11 message: 'Your request is invalid',
12 fields: errors.map(err => ({
13 field: err.field,
14 issue: err.message
15 }))
16 }
17 })
18 })
19 .handler((req) => {
20 try {
21 // Your logic here
22 return { success: true };
23 } catch (error) {
24 return {
25 status: 400,
26 body: {
27 error: 'BUSINESS_LOGIC_ERROR',
28 message: error.message
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