Validation & Schemas

If you are coming from frameworks like Fastify, you are likely used to writing validation schemas using verbose JSON Schema strings and relying on external dependencies like AJV.

ExisJS takes a drastically more modern and strictly-typed approach. It includes a native, blazingly fast validation engine out of the box, heavily inspired by Zod. You don't need any external dependencies, and you don't need to write error-prone string references (like $ref: '#address').

The Native Validator (v)

ExisJS exports a global validator object v that allows you to fluently build complex validation schemas using pure, typesafe JavaScript.

example.ts
import { v } from 'exisjs/validator'
// Build an incredibly strict schema intuitivelyconst userSchema = v.object({  name: v.string().min(3),  age: v.number().optional().default(18),  role: v.enum(['admin', 'user']),  email: v.string().refine((val) => val.includes('@'), 'Must be a valid email')})
// You can natively parse data directly!const safeData = userSchema.parse(rawInput)

Validation Primitives

ExisJS supports a comprehensive set of strict validation primitives:

  • v.string() - String validation (supports .min(), .max(), .email(), .regex()).
  • v.number() - Strict number validation. It handles smart coercion (like converting "123" from a query string into 123).
  • v.boolean() - Boolean validation (understands "true" and "false" from URLs).
  • v.array(schema) - Validates an array of items.
  • v.object({ ... }) - Validates structured JSON objects.
  • v.enum(['A', 'B']) - Ensures the value matches one of the provided exact strings.
  • v.date() - Parses string, number, or Date inputs into a valid Date object.
  • v.record(schema) - Validates an object with arbitrary keys but specific value types.
  • v.literal('exact') - Validates an exact primitive value.
  • v.union([schema1, schema2]) - Validates if the value matches at least one of the schemas.
  • v.file() - Validates multipart file uploads natively!
  • v.any() - Accepts any value without validation.

Advanced Features

ExisJS validation isn't just about basic types. It comes packed with advanced features to handle complex real-world use cases seamlessly.

Schema Composition

You can construct powerful shared schemas easily using composition methods (.partial(), .omit(), .pick()). This prevents schema duplication.

example.ts
const UserSchema = v.object({  id: v.string(),  name: v.string(),  email: v.string().email(),  password: v.string().min(8)})
// Create an update schema (all fields optional)const UpdateUserSchema = UserSchema.partial()
// Create a safe response schema (omit password)const PublicUserSchema = UserSchema.omit(['password'])
// Create a specialized schemaconst LoginSchema = UserSchema.pick(['email', 'password'])

Asynchronous Validation

Need to check a database during validation? No problem! Use .refineAsync() to run complex async logic seamlessly within your validation schemas. The ExisJS router will automatically wait for the async checks to resolve before executing your route handler.

example.ts
const registerSchema = v.object({  email: v.string()    .email()    .refineAsync(async (val) => {      const exists = await db.users.find({ email: val })      return !exists // Return false if email is taken    }, 'Email is already taken'),  password: v.string().min(8)})

Response Stripping & Serialization

Because ObjectValidator validates by strictly iterating over its defined schema keys, it naturally strips any extra or unknown properties from the resulting object.

This provides immense security out of the box when used as a response schema, ensuring you never accidentally leak sensitive data (like passwords or internal IDs) to the client. The ExisJS router handles this stripping serialization natively behind the scenes.

Separating Schemas

To keep your route files clean and maintainable, we highly recommend defining your schemas in a dedicated schema.ts file alongside your routes.

src/http/users/schema.ts
import { v } from 'exisjs/validator'import type { Infer } from 'exisjs/validator'
export const UserParamsSchema = v.object({  id: v.number().transform(Number)})
export type UserParams = Infer<typeof UserParamsSchema>

Validating Routes

To validate incoming HTTP requests, ExisJS provides built-in validation configurations. Whether you use the Functional paradigm or the Class-based (OOP) paradigm, ExisJS seamlessly validates incoming payloads and strictly types your handlers!

Functional Paradigm

src/http/users/route.ts
import { controller, route } from 'exisjs/router'import { v } from 'exisjs/validator'
export default controller({  create: route.post('/', {    // 1. The validation schema is passed directly in the config!    body: v.object({      username: v.string().min(4),      password: v.string().min(8)    }),    query: v.object({      source: v.string().optional()    }),          // 2. Your handler only runs if validation succeeds!    async handle({ body, res }) {      // body and query are perfectly strongly-typed here!      await db.users.create(body)      res.status(201).json({ success: true })    }  })})

Class-Based (OOP) Paradigm

src/http/api/books/route.ts
import { Controller, Use, Post, Body, Req } from 'exisjs/decorators'import { v } from 'exisjs/validator'import type { Infer } from 'exisjs/validator'
// 1. Define your schemaconst CreateBookSchema = v.object({  title: v.string(),  caption: v.string(),  rating: v.number(),  image: v.string(),})
// 2. Infer the TypeScript type!type CreateBookDto = Infer<typeof CreateBookSchema>
@Use(protectRoute)@Controller()export default class BooksController {    // 3. Inject the schema directly into the @Body() decorator!  @Post('/')  async create(@Body(CreateBookSchema) body: CreateBookDto, @Req() req: any) {    // If validation fails, this block is never reached!    // body is strictly typed as CreateBookDto        const newBook = new Book({      title: body.title,      caption: body.caption,      rating: body.rating,      image: body.image,      user: req.user._id,    })
    await newBook.save()    return newBook  }}

Error Handling

If a request fails validation (e.g., missing a required body field), the validation engine will instantly catch it and return a standardized 400 Bad Request JSON response to the client.

Response
{  "statusCode": 400,  "error": "Bad Request",  "message": "Validation Error: body.username: Required, body.password: Must be at least 8 characters"}

Because this is built right into the framework, you don't have to manually catch errors or construct complex schema error formatters. It just works exactly as you expect!

Third-Party Validators (Zod, Yup)

Do you absolutely love Zod or Yup and want to use them instead of the native v validator? No problem! ExisJS routing fully supports third-party validators.

Even better, if validation fails using a third-party library, ExisJS will intercept the custom error formats (like ZodError or ValidationError) and normalize them perfectly into the exact same 400 Bad Request structure shown above. It behaves exactly as if you used the native validator, ensuring your API error responses are always consistent for your frontend clients!