Backend

Validation

Joi schema patterns for request validation in booki-api.

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:

// 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(),
})

// Type inference from schema
export type TLoginInput    = Joi.extractType<typeof loginSchema>
export type TRegisterInput = Joi.extractType<typeof registerSchema>

Using Schemas in Controllers

async function login(req: TenantRequest, res: Response, next: NextFunction) {
  const { error, value } = loginSchema.validate(req.body, { abortEarly: false })
  if (error) return next(new UnprocessableEntityError(error.message))

  const session = await _login(value)   // value is fully typed as TLoginInput
  res.json(session)
}

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.