Validation¶
Katal's schema-based validation runs automatically before handle() is called. If validation fails the controller is never invoked and a 422 Unprocessable Entity response (RFC 9457 Problem Details) is returned.
You can define validation in two places:
- Controller-level schema (protected schema) inside a controller.
- Route-level schema (options.validation) when registering a route.
When both are present, Katal merges them per request: - Route-level rules override controller-level rules for overlapping fields. - Non-overlapping fields from both schemas are included.
Basic Usage¶
import { Controller, ValidationSchema } from 'katal';
class CreateUserController extends Controller {
// Define validation schema - validation happens automatically
protected schema: ValidationSchema = {
name: {
required: true,
type: 'string',
minLength: 2
},
email: {
required: true,
type: 'email'
},
age: {
type: 'number',
min: 18
}
};
async handle(context: RequestContext) {
// At this point, validation has already passed
// and context.body contains the validated data
const { name, email, age } = context.body;
// Create user with validated data
const user = await this.createUser({ name, email, age });
return this.success(user);
}
}
How It Works¶
When a request is handled by a controller:
- The framework checks if the controller has a
schemaproperty - The framework checks if the route has an
options.validationschema - If both exist, schemas are merged (
routeoverridescontrolleron matching keys) - The request body is validated against the effective schema
- If validation fails, a 422 Problem Details response is returned automatically
- If validation passes, the
handlemethod is called with validated data incontext.body
Route-level Validation (Registration-Time)¶
router.post('/users', CreateUserController, {
validation: {
email: { required: true, type: 'email' },
password: { required: true, type: 'string', minLength: 8 }
}
});
Merge Example¶
class CreateUserController extends Controller {
protected schema: ValidationSchema = {
email: { required: true, type: 'string' },
age: { type: 'number', min: 13 }
};
}
router.post('/users', CreateUserController, {
validation: {
// overrides controller email rule
email: { required: true, type: 'email' },
// adds an extra field
name: { required: true, type: 'string' }
}
});
Effective schema for this route includes:
- email from route-level schema (override)
- age from controller schema
- name from route-level schema
Manual Validation¶
You can also validate data manually using the validateRequest method:
class CustomController extends Controller {
async handle(context: RequestContext) {
const schema: ValidationSchema = {
id: { required: true, type: 'number' }
};
// Returns null if valid, or a Response with validation errors
const validationError = this.validateRequest(context, schema);
if (validationError) {
return validationError;
}
// Validation passed
const { id } = context.body;
// ... continue processing
}
}
Validation Types¶
ValidationRule Interface¶
interface ValidationRule {
required?: boolean;
type?: "string" | "number" | "boolean" | "email" | "url" | "array" | "object";
min?: number;
max?: number;
minLength?: number;
maxLength?: number;
pattern?: RegExp;
custom?: (value: any) => boolean | string;
enum?: any[];
schema?: ValidationSchema;
}
interface ValidationSchema {
[key: string]: ValidationRule;
}
Basic Type Validation¶
const schema: ValidationSchema = {
// String validation
username: {
type: 'string',
required: true,
minLength: 3
},
// Number validation
age: {
type: 'number',
min: 18,
max: 150
},
// Boolean validation
active: {
type: 'boolean'
}
};
Special Types¶
const schema: ValidationSchema = {
// Email validation (built-in)
email: {
type: 'email',
required: true
},
// URL validation (built-in)
website: {
type: 'url'
}
};
Pattern & Enum Validation¶
const schema: ValidationSchema = {
// Regular expression pattern
username: {
type: 'string',
pattern: /^[a-zA-Z0-9_]+$/
},
// Enum values
role: {
type: 'string',
enum: ['admin', 'user', 'guest']
}
};
Custom Validation¶
const schema: ValidationSchema = {
password: {
type: 'string',
custom: (value) => {
if (!/[A-Z]/.test(value)) {
return 'Password must contain an uppercase letter';
}
if (!/[0-9]/.test(value)) {
return 'Password must contain a number';
}
return true;
}
}
};
Nested Objects¶
const schema: ValidationSchema = {
profile: {
type: 'object',
schema: {
name: { required: true, type: 'string' },
contact: {
type: 'object',
schema: {
email: { type: 'email' },
phone: { type: 'string', pattern: /^\+?[\d\s-]{10,}$/ }
}
}
}
}
};
Array Validation¶
const schema: ValidationSchema = {
// Simple array
tags: {
type: 'array',
minLength: 1
},
// Array with object schema
items: {
type: 'array',
schema: {
name: { required: true, type: 'string' },
price: { type: 'number', min: 0 }
}
}
};
Error Handling¶
interface ValidationError {
field: string; // Field path (includes nested paths)
message: string; // Error message
}
// Example validation response
{
"errors": [
{
"field": "email",
"message": "email is required"
},
{
"field": "profile.contact.phone",
"message": "profile.contact.phone format is invalid"
}
]
}
Best Practices¶
- Use Built-in Types When Available
// Prefer this:
const schema: ValidationSchema = {
email: { type: 'email' }
};
// Over this:
const schema: ValidationSchema = {
email: {
type: 'string',
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
}
};
- Clear Error Messages
const schema: ValidationSchema = {
age: {
type: 'number',
custom: (value) => {
return value >= 21 || 'Must be 21 years or older';
}
}
};
- Type-Safe Schemas
interface CreateUserInput {
username: string;
email: string;
profile: {
firstName: string;
lastName: string;
};
}
const schema: ValidationSchema = {
username: { required: true, type: 'string' },
email: { required: true, type: 'email' },
profile: {
type: 'object',
schema: {
firstName: { required: true, type: 'string' },
lastName: { required: true, type: 'string' }
}
}
} as const;
Validation Rule Reference¶
| Rule | Applies To | Description |
|---|---|---|
required |
all | Field must be present and non-empty |
type |
all | "string", "number", "boolean", "email", "url", "array", "object" |
min / max |
"number" |
Numeric range |
minLength / maxLength |
"string", "array" |
Length range |
pattern |
"string" |
RegExp match |
enum |
any | Value must be in the list |
custom |
any | Returns true or an error message string |
schema |
"object", "array" |
Nested / array-item schema |
See Also¶
- Controllers & HTTP — schema prop on controllers
- Getting Started — validation in context