Skip to content

Validation

Chapter 25: Input Validation & Error Handling

Section titled “Chapter 25: Input Validation & Error Handling”

Proper validation and error handling are crucial for building reliable applications. This chapter covers both input validation and comprehensive error handling.

Validation & Error Handling Flow
================================================================================
HTTP Request
|
v
+------------------+
| Input Check | ----> Validate content type
+------------------+
|
v
+------------------+
| DTO Validation | ----> Validate required fields
+------------------+ Validate types
| Validate ranges
v Validate formats
+------------------+
| Business Logic | ----> Validate business rules
+------------------+
|
v
+------------------+
| Database | ----> Validate constraints
+------------------+
|
v
HTTP Response
================================================================================

src/utils/validation.util.ts
// String validators
export function isString(value: any): value is string {
return typeof value === 'string';
}
export function isNotEmpty(value: any): boolean {
return isString(value) && value.trim().length > 0;
}
export function minLength(value: any, min: number): boolean {
return isString(value) && value.length >= min;
}
export function maxLength(value: any, max: number): boolean {
return isString(value) && value.length <= max;
}
export function isEmail(value: any): boolean {
if (!isString(value)) return false;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value);
}
export function isUrl(value: any): boolean {
if (!isString(value)) return false;
try {
new URL(value);
return true;
} catch {
return false;
}
}
// Number validators
export function isNumber(value: any): value is number {
return typeof value === 'number' && !isNaN(value);
}
export function isInteger(value: any): boolean {
return isNumber(value) && Number.isInteger(value);
}
export function minValue(value: any, min: number): boolean {
return isNumber(value) && value >= min;
}
export function maxValue(value: any, max: number): boolean {
return isNumber(value) && value <= max;
}
// Array validators
export function isArray(value: any): value is any[] {
return Array.isArray(value);
}
export function arrayMinLength(value: any, min: number): boolean {
return isArray(value) && value.length >= min;
}
export function arrayMaxLength(value: any, max: number): boolean {
return isArray(value) && value.length <= max;
}
// Date validators
export function isDate(value: any): value is Date {
return value instanceof Date;
}
export function isValidDate(value: any): boolean {
if (isString(value)) {
const date = new Date(value);
return !isNaN(date.getTime());
}
return isDate(value) && !isNaN(value.getTime());
}

src/utils/validation-error.util.ts
export interface ValidationErrorItem {
field: string;
message: string;
value?: any;
}
export interface ValidationResult {
isValid: boolean;
errors: ValidationErrorItem[];
}
export class ValidationError extends Error {
public errors: ValidationErrorItem[];
constructor(errors: ValidationErrorItem[]) {
super('Validation failed');
this.name = 'ValidationError';
this.errors = errors;
}
}
export class ValidationHelper {
private errors: ValidationErrorItem[] = [];
addError(field: string, message: string, value?: any): this {
this.errors.push({ field, message, value });
return this;
}
addErrorIf(
condition: boolean,
field: string,
message: string,
value?: any
): this {
if (condition) {
this.addError(field, message, value);
}
return this;
}
isValid(): boolean {
return this.errors.length === 0;
}
getErrors(): ValidationErrorItem[] {
return this.errors;
}
getResult(): ValidationResult {
return {
isValid: this.isValid(),
errors: this.errors,
};
}
throwIfInvalid(): void {
if (!this.isValid()) {
throw new ValidationError(this.errors);
}
}
}

src/validators/user.validator.ts
import {
isString,
isNotEmpty,
isEmail,
minLength,
maxLength,
isNumber,
minValue,
maxValue,
isArray,
arrayMinLength,
arrayMaxLength,
isValidDate,
} from '../utils/validation.util';
import { ValidationHelper } from '../utils/validation-error.util';
import { CreateUserDto } from '../dto/create-user.dto';
export function validateCreateUserDto(data: any): ValidationHelper {
const v = new ValidationHelper();
// Name validation
v.addErrorIf(
!isNotEmpty(data.name),
'name',
'Name is required'
);
v.addErrorIf(
data.name && !minLength(data.name, 2),
'name',
'Name must be at least 2 characters'
);
v.addErrorIf(
data.name && !maxLength(data.name, 100),
'name',
'Name must not exceed 100 characters'
);
// Email validation
v.addErrorIf(
!isNotEmpty(data.email),
'email',
'Email is required'
);
v.addErrorIf(
data.email && !isEmail(data.email),
'email',
'Invalid email format'
);
// Password validation
v.addErrorIf(
!isNotEmpty(data.password),
'password',
'Password is required'
);
v.addErrorIf(
data.password && !minLength(data.password, 8),
'password',
'Password must be at least 8 characters'
);
v.addErrorIf(
data.password && !maxLength(data.password, 128),
'password',
'Password must not exceed 128 characters'
);
// Age validation (optional)
v.addErrorIf(
data.age !== undefined && !isNumber(data.age),
'age',
'Age must be a number'
);
v.addErrorIf(
data.age && (data.age < 0 || data.age > 150),
'age',
'Age must be between 0 and 150'
);
// Role validation (optional)
const validRoles = ['user', 'admin', 'moderator'];
v.addErrorIf(
data.role && !validRoles.includes(data.role),
'role',
`Role must be one of: ${validRoles.join(', ')}`
);
// Tags validation (optional)
v.addErrorIf(
data.tags && !isArray(data.tags),
'tags',
'Tags must be an array'
);
v.addErrorIf(
data.tags && !arrayMinLength(data.tags, 1),
'tags',
'Tags must contain at least 1 item'
);
v.addErrorIf(
data.tags && !arrayMaxLength(data.tags, 10),
'tags',
'Tags must not exceed 10 items'
);
// Birth date validation (optional)
v.addErrorIf(
data.birthDate && !isValidDate(data.birthDate),
'birthDate',
'Invalid birth date'
);
return v;
}

src/utils/error-handler.util.ts
// Base error classes
export class AppError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public code?: string
) {
super(message);
this.name = 'AppError';
}
}
export class NotFoundError extends AppError {
constructor(message: string = 'Resource not found') {
super(message, 404, 'NOT_FOUND');
this.name = 'NotFoundError';
}
}
export class ValidationError extends AppError {
constructor(message: string = 'Validation failed') {
super(message, 400, 'VALIDATION_ERROR');
this.name = 'ValidationError';
}
}
export class UnauthorizedError extends AppError {
constructor(message: string = 'Unauthorized') {
super(message, 401, 'UNAUTHORIZED');
this.name = 'UnauthorizedError';
}
}
export class ForbiddenError extends AppError {
constructor(message: string = 'Forbidden') {
super(message, 403, 'FORBIDDEN');
this.name = 'ForbiddenError';
}
}
export class ConflictError extends AppError {
constructor(message: string = 'Conflict') {
super(message, 409, 'CONFLICT');
this.name = 'ConflictError';
}
}
export class InternalServerError extends AppError {
constructor(message: string = 'Internal server error') {
super(message, 500, 'INTERNAL_SERVER_ERROR');
this.name = 'InternalServerError';
}
}
// Express error handler middleware
import { Request, Response, NextFunction } from 'express';
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
console.error('Error:', err);
// Handle known errors
if (err instanceof AppError) {
return res.status(err.statusCode).json({
success: false,
error: {
message: err.message,
code: err.code,
},
});
}
// Handle validation errors with multiple messages
if (err.name === 'ValidationError' && (err as any).errors) {
return res.status(400).json({
success: false,
error: {
message: err.message,
code: 'VALIDATION_ERROR',
errors: (err as any).errors,
},
});
}
// Handle unknown errors
return res.status(500).json({
success: false,
error: {
message: 'Internal server error',
code: 'INTERNAL_SERVER_ERROR',
},
});
}
// Async handler wrapper
export function asyncHandler(
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
) {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}

src/controllers/user.controller.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/user.service';
import { validateCreateUserDto } from '../validators/user.validator';
import { asyncHandler } from '../utils/error-handler.util';
import { ApiResponseDto } from '../dto';
export class UserController {
constructor(private userService: UserService) {}
createUser = asyncHandler(async (req: Request, res: Response) => {
// Validate input
const validation = validateCreateUserDto(req.body);
validation.throwIfInvalid();
// Create user
const user = await this.userService.create(req.body);
// Return response
res.status(201).json(
ApiResponseDto.success(user, 'User created successfully')
);
});
getUserById = asyncHandler(async (req: Request, res: Response) => {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json(
ApiResponseDto.error('Invalid user ID')
);
}
const user = await this.userService.findById(id);
if (!user) {
return res.status(404).json(
ApiResponseDto.error('User not found')
);
}
res.json(ApiResponseDto.success(user));
});
}

This chapter covered:

  • Validation Utilities: Reusable validation functions
  • Error Classes: Custom error types for different scenarios
  • Validator Implementation: Complete input validation
  • Error Handling: Middleware for Express.js
  • Best Practices: Using async handlers and proper error responses

Key takeaways:

  1. Always validate input at the entry point
  2. Use custom error classes for different error types
  3. Implement consistent error response format
  4. Log errors for debugging while hiding sensitive info from users

This concludes Part 5: TypeScript Application Structure. You now have a solid foundation for building TypeORM applications with pure TypeScript.


Last Updated: February 2026