Validation ​
Asena provides built-in validation support for the Ergenecore adapter using Zod. Validation ensures that incoming request data meets your requirements before reaching your route handlers.
Adapter Support
Validation is currently supported only in the Ergenecore adapter. The Hono adapter can use Zod directly with Hono's validation middleware.
Why Use Validation? ​
- Type Safety: Catch type mismatches at runtime
- Security: Prevent malicious or malformed input
- Better Error Messages: Provide clear validation feedback
- Self-Documentation: Schema serves as API documentation
- Automatic Error Handling: 400 Bad Request responses automatically
Quick Start ​
1. Create a Validator ​
Extend ValidationService and implement the json() method:
import { Middleware } from '@asenajs/asena/server';
import { ValidationService } from '@asenajs/ergenecore';
import { z } from 'zod';
@Middleware({ validator: true })
export class CreateUserValidator extends ValidationService {
json() {
return z.object({
name: z.string().min(3).max(50),
email: z.string().email(),
age: z.number().min(18).max(120)
});
}
}2. Apply to a Route ​
Use the validator option in route decorators:
import { Controller } from '@asenajs/asena/server';
import { Post } from '@asenajs/asena/web';
import type { Context } from '@asenajs/ergenecore';
@Controller('/users')
export class UserController {
@Post({ path: '/', validator: CreateUserValidator })
async create(context: Context) {
const body = await context.getBody();
// body is guaranteed to be valid!
return context.send({ created: true, user: body }, 201);
}
}Validation Happens Automatically
The validator runs before your route handler. If validation fails, the handler is never called.
ValidationService API ​
The json() Method ​
The json() method defines the validation schema for request bodies:
@Middleware({ validator: true })
export class MyValidator extends ValidationService {
json() {
// Return a Zod schema
return z.object({
// Your validation rules
});
}
}Method Signature:
json(): ValidationSchema | Promise<ValidationSchema>- Return Type: A Zod schema (
z.ZodType) - Async Support: Can be
asyncfor dynamic schemas
Basic Example ​
@Middleware({ validator: true })
export class ProductValidator extends ValidationService {
json() {
return z.object({
name: z.string(),
price: z.number().positive(),
category: z.enum(['electronics', 'clothing', 'food']),
inStock: z.boolean()
});
}
}Async Validation ​
For database lookups or async checks:
@Middleware({ validator: true })
export class UniqueEmailValidator extends ValidationService {
@Inject(UserRepository)
private userRepo: UserRepository;
async json() {
return z.object({
email: z.string().email().refine(async (email) => {
const exists = await this.userRepo.findByEmail(email);
return !exists;
}, 'Email already exists')
});
}
}Validation Hooks ​
Use ValidationSchemaWithHook to execute custom logic after validation:
import { type ValidationSchemaWithHook, ValidationService } from '@asenajs/ergenecore';
@Middleware({ validator: true })
export class UserValidatorWithHook extends ValidationService {
json(): ValidationSchemaWithHook {
return {
schema: z.object({
email: z.string().email(),
password: z.string().min(8)
}),
hook: (result, context) => {
// result: validated data
// context: Ergenecore Context
// Log validation success
console.log('Validated user data:', result);
// Store in context for later use
context.setValue('validatedEmail', result.email);
// Transform or enrich data
return {
...result,
emailLowercase: result.email.toLowerCase()
};
}
};
}
}import { type ValidationSchemaWithHook, ValidationService } from '@asenajs/hono-adapter';
@Middleware({ validator: true })
export class UserValidatorWithHook extends ValidationService {
json(): ValidationSchemaWithHook {
return {
schema: z.object({
email: z.string().email(),
password: z.string().min(8)
}),
hook: (result, context) => {
// result: validated data
// context: Ergenecore Context
// Log validation success
console.log('Validated user data:', result);
// Store in context for later use
context.setValue('validatedEmail', result.email);
// Transform or enrich data
return {
...result,
emailLowercase: result.email.toLowerCase()
};
}
};
}
}Hook Function Signature ​
hook: (result: any, context: Context) => any| Parameter | Type | Description |
|---|---|---|
result | any | The validated and parsed data from Zod |
context | Context | Ergenecore Context object |
| Returns | any | Transformed data (optional) |
Hook Use Cases ​
1. Logging and Monitoring
hook: (result, context) => {
console.log('Validation passed:', {
endpoint: context.req.url,
data: result
});
}2. Storing Validated Data in Context
hook: (result, context) => {
// Make validated data available to middlewares/handlers
context.setValue('validatedUser', result);
context.setValue('userEmail', result.email);
}3. Data Transformation
hook: (result, context) => {
// Transform validated data
return {
...result,
email: result.email.toLowerCase(),
createdAt: new Date(),
ipAddress: context.req.headers.get('x-forwarded-for')
};
}4. Side Effects
hook: async (result, context) => {
// Send notification, update cache, etc.
await this.notificationService.sendAlert('New signup', result.email);
}Validation Error Responses ​
When validation fails, Asena automatically returns 400 Bad Request with detailed error information:
{
"error": "Validation failed",
"details": [
{
"path": ["email"],
"message": "Invalid email"
},
{
"path": ["age"],
"message": "Must be at least 18"
}
]
}Custom Error Handling
You cannot customize the error response format directly. For custom error handling, use a global error handler in your @Config class.
Integration with Controllers ​
Route-Level Validation ​
@Controller('/products')
export class ProductController {
@Post({ path: '/', validator: CreateProductValidator })
async create(context: Context) {
// Body is already validated
const product = await context.getBody();
return context.send(product, 201);
}
@Put({ path: '/:id', validator: UpdateProductValidator })
async update(context: Context) {
const id = context.getParam('id');
const updates = await context.getBody();
return context.send({ updated: true });
}
}Combining with Middleware ​
@Post({
path: '/',
middlewares: [AuthMiddleware, RateLimitMiddleware],
validator: CreateUserValidator
})
async create(context: Context) {
// Execution order:
// 1. AuthMiddleware
// 2. RateLimitMiddleware
// 3. CreateUserValidator (validation)
// 4. create() handler
}Zod Schema Definition ​
Asena uses Zod for schema definition. The json() method returns a Zod schema:
json() {
return z.object({
name: z.string(),
email: z.string().email(),
age: z.number().int().positive()
});
}Reusable Schemas ​
You can extract and reuse common schemas across multiple validators:
// src/validators/schemas/common.ts
import { z } from 'zod';
// Reusable schema definitions
export const emailSchema = z.string().email().toLowerCase();
export const passwordSchema = z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/);
export const addressSchema = z.object({
street: z.string().min(1),
city: z.string().min(1),
zipCode: z.string().regex(/^\d{5}$/),
country: z.string().length(2)
});
export const paginationSchema = z.object({
page: z.number().int().positive().default(1),
limit: z.number().int().min(1).max(100).default(10)
});Then use them in your validators:
import { Middleware } from '@asenajs/asena/server';
import { ValidationService } from '@asenajs/ergenecore';
import { emailSchema, passwordSchema, addressSchema } from '../schemas/common';
import { z } from 'zod';
@Middleware({ validator: true })
export class CreateUserValidator extends ValidationService {
json() {
return z.object({
email: emailSchema, // Reuse common schema
password: passwordSchema, // Reuse common schema
shippingAddress: addressSchema // Reuse common schema
});
}
}
@Middleware({ validator: true })
export class UpdateUserValidator extends ValidationService {
json() {
return z.object({
email: emailSchema.optional(), // Reuse and modify
shippingAddress: addressSchema.partial() // All fields optional
});
}
}Benefits of Reusable Schemas
- Consistency: Same validation rules across your app
- Maintainability: Update once, apply everywhere
- Type Safety: Share types between validators
- Composition: Build complex schemas from simple ones
Zod API Documentation
For complete schema documentation including strings, numbers, arrays, objects, unions, transformations, and refinements, see the Zod API Documentation.
Asena-Specific Best Practices ​
1. Use Validators for All User Input ​
// ✅ Good: Validate all POST/PUT/PATCH requests
@Post({ path: '/', validator: CreateUserValidator })
async create(context: Context) { }2. Keep Validators Focused ​
// ✅ Good: One validator per endpoint
@Middleware({ validator: true })
export class CreateUserValidator extends ValidationService {
json() {
return z.object({
name: z.string(),
email: z.string().email()
});
}
}3. Use Hooks for Context Integration ​
// ✅ Good: Use hooks to enrich context
hook: (result, context) => {
context.setValue('userId', result.id);
context.setValue('userRole', result.role);
}4. Leverage Dependency Injection ​
// ✅ Good: Inject services in validators
@Middleware({ validator: true })
export class UniqueUsernameValidator extends ValidationService {
@Inject(UserService)
private userService: UserService;
async json() {
return z.object({
username: z.string().refine(async (name) => {
return !(await this.userService.usernameExists(name));
}, 'Username already taken')
});
}
}ValidationSchema Types ​
// Simple schema
type ValidationSchema = z.ZodType<any, z.ZodTypeDef, any>;
// Schema with hook
interface ValidationSchemaWithHook {
schema: ValidationSchema;
hook?: (result: any, context: Context) => any;
}Complete Example ​
Here's a real-world validator with hooks and service injection:
import { Middleware } from '@asenajs/asena/server';
import { ValidationService, type ValidationSchemaWithHook } from '@asenajs/ergenecore';
import { Inject } from '@asenajs/asena/ioc';
import { z } from 'zod';
@Middleware({ validator: true })
export class RegisterUserValidator extends ValidationService {
@Inject(UserService)
private userService: UserService;
@Inject(Logger)
private logger: Logger;
async json(): Promise<ValidationSchemaWithHook> {
return {
schema: z.object({
username: z.string()
.min(3, 'Username must be at least 3 characters')
.max(20)
.regex(/^[a-zA-Z0-9_]+$/, 'Alphanumeric and underscore only')
.refine(async (name) => {
const exists = await this.userService.usernameExists(name);
return !exists;
}, 'Username already taken'),
email: z.string()
.email('Invalid email')
.transform(v => v.toLowerCase()),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain uppercase letter')
.regex(/[0-9]/, 'Must contain number'),
confirmPassword: z.string(),
terms: z.literal(true, {
errorMap: () => ({ message: 'Must accept terms' })
})
}).refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword']
}),
hook: (result, context) => {
// Log validation success
this.logger.info('User registration validated', {
username: result.username,
email: result.email
});
// Store validated data in context
context.setValue('registrationData', result);
// Remove confirmPassword from result
const { confirmPassword, ...userData } = result;
return userData;
}
};
}
}Related Documentation ​
- Controllers - Using validators in controllers
- Middleware - Understanding middleware flow
- Ergenecore Adapter - Ergenecore-specific features
- Context API - Working with Context in hooks
- Zod Documentation - Complete Zod schema guide
Next Steps:
- Learn about Middleware
- Explore Error Handling
- Understand Context API