Input validation system with support for Joi and other validation libraries for request payload, query parameters, and headers in @hapi/hapi.
Configure validation for different parts of the HTTP request including payload, query parameters, path parameters, and headers.
interface ValidationOptions {
/** Validate request headers */
headers?: object | boolean;
/** Validate path parameters */
params?: object | boolean;
/** Validate query parameters */
query?: object | boolean;
/** Validate request payload */
payload?: object | boolean;
/** Validate cookie state */
state?: object | boolean;
/** Action to take when validation fails */
failAction?: ValidationFailAction;
/** Validation library options */
options?: object;
/** Custom error messages */
errorFields?: object;
}
type ValidationFailAction = 'error' | 'log' | 'ignore' | Function;Validate request payload data with comprehensive schema definitions.
// Payload validation using Joi schema
const Joi = require('joi');
interface PayloadValidationSchema {
/** Required fields validation */
[key: string]: any;
}Usage Examples:
const Joi = require('joi');
// Basic payload validation
server.route({
method: 'POST',
path: '/users',
options: {
validate: {
payload: Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(18).max(120).optional(),
address: Joi.object({
street: Joi.string().required(),
city: Joi.string().required(),
zipCode: Joi.string().pattern(/^\d{5}$/).required()
}).optional()
})
}
},
handler: async (request, h) => {
const user = await database.createUser(request.payload);
return h.response(user).code(201);
}
});
// Complex payload validation
server.route({
method: 'POST',
path: '/orders',
options: {
validate: {
payload: Joi.object({
customerId: Joi.string().uuid().required(),
items: Joi.array().items(
Joi.object({
productId: Joi.string().uuid().required(),
quantity: Joi.number().integer().min(1).required(),
price: Joi.number().precision(2).positive().required()
})
).min(1).required(),
shippingAddress: Joi.object({
name: Joi.string().required(),
street: Joi.string().required(),
city: Joi.string().required(),
state: Joi.string().length(2).required(),
zipCode: Joi.string().pattern(/^\d{5}(-\d{4})?$/).required()
}).required(),
paymentMethod: Joi.string().valid('credit_card', 'paypal', 'bank_transfer').required()
})
}
},
handler: async (request, h) => {
const order = await orderService.createOrder(request.payload);
return h.response(order).code(201);
}
});Validate URL query string parameters with type conversion and constraints.
Usage Examples:
// Query parameter validation
server.route({
method: 'GET',
path: '/users',
options: {
validate: {
query: Joi.object({
page: Joi.number().integer().min(1).default(1),
limit: Joi.number().integer().min(1).max(100).default(10),
sort: Joi.string().valid('name', 'email', 'created_at').default('created_at'),
order: Joi.string().valid('asc', 'desc').default('desc'),
search: Joi.string().min(2).max(50).optional(),
active: Joi.boolean().default(true),
role: Joi.array().items(Joi.string().valid('admin', 'user', 'moderator')).single().optional()
})
}
},
handler: async (request, h) => {
const { page, limit, sort, order, search, active, role } = request.query;
const users = await database.getUsers({
page,
limit,
sort,
order,
search,
active,
role
});
return users;
}
});
// Advanced query validation with custom logic
server.route({
method: 'GET',
path: '/analytics',
options: {
validate: {
query: Joi.object({
startDate: Joi.date().iso().required(),
endDate: Joi.date().iso().min(Joi.ref('startDate')).required(),
metrics: Joi.array().items(
Joi.string().valid('views', 'clicks', 'conversions', 'revenue')
).min(1).required(),
groupBy: Joi.string().valid('day', 'week', 'month').default('day'),
timezone: Joi.string().default('UTC')
}).custom((value, helpers) => {
const { startDate, endDate } = value;
const daysDiff = (new Date(endDate) - new Date(startDate)) / (1000 * 60 * 60 * 24);
if (daysDiff > 365) {
return helpers.error('custom.dateRange', { maxDays: 365 });
}
return value;
})
}
},
handler: async (request, h) => {
const analytics = await analyticsService.getMetrics(request.query);
return analytics;
}
});Validate URL path parameters with type checking and format validation.
Usage Examples:
// Path parameter validation
server.route({
method: 'GET',
path: '/users/{id}',
options: {
validate: {
params: Joi.object({
id: Joi.string().uuid().required()
})
}
},
handler: async (request, h) => {
const user = await database.getUser(request.params.id);
if (!user) {
return h.response({ error: 'User not found' }).code(404);
}
return user;
}
});
// Multiple path parameters
server.route({
method: 'GET',
path: '/users/{userId}/posts/{postId}',
options: {
validate: {
params: Joi.object({
userId: Joi.string().uuid().required(),
postId: Joi.alternatives().try(
Joi.string().uuid(),
Joi.string().alphanum().min(5).max(20)
).required()
})
}
},
handler: async (request, h) => {
const { userId, postId } = request.params;
const post = await database.getUserPost(userId, postId);
return post;
}
});
// Path parameter with custom validation
server.route({
method: 'GET',
path: '/files/{path*}',
options: {
validate: {
params: Joi.object({
path: Joi.string().custom((value, helpers) => {
// Prevent directory traversal
if (value.includes('..') || value.includes('~')) {
return helpers.error('custom.invalidPath');
}
return value;
}).required()
})
}
},
handler: async (request, h) => {
const filePath = path.join('./uploads', request.params.path);
return h.file(filePath);
}
});Validate HTTP headers including custom headers and standard headers.
Usage Examples:
// Header validation
server.route({
method: 'POST',
path: '/api/data',
options: {
validate: {
headers: Joi.object({
'content-type': Joi.string().valid('application/json').required(),
'x-api-key': Joi.string().alphanum().length(32).required(),
'user-agent': Joi.string().required(),
'x-request-id': Joi.string().uuid().optional(),
'x-client-version': Joi.string().pattern(/^\d+\.\d+\.\d+$/).optional()
}).unknown(true) // Allow other headers
}
},
handler: async (request, h) => {
const apiKey = request.headers['x-api-key'];
const requestId = request.headers['x-request-id'];
// Process with validated headers
const result = await processData(request.payload, { apiKey, requestId });
return result;
}
});Validate cookie values and state information.
Usage Examples:
// State validation
server.route({
method: 'GET',
path: '/dashboard',
options: {
validate: {
state: Joi.object({
session: Joi.object({
userId: Joi.string().uuid().required(),
role: Joi.string().valid('admin', 'user').required(),
expires: Joi.date().timestamp().required()
}).required(),
preferences: Joi.object({
theme: Joi.string().valid('light', 'dark').default('light'),
language: Joi.string().length(2).default('en')
}).optional()
})
}
},
handler: async (request, h) => {
const { session, preferences } = request.state;
const dashboard = await getDashboard(session.userId, preferences);
return dashboard;
}
});Create custom validation logic for complex scenarios.
Usage Examples:
// Custom validation function
const validateBusinessRules = (value, helpers) => {
const { startDate, endDate, eventType } = value;
// Business rule: Premium events require at least 30 days notice
if (eventType === 'premium') {
const daysNotice = (new Date(startDate) - new Date()) / (1000 * 60 * 60 * 24);
if (daysNotice < 30) {
return helpers.error('custom.premiumEventNotice', { minDays: 30 });
}
}
// Business rule: Events cannot span more than 7 days
const eventDuration = (new Date(endDate) - new Date(startDate)) / (1000 * 60 * 60 * 24);
if (eventDuration > 7) {
return helpers.error('custom.eventTooLong', { maxDays: 7 });
}
return value;
};
server.route({
method: 'POST',
path: '/events',
options: {
validate: {
payload: Joi.object({
title: Joi.string().min(5).max(100).required(),
description: Joi.string().max(1000).optional(),
startDate: Joi.date().iso().min('now').required(),
endDate: Joi.date().iso().min(Joi.ref('startDate')).required(),
eventType: Joi.string().valid('basic', 'premium').required(),
attendeeLimit: Joi.number().integer().min(1).max(1000).required()
}).custom(validateBusinessRules)
}
},
handler: async (request, h) => {
const event = await eventService.createEvent(request.payload);
return h.response(event).code(201);
}
});Handle validation errors with custom error messages and responses.
interface ValidationFailActionFunction {
(request: Request, h: ResponseToolkit, error: Error): any;
}Usage Examples:
// Custom validation error handler
const customValidationHandler = (request, h, error) => {
const details = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message,
value: detail.context?.value
}));
return h.response({
error: 'Validation Failed',
statusCode: 400,
details: details,
timestamp: new Date().toISOString(),
path: request.path
}).code(400).takeover();
};
server.route({
method: 'POST',
path: '/users',
options: {
validate: {
payload: Joi.object({
name: Joi.string().min(2).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).required()
}),
failAction: customValidationHandler
}
},
handler: async (request, h) => {
const user = await userService.createUser(request.payload);
return h.response(user).code(201);
}
});
// Different fail actions per validation type
server.route({
method: 'GET',
path: '/search',
options: {
validate: {
query: Joi.object({
q: Joi.string().min(2).required(),
category: Joi.string().valid('all', 'posts', 'users').default('all')
}),
failAction: 'log' // Just log query validation errors, don't fail request
}
},
handler: async (request, h) => {
// Will use default values for invalid query params
const results = await searchService.search(request.query);
return results;
}
});Set global validation settings and custom validators.
/**
* Set validation engine for the server
* @param validator - Validation library (Joi, etc.)
*/
validator(validator: object): void;
/**
* Set validation rules processor
* @param processor - Rules processing function
* @param options - Processor options
*/
rules(processor: Function, options?: object): void;Usage Examples:
const Joi = require('joi');
// Set global validation engine
server.validator(Joi);
// Custom validation rules
server.rules((schema, options) => {
// Custom preprocessing of validation schemas
return schema.options({
allowUnknown: options.allowUnknown !== false,
stripUnknown: true,
abortEarly: false
});
});
// Global validation defaults in server options
const server = Hapi.server({
port: 3000,
routes: {
validate: {
options: {
abortEarly: false,
stripUnknown: true
},
failAction: (request, h, error) => {
console.log('Validation error:', error.details);
throw error;
}
}
}
});interface ValidationError extends Error {
/** Validation error details */
details: ValidationErrorDetail[];
/** Whether error is a validation error */
isJoi: boolean;
}
interface ValidationErrorDetail {
/** Error message */
message: string;
/** Field path that failed validation */
path: (string | number)[];
/** Validation error type */
type: string;
/** Validation context */
context?: {
value?: any;
key?: string;
label?: string;
[key: string]: any;
};
}
interface ValidateRouteOptions {
/** Header validation schema */
headers?: object | boolean;
/** Parameter validation schema */
params?: object | boolean;
/** Query validation schema */
query?: object | boolean;
/** Payload validation schema */
payload?: object | boolean;
/** State validation schema */
state?: object | boolean;
/** Validation failure action */
failAction?: ValidationFailAction;
/** Validation options */
options?: object;
}