ExisJS

Validation & Schemas

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

ExisJS takes a strictly-typed approach. It includes a native validation engine out of the box, powered by Rust for maximum performance. You don't need any external dependencies, and you avoid string-based references.

The Native Validator (tex)

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

example.ts
import { tex } from 'exisjs/validator'
// Build a strict schema intuitivelyconst userSchema = tex.object({  name: tex.string({ min: 3 }),  age: tex.number({ optional: true }),  role: tex.enum(['admin', 'user']),  email: tex.email()})
// You can natively parse data directly!const safeData = userSchema.parse(rawInput)

Validation Primitives

ExisJS supports a comprehensive set of strict validation primitives:

  • tex.string() - String validation (supports options like { min: 5, max: 10 }).
  • tex.email() - Validates standard email formatting.
  • tex.number() - Strict number validation (fails if passed a string).
  • tex.boolean() - Strict boolean validation.
  • tex.number({ coerce: true }) - Automatically coerces incoming string representations into native JavaScript numbers.
  • tex.boolean({ coerce: true }) - Coerces "true"/"1" and "false"/"0" into native booleans.
  • tex.array(schema) - Validates an array of items.
  • tex.object({ ... }) - Validates structured JSON objects.
  • tex.enum(['A', 'B']) - Ensures the value matches one of the provided exact strings.

All primitives accept an options object where you can pass { optional: true } to allow undefined.

Data Sanitization (Trimming & Formatting)

ExisJS allows you to mutate and sanitize incoming data in real-time before it reaches your controller. It includes a native Rust-powered sanitization engine for Strings and Arrays.

example.ts
import { tex } from 'exisjs/validator'import { sanitize } from 'exisjs/sanitize'
const loginSchema = tex.object({  // Automatically trim whitespace, convert to lowercase, and prevent HTML injections  email: tex.email().sanitize(sanitize.trim, sanitize.toLowerCase, sanitize.escapeHtml),  password: tex.string({ min: 8 })})
const articleSchema = tex.object({  title: tex.string().sanitize(sanitize.collapseWhitespace, sanitize.truncate(100)),  // Auto-generate slugs from the title using sanitize.slugify!  slug: tex.string().sanitize(sanitize.slugify),  tags: tex.array(tex.string()).sanitize(sanitize.dedupe, sanitize.compact)})

Standalone Sanitization Usage

Because the sanitization engine is designed as a collection of pure, independent functions, you don't have to use it with the Validator. If you ever need to manually sanitize data deep inside your services, background jobs, or generic Node.js scripts, you can import the engine directly:

src/services/formatter.ts
import { sanitize } from 'exisjs/sanitize'
export function formatArticleTitle(rawInput: string) {  let clean = sanitize.stripHtml(rawInput)  clean = sanitize.collapseWhitespace(clean)  return sanitize.trim(clean)}

Schema Composition & Features

ExisJS validation provides robust features to handle real-world use cases efficiently and seamlessly.

Schema Composition

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

example.ts
const UserSchema = tex.object({  id: tex.string(),  name: tex.string(),  email: tex.email(),  password: tex.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'])
// Chain multiple schemas using tex.union()const FlexibleIdSchema = tex.union([tex.string(), tex.number()])

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 = tex.object({  email: tex.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: tex.string({ min: 8 })})

Response Stripping & Serialization

Because TexEngine 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 { tex } from 'exisjs/validator'import type { ResolveSchema } from 'exisjs/validator'
export const UserParamsSchema = tex.object({  id: tex.number({ coerce: true })})
export type UserParams = ResolveSchema<typeof UserParamsSchema>

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

Functional Paradigm

src/http/users/route.ts
import { controller, route } from 'exisjs/router'import { tex } from 'exisjs/validator'
export default controller({  create: route.post('/', {    // 1. The validation schema is passed directly in the config    body: tex.object({      username: tex.string({ min: 4 }),      password: tex.string({ min: 8 })    }),    query: tex.object({      source: tex.string({ optional: true })    }),          // 2. Your handler only runs if validation succeeds    async handle({ body, query, req, res }) {      // body and query are 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 { tex } from 'exisjs/validator'import type { ResolveSchema } from 'exisjs/validator'
// 1. Define your schemaconst CreateBookSchema = tex.object({  title: tex.string(),  caption: tex.string(),  rating: tex.number(),  image: tex.string(),})
// 2. Infer the TypeScript type!type CreateBookDto = ResolveSchema<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 is required"}

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!