Node / Express Guide
Validation
Joi schema patterns for request validation.
All request inputs are validated with Joi before reaching service logic. Validation lives in src/validations/.
Base Helpers — local.validation.ts
Reusable Joi primitives shared across schema files:
// src/validations/local.validation.ts
import Joi from 'joi'
// MongoDB ObjectId validator
export const mongoId = (label = 'ID') =>
Joi.string()
.hex()
.length(24)
.messages({
'string.hex': `"${label}" must be a valid hexadecimal string.`,
'string.length': `"${label}" must be 24 characters long.`,
})
export const commonSchemas = {
email: Joi.string().email().lowercase().trim().messages({
'string.email': '"email" must be a valid email address.',
}),
password: Joi.string()
.min(8)
.max(128)
.pattern(/^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])/)
.messages({
'string.min': '"password" must be at least 8 characters.',
'string.pattern.base': '"password" must contain uppercase, a number, and a special character.',
}),
pagination: Joi.object({
page: Joi.number().integer().min(1).default(1),
limit: Joi.number().integer().min(1).max(100).default(10),
}),
name: Joi.string().min(1).max(100).trim(),
phone: Joi.string().trim(),
}
Schema Files
One schema file per controller/route. Each exports a Joi.ObjectSchema named <action>Schema and an inferred TypeScript type:
// src/validations/auth.validation.ts
import Joi from 'joi'
import { mongoId, commonSchemas } from './local.validation'
export const loginSchema = Joi.object({
email: commonSchemas.email.required(),
password: Joi.string().min(1).required(),
organizationId: mongoId('Organization ID').optional().allow(null, ''),
})
export const registerSchema = Joi.object({
firstName: commonSchemas.name.required(),
lastName: commonSchemas.name.required(),
email: commonSchemas.email.required(),
phone: commonSchemas.phone.required(),
password: commonSchemas.password.required(),
organizationId: mongoId('Organization ID').optional(),
})
export type TLoginInput = Joi.extractType<typeof loginSchema>
export type TRegisterInput = Joi.extractType<typeof registerSchema>
Resource Validation Example
// src/validations/resource.validation.ts
import Joi from 'joi'
import { mongoId, commonSchemas } from './local.validation'
export const createResourceSchema = Joi.object({
name: commonSchemas.name.required(),
description: Joi.string().max(500).optional(),
organizationId: mongoId('Organization ID').optional(),
})
export const updateResourceSchema = Joi.object({
name: commonSchemas.name.optional(),
description: Joi.string().max(500).optional(),
}).min(1) // require at least one field on updates
export type TCreateResourceInput = Joi.extractType<typeof createResourceSchema>
export type TUpdateResourceInput = Joi.extractType<typeof updateResourceSchema>
Using Schemas in Controllers
async function create(req, res, next) {
const { error, value } = createResourceSchema.validate(req.body, { abortEarly: false })
if (error) return next(new UnprocessableEntityError(error.message))
const result = await service.create(value) // value is fully typed
res.status(201).json(result)
}
Rules
abortEarly: false— collect all validation errors, not just the first.- Use
mongoId()for every ObjectId parameter — never trust raw string IDs. - Export inferred types (
TLoginInput) from schema files so services are typed without duplicating shapes. - Lowercase + trim email fields at the Joi level — not in the service or model.
- Validate at the controller — not in the route file or the service.
.min(1)on update schemas — require at least one field to prevent empty PATCH requests.