数据验证 #

一、验证概述 #

1.1 什么是Joi? #

Joi是Hapi生态系统中的数据验证库,用于验证JavaScript对象的结构和数据类型。

1.2 安装Joi #

bash
npm install @hapi/joi

1.3 基本使用 #

javascript
const Joi = require('@hapi/joi');

server.route({
    method: 'POST',
    path: '/users',
    options: {
        validate: {
            payload: Joi.object({
                name: Joi.string().required(),
                email: Joi.string().email().required()
            })
        }
    },
    handler: (request, h) => {
        return request.payload;
    }
});

二、验证位置 #

2.1 路径参数验证 #

javascript
server.route({
    method: 'GET',
    path: '/users/{id}',
    options: {
        validate: {
            params: Joi.object({
                id: Joi.number().integer().positive().required()
            })
        }
    },
    handler: (request, h) => {
        return { id: request.params.id };
    }
});

2.2 查询参数验证 #

javascript
server.route({
    method: 'GET',
    path: '/search',
    options: {
        validate: {
            query: Joi.object({
                q: Joi.string().required(),
                page: Joi.number().default(1),
                limit: Joi.number().default(10)
            })
        }
    },
    handler: (request, h) => {
        return request.query;
    }
});

2.3 请求体验证 #

javascript
server.route({
    method: 'POST',
    path: '/users',
    options: {
        validate: {
            payload: Joi.object({
                name: Joi.string().min(2).max(50).required(),
                email: Joi.string().email().required(),
                password: Joi.string().min(6).required()
            })
        }
    },
    handler: (request, h) => {
        return request.payload;
    }
});

2.4 请求头验证 #

javascript
server.route({
    method: 'GET',
    path: '/api/data',
    options: {
        validate: {
            headers: Joi.object({
                authorization: Joi.string().required(),
                'x-api-version': Joi.string().valid('1.0', '2.0').required()
            }).options({ allowUnknown: true })
        }
    },
    handler: (request, h) => {
        return { message: 'OK' };
    }
});

2.5 响应验证 #

javascript
server.route({
    method: 'GET',
    path: '/users/{id}',
    options: {
        response: {
            schema: Joi.object({
                id: Joi.number().required(),
                name: Joi.string().required(),
                email: Joi.string().email().required()
            })
        }
    },
    handler: async (request, h) => {
        return await getUser(request.params.id);
    }
});

三、Joi类型 #

3.1 字符串 #

javascript
const schema = Joi.object({
    name: Joi.string().min(2).max(50).required(),
    email: Joi.string().email().required(),
    phone: Joi.string().pattern(/^[0-9]{10,11}$/),
    username: Joi.string().alphanum().min(3).max(30),
    description: Joi.string().allow(''),
    bio: Joi.string().max(500).optional()
});

3.2 数字 #

javascript
const schema = Joi.object({
    age: Joi.number().integer().min(0).max(120),
    price: Joi.number().positive().precision(2),
    quantity: Joi.number().integer().min(0),
    rating: Joi.number().min(0).max(5),
    discount: Joi.number().min(0).max(100).default(0)
});

3.3 布尔值 #

javascript
const schema = Joi.object({
    active: Joi.boolean().required(),
    subscribed: Joi.boolean().default(false),
    terms: Joi.boolean().truthy('yes', '1').falsy('no', '0')
});

3.4 日期 #

javascript
const schema = Joi.object({
    birthDate: Joi.date().required(),
    startDate: Joi.date().min('now'),
    endDate: Joi.date().greater(Joi.ref('startDate')),
    createdAt: Joi.date().iso()
});

3.5 数组 #

javascript
const schema = Joi.object({
    tags: Joi.array().items(Joi.string()).min(1).max(10),
    ids: Joi.array().items(Joi.number()).unique(),
    scores: Joi.array().items(Joi.number().min(0).max(100)),
    items: Joi.array().items(
        Joi.object({
            id: Joi.number().required(),
            name: Joi.string().required()
        })
    )
});

3.6 对象 #

javascript
const schema = Joi.object({
    user: Joi.object({
        name: Joi.string().required(),
        email: Joi.string().email().required(),
        address: Joi.object({
            street: Joi.string().required(),
            city: Joi.string().required(),
            zipCode: Joi.string().required()
        })
    })
});

四、常用验证规则 #

4.1 必填与可选 #

javascript
const schema = Joi.object({
    name: Joi.string().required(),
    nickname: Joi.string().optional(),
    bio: Joi.string().allow(null),
    website: Joi.string().uri().allow('')
});

4.2 默认值 #

javascript
const schema = Joi.object({
    page: Joi.number().default(1),
    limit: Joi.number().default(10),
    sort: Joi.string().default('createdAt'),
    order: Joi.string().valid('asc', 'desc').default('desc')
});

4.3 枚举值 #

javascript
const schema = Joi.object({
    status: Joi.string().valid('active', 'inactive', 'pending'),
    role: Joi.string().valid('admin', 'user', 'guest').default('user'),
    type: Joi.number().valid(1, 2, 3)
});

4.4 正则表达式 #

javascript
const schema = Joi.object({
    phone: Joi.string().pattern(/^[0-9]{10,11}$/),
    zipCode: Joi.string().pattern(/^[0-9]{5}(-[0-9]{4})?$/),
    username: Joi.string().pattern(/^[a-zA-Z][a-zA-Z0-9_]{2,29}$/)
});

4.5 自定义验证 #

javascript
const schema = Joi.object({
    password: Joi.string().custom((value, helpers) => {
        if (value.length < 8) {
            return helpers.error('string.min', { limit: 8 });
        }
        if (!/[A-Z]/.test(value)) {
            return helpers.error('any.custom', { message: '需要包含大写字母' });
        }
        if (!/[0-9]/.test(value)) {
            return helpers.error('any.custom', { message: '需要包含数字' });
        }
        return value;
    })
});

五、条件验证 #

5.1 when条件 #

javascript
const schema = Joi.object({
    type: Joi.string().valid('individual', 'company').required(),
    name: Joi.string().when('type', {
        is: 'individual',
        then: Joi.required()
    }),
    companyName: Joi.string().when('type', {
        is: 'company',
        then: Joi.required()
    })
});

5.2 引用其他字段 #

javascript
const schema = Joi.object({
    password: Joi.string().min(6).required(),
    confirmPassword: Joi.string()
        .valid(Joi.ref('password'))
        .required()
        .messages({
            'any.only': '密码不匹配'
        }),
    startDate: Joi.date().required(),
    endDate: Joi.date().greater(Joi.ref('startDate')).required()
});

5.3 复杂条件 #

javascript
const schema = Joi.object({
    paymentMethod: Joi.string().valid('credit', 'paypal', 'bank').required(),
    cardNumber: Joi.string().when('paymentMethod', {
        is: 'credit',
        then: Joi.string().creditCard().required()
    }),
    bankAccount: Joi.string().when('paymentMethod', {
        is: 'bank',
        then: Joi.string().pattern(/^[0-9]{10,20}$/).required()
    })
});

六、错误消息 #

6.1 自定义错误消息 #

javascript
const schema = Joi.object({
    name: Joi.string()
        .min(2)
        .max(50)
        .required()
        .messages({
            'string.empty': '姓名不能为空',
            'string.min': '姓名至少需要{#limit}个字符',
            'string.max': '姓名不能超过{#limit}个字符',
            'any.required': '姓名是必填字段'
        }),
    email: Joi.string()
        .email()
        .required()
        .messages({
            'string.email': '请输入有效的邮箱地址',
            'any.required': '邮箱是必填字段'
        })
});

6.2 全局错误消息 #

javascript
const schema = Joi.object({
    name: Joi.string().required()
}).messages({
    'any.required': '此字段为必填项',
    'string.empty': '此字段不能为空'
});

七、验证失败处理 #

7.1 默认处理 #

javascript
server.route({
    method: 'POST',
    path: '/users',
    options: {
        validate: {
            payload: Joi.object({
                name: Joi.string().required()
            })
        }
    },
    handler: (request, h) => {
        return request.payload;
    }
});

7.2 自定义失败处理 #

javascript
server.route({
    method: 'POST',
    path: '/users',
    options: {
        validate: {
            payload: Joi.object({
                name: Joi.string().required(),
                email: Joi.string().email().required()
            }),
            failAction: (request, h, err) => {
                const errors = err.details.map(d => ({
                    field: d.path.join('.'),
                    message: d.message
                }));
                return h.response({
                    success: false,
                    errors: errors
                }).code(400).takeover();
            }
        }
    },
    handler: (request, h) => {
        return request.payload;
    }
});

7.3 全局失败处理 #

javascript
const server = Hapi.server({
    port: 3000,
    routes: {
        validate: {
            failAction: async (request, h, err) => {
                console.error('Validation error:', err);
                
                const errors = err.details.map(d => ({
                    field: d.path.join('.'),
                    message: d.message
                }));
                
                return h.response({
                    success: false,
                    errors: errors
                }).code(400).takeover();
            }
        }
    }
});

八、Schema复用 #

8.1 定义公共Schema #

javascript
const schemas = {
    userId: Joi.object({
        id: Joi.number().integer().positive().required()
    }),
    
    pagination: Joi.object({
        page: Joi.number().integer().min(1).default(1),
        limit: Joi.number().integer().min(1).max(100).default(10),
        sort: Joi.string().default('createdAt'),
        order: Joi.string().valid('asc', 'desc').default('desc')
    }),
    
    user: Joi.object({
        name: Joi.string().min(2).max(50).required(),
        email: Joi.string().email().required(),
        password: Joi.string().min(6).required()
    })
};

module.exports = schemas;

8.2 使用公共Schema #

javascript
const schemas = require('./schemas');

server.route({
    method: 'GET',
    path: '/users/{id}',
    options: {
        validate: {
            params: schemas.userId
        }
    },
    handler: getUser
});

server.route({
    method: 'GET',
    path: '/users',
    options: {
        validate: {
            query: schemas.pagination
        }
    },
    handler: getUsers
});

server.route({
    method: 'POST',
    path: '/users',
    options: {
        validate: {
            payload: schemas.user
        }
    },
    handler: createUser
});

九、完整示例 #

9.1 用户API验证 #

javascript
const Joi = require('@hapi/joi');

const userSchemas = {
    create: Joi.object({
        name: Joi.string().min(2).max(50).required()
            .messages({
                'string.min': '姓名至少需要{#limit}个字符',
                'any.required': '姓名是必填字段'
            }),
        email: Joi.string().email().required()
            .messages({
                'string.email': '请输入有效的邮箱地址',
                'any.required': '邮箱是必填字段'
            }),
        password: Joi.string().min(6).max(50).required()
            .messages({
                'string.min': '密码至少需要{#limit}个字符',
                'any.required': '密码是必填字段'
            }),
        age: Joi.number().integer().min(0).max(120)
    }),
    
    update: Joi.object({
        name: Joi.string().min(2).max(50),
        email: Joi.string().email(),
        age: Joi.number().integer().min(0).max(120)
    }).min(1),
    
    id: Joi.object({
        id: Joi.number().integer().positive().required()
    }),
    
    list: Joi.object({
        page: Joi.number().integer().min(1).default(1),
        limit: Joi.number().integer().min(1).max(100).default(10),
        name: Joi.string(),
        email: Joi.string().email()
    })
};

server.route([
    {
        method: 'GET',
        path: '/users',
        options: {
            validate: {
                query: userSchemas.list
            }
        },
        handler: getUsers
    },
    {
        method: 'GET',
        path: '/users/{id}',
        options: {
            validate: {
                params: userSchemas.id
            }
        },
        handler: getUser
    },
    {
        method: 'POST',
        path: '/users',
        options: {
            validate: {
                payload: userSchemas.create
            }
        },
        handler: createUser
    },
    {
        method: 'PUT',
        path: '/users/{id}',
        options: {
            validate: {
                params: userSchemas.id,
                payload: userSchemas.update
            }
        },
        handler: updateUser
    }
]);

十、总结 #

数据验证要点:

验证位置 配置项
路径参数 validate.params
查询参数 validate.query
请求体 validate.payload
请求头 validate.headers
响应 response.schema

下一步,让我们学习Cookie与Session!

最后更新:2026-03-28