Nexus NF

Data Validation

Learn how to validate incoming data with Zod schemas in NexusNF

Validation

NexusNF provides built-in request validation using Zod, a TypeScript-first schema validation library. When a schema is provided, incoming data is automatically validated before reaching your handler.

Why Validation Matters

Input data validation ensures that your endpoints receive data in the expected format, preventing runtime errors and improving security:

  • Type Safety - Catch type mismatches before processing
  • Data Integrity - Ensure required fields are present
  • Security - Reject malformed or malicious input
  • Better Error Messages - Provide clear feedback to clients

Validation happens automatically before your handler is called. Invalid data never reaches your business logic.

Basic Validation

Define a Zod schema and pass it to the @Endpoint decorator:

import { ControllerBase, Endpoint } from 'nexusnf';
import { z } from 'zod';

const createUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().min(18)
});

export class UserController extends ControllerBase {
  @Endpoint('create', { schema: createUserSchema })
  async createUser(data: z.infer<typeof createUserSchema>) {
    // data is guaranteed to be valid here
    return {
      id: 123,
      name: data.name,
      email: data.email,
      age: data.age
    };
  }
}

Validation Error Response

When validation fails, NexusNF automatically returns a structured error response:

{
  "error": true,
  "code": "400",
  "message": "Bad Request: Validation failed.",
  "details": [
    {
      "code": "invalid_type",
      "expected": "string",
      "received": "number",
      "path": ["name"],
      "message": "Expected string, received number"
    },
    {
      "code": "invalid_string",
      "validation": "email",
      "path": ["email"],
      "message": "Invalid email"
    }
  ]
}

Validation errors return HTTP 400 (Bad Request) status code. The details array contains all validation issues found.

Read more about more advanced schema patterns on the Zod API Documentation.

Schema Organization

Separate Schema Files

Keep schemas organized in dedicated files:

// schemas/user.schema.ts
import { z } from 'zod';

export const createUserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().min(18).max(120)
});

export const updateUserSchema = createUserSchema.partial().extend({
  id: z.number().int().positive()
});

export const findUserSchema = z.object({
  id: z.number().int().positive()
});

export type CreateUserInput = z.infer<typeof createUserSchema>;
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
export type FindUserInput = z.infer<typeof findUserSchema>;
// controllers/user.controller.ts
import { ControllerBase, Endpoint } from 'nexusnf';
import {
  createUserSchema,
  updateUserSchema,
  findUserSchema,
  type CreateUserInput,
  type UpdateUserInput,
  type FindUserInput
} from '../schemas/user.schema';

export class UserController extends ControllerBase {
  @Endpoint('create', { schema: createUserSchema })
  async createUser(data: CreateUserInput) {
    return { id: 123, ...data };
  }

  @Endpoint('update', { schema: updateUserSchema })
  async updateUser(data: UpdateUserInput) {
    return { id: data.id, updated: true };
  }

  @Endpoint('find', { schema: findUserSchema })
  async findUser(data: FindUserInput) {
    return { id: data.id, name: 'John Doe' };
  }
}

Reusable Schema Components

Build complex schemas from reusable components:

// schemas/common.schema.ts
import { z } from 'zod';

export const idSchema = z.number().int().positive();
export const emailSchema = z.string().email().toLowerCase();
export const timestampSchema = z.string().datetime();
export const paginationSchema = z.object({
  page: z.number().int().min(1).default(1),
  limit: z.number().int().min(1).max(100).default(20)
});

// schemas/user.schema.ts
import { idSchema, emailSchema, paginationSchema } from './common.schema';

export const userSchema = z.object({
  id: idSchema,
  name: z.string().min(1),
  email: emailSchema
});

export const listUsersSchema = paginationSchema.extend({
  role: z.enum(['user', 'admin']).optional(),
  search: z.string().optional()
});

Validation Best Practices

1. Be Specific with Error Messages

const userSchema = z.object({
  name: z.string()
    .min(1, 'Name is required')
    .max(100, 'Name must be less than 100 characters'),
  email: z.string()
    .email('Invalid email address'),
  age: z.number()
    .min(18, 'You must be at least 18 years old')
    .max(120, 'Please enter a valid age')
});

2. Use Enums for Fixed Values

const orderSchema = z.object({
  status: z.enum(['pending', 'processing', 'completed', 'cancelled']),
  priority: z.enum(['low', 'medium', 'high']),
  paymentMethod: z.enum(['card', 'paypal', 'crypto'])
});

3. Validate Business Rules - Be as strict as possible

const bookingSchema = z.object({
  checkIn: z.string().datetime(),
  checkOut: z.string().datetime(),
  guests: z.number().int().min(1).max(10)
}).refine(data => {
  const checkIn = new Date(data.checkIn);
  const checkOut = new Date(data.checkOut);
  return checkOut > checkIn;
}, {
  message: 'Check-out date must be after check-in date',
  path: ['checkOut']
});

4. Use Coercion Carefully

// Coerce string to number
const querySchema = z.object({
  userId: z.coerce.number().int().positive(),
  includeDeleted: z.coerce.boolean().optional()
});

// Input: { userId: "123", includeDeleted: "true" }
// Parsed: { userId: 123, includeDeleted: true }

Be careful with coercion - it can hide data quality issues. Use explicit transforms when you need control over the conversion logic.

Performance Considerations

Schema Compilation

Define schemas outside of your class to avoid recompilation:

// ✅ Schema defined once
const userSchema = z.object({
  name: z.string(),
  email: z.string().email()
});

export class UserController extends ControllerBase {
  @Endpoint('create', { schema: userSchema })
  async createUser(data: z.infer<typeof userSchema>) {
    return { id: 123, ...data };
  }
}
// ❌ Schema created on every endpoint registration
export class UserController extends ControllerBase {
  @Endpoint('create', {
    schema: z.object({
      name: z.string(),
      email: z.string().email()
    })
  })
  async createUser(data: any) {
    return { id: 123, ...data };
  }
}

Next Steps