Validation
Kysera uses a smart validation strategy that balances type safety with performance.
Validation Strategy
Input Validation (Always Enabled)
All external inputs are validated using validation adapters:
import { z } from 'zod'
import { createRepositoryFactory, zodAdapter } from '@kysera/repository'
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100)
})
const factory = createRepositoryFactory(db)
const userRepo = factory.create({
tableName: 'users',
mapRow: (row) => row,
schemas: {
create: zodAdapter(CreateUserSchema), // Always validated
update: zodAdapter(CreateUserSchema.partial()) // Always validated
}
})
// Input is validated before database operation
await userRepo.create({
email: 'invalid-email', // Throws validation error
name: 'John'
})
Output Validation (Configurable)
Database results can optionally be validated:
import { zodAdapter } from '@kysera/repository'
const UserSchema = z.object({
id: z.number(),
email: z.string().email(),
name: z.string(),
created_at: z.date(),
updated_at: z.date().nullable()
})
const userRepo = factory.create({
tableName: 'users',
mapRow: (row) => row,
schemas: {
entity: zodAdapter(UserSchema), // Optional - validates DB results
create: zodAdapter(CreateUserSchema)
}
// Output validation controlled via KYSERA_VALIDATION_MODE or NODE_ENV
})
Validation Modes
Control validation behavior via environment variables:
# Full validation (development)
KYSERA_VALIDATION_MODE=always
# Input only (production)
KYSERA_VALIDATION_MODE=production
# No validation (testing/performance)
KYSERA_VALIDATION_MODE=never
# Default behavior based on NODE_ENV
KYSERA_VALIDATION_MODE=development
Using getValidationMode
import { getValidationMode, shouldValidate } from '@kysera/repository'
// Get current mode
const mode = getValidationMode() // 'always' | 'never' | 'development' | 'production'
// Check if validation should run
if (shouldValidate({ mode: 'development' })) {
// Validate data
}
Defining Schemas
Entity Schema
Represents the full entity as stored in the database:
const UserSchema = z.object({
id: z.number(),
email: z.string().email(),
name: z.string(),
created_at: z.date(),
updated_at: z.date().nullable()
})
Create Schema
For insert operations (excludes auto-generated fields):
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(['user', 'admin']).default('user')
})
Update Schema
For update operations (partial of create schema):
const UpdateUserSchema = CreateUserSchema.partial()
// Or with specific requirements
const UpdateUserSchema = z.object({
email: z.string().email().optional(),
name: z.string().min(1).max(100).optional(),
// role cannot be updated
})
Custom Validators
Using createValidator
import { createValidator } from '@kysera/repository'
import { zodAdapter } from '@kysera/repository'
const userValidator = createValidator(zodAdapter(UserSchema), {
mode: 'development'
})
// Different validation methods
const user = userValidator.validate(data) // Throws on error
const user = userValidator.validateSafe(data) // Returns null on error
const isValid = userValidator.isValid(data) // Returns boolean
const user = userValidator.validateConditional(data) // Validates based on mode
Safe Parsing
import { safeParse } from '@kysera/repository'
import { zodAdapter } from '@kysera/repository'
const result = safeParse(zodAdapter(UserSchema), data, {
throwOnError: false,
logErrors: true
})
if (result) {
// Use validated data
}
Validation at API Boundaries
Always validate at the API layer, not just in repositories:
// Good: Validate at API boundary
app.post('/users', async (req, res) => {
// Validate request body first
const input = CreateUserSchema.parse(req.body)
// Repository receives validated data
const user = await userRepo.create(input)
res.json(user)
})
// Bad: Relying only on repository validation
app.post('/users', async (req, res) => {
try {
const user = await userRepo.create(req.body) // Unvalidated!
res.json(user)
} catch (error) {
res.status(500).json({ error: error.message }) // Leaks internal errors
}
})
Complex Validation Rules
Cross-Field Validation
const CreateOrderSchema = z.object({
items: z.array(z.object({
productId: z.number(),
quantity: z.number().min(1)
})).min(1),
shippingAddress: z.object({
street: z.string(),
city: z.string(),
country: z.string()
}),
paymentMethod: z.enum(['card', 'bank_transfer'])
}).refine(
(data) => data.items.reduce((sum, item) => sum + item.quantity, 0) <= 100,
{ message: 'Maximum 100 items per order' }
)
Async Validation
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1)
}).refine(
async (data) => {
const existing = await userRepo.findByEmail(data.email)
return !existing
},
{ message: 'Email already exists' }
)
Conditional Validation
const PaymentSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('card'),
cardNumber: z.string().length(16),
expiryDate: z.string()
}),
z.object({
type: z.literal('bank_transfer'),
accountNumber: z.string(),
routingNumber: z.string()
})
])
Error Handling
ValidationError
import { ValidationError } from '@kysera/core'
try {
await userRepo.create(invalidData)
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.errors) // Zod error details
// Return 400 Bad Request with error details
}
}
Formatting Validation Errors
const formatValidationError = (error: ValidationError) => {
return {
message: 'Validation failed',
errors: error.errors.map(e => ({
field: e.path.join('.'),
message: e.message
}))
}
}
Performance Considerations
When to Skip Result Validation
- Trusted internal queries: Data from your own application
- High-throughput operations: Bulk reads where validation overhead matters
- Production environments: Use
KYSERA_VALIDATION_MODE=production
When to Keep Result Validation
- Development: Catch type mismatches early
- External data sources: Data from migrations or imports
- Schema changes: After database migrations
- Critical paths: Where data integrity is paramount