Validación de Comandos con Zod
Validacion de Comandos con Zod
En este capitulo implementaremos validacion robusta de comandos, asegurando que los datos sean correctos antes de que lleguen al dominio.
Por Que Validar Comandos
La validacion de comandos ocurre antes de llegar al dominio. Queremos rechazar datos invalidos lo mas temprano posible por varias razones:
- Feedback rapido: El usuario sabe inmediatamente si sus datos tienen problemas
- Proteccion del dominio: El agregado no debe preocuparse por datos malformados
- Seguridad: Previene inyecciones y datos maliciosos
- Claridad: Separa la validacion de formato de la validacion de negocio
Hay dos tipos de validacion:
- Validacion de formato: El email tiene formato correcto, la cantidad es un numero positivo
- Validacion de negocio: El producto tiene stock suficiente, el cliente no ha excedido su credito
La validacion de formato ocurre en el comando. La validacion de negocio ocurre en el handler/agregado.
Que es Zod
Zod es una libreria de validacion y parsing para TypeScript. Permite definir schemas (esquemas) que describen la estructura y restricciones de los datos. Zod valida los datos contra el schema y retorna un resultado tipado.
La ventaja de Zod sobre validacion manual es que:
- Los tipos TypeScript se infieren automaticamente del schema
- Los mensajes de error son descriptivos y estructurados
- La sintaxis es declarativa y facil de leer
Schemas con Zod - TypeScript
// src/application/commands/order/create-order.schema.ts
import { z } from "zod";
const orderItemSchema = z.object({
productId: z.string().uuid("Product ID must be valid UUID"),
quantity: z.number().int().min(1, "Quantity must be at least 1").max(100)
});
const addressSchema = z.object({
street: z.string().min(5).max(200),
city: z.string().min(2).max(100),
zip: z.string().regex(/^\d{5}(-\d{4})?$/, "Invalid ZIP code"),
country: z.string().length(2).default("US")
});
export const createOrderSchema = z.object({
customerId: z.string().uuid("Customer ID must be valid UUID"),
items: z.array(orderItemSchema).min(1, "Order must have at least one item"),
shippingAddress: addressSchema
});
export type CreateOrderInput = z.infer<typeof createOrderSchema>;
El schema define todas las restricciones: UUIDs validos, cantidades entre 1 y 100, codigo postal con formato correcto, etc.
Command Validado - TypeScript
Ahora usamos el schema en el comando. El metodo estatico create valida los datos y lanza un error si son invalidos:
// src/application/commands/order/create-order.command.ts
import { BaseCommand } from "../command";
import { createOrderSchema, CreateOrderInput } from "./create-order.schema";
import { ValidationError } from "@domain/shared/errors";
export class CreateOrderCommand extends BaseCommand {
readonly customerId: string;
readonly items: readonly { productId: string; quantity: number }[];
readonly shippingAddress: {
street: string;
city: string;
zip: string;
country: string;
};
private constructor(input: CreateOrderInput) {
super();
this.customerId = input.customerId;
this.items = Object.freeze(input.items);
this.shippingAddress = Object.freeze(input.shippingAddress);
}
static create(input: unknown): CreateOrderCommand {
const result = createOrderSchema.safeParse(input);
if (!result.success) {
throw new ValidationError(
"Invalid CreateOrderCommand",
result.error.flatten().fieldErrors
);
}
return new CreateOrderCommand(result.data);
}
}
El constructor es privado (private constructor), forzando el uso de create() que siempre valida.
Validacion en Go
Go no tiene una libreria estandar como Zod, pero podemos implementar validacion con errores estructurados:
// internal/application/command/create_order_command.go
package command
import (
"fmt"
"regexp"
"github.com/google/uuid"
)
type ValidationError struct {
Field string
Message string
}
type ValidationErrors []ValidationError
func (e ValidationErrors) Error() string {
return fmt.Sprintf("validation failed: %d errors", len(e))
}
func NewCreateOrderCommand(
customerID string,
items []OrderItemDTO,
address AddressDTO,
) (CreateOrderCommand, error) {
var errors ValidationErrors
// Validate customerID
if _, err := uuid.Parse(customerID); err != nil {
errors = append(errors, ValidationError{
Field: "customerId", Message: "must be valid UUID",
})
}
// Validate items
if len(items) == 0 {
errors = append(errors, ValidationError{
Field: "items", Message: "must have at least one item",
})
}
for i, item := range items {
if _, err := uuid.Parse(item.ProductID); err != nil {
errors = append(errors, ValidationError{
Field: fmt.Sprintf("items[%d].productId", i),
Message: "must be valid UUID",
})
}
if item.Quantity < 1 || item.Quantity > 100 {
errors = append(errors, ValidationError{
Field: fmt.Sprintf("items[%d].quantity", i),
Message: "must be between 1 and 100",
})
}
}
// Validate address
zipRegex := regexp.MustCompile(`^\d{5}(-\d{4})?$`)
if !zipRegex.MatchString(address.Zip) {
errors = append(errors, ValidationError{
Field: "shippingAddress.zip", Message: "invalid ZIP code",
})
}
if len(errors) > 0 {
return CreateOrderCommand{}, errors
}
return CreateOrderCommand{
commandID: uuid.NewString(),
CustomerID: customerID,
Items: items,
ShippingAddress: address,
}, nil
}
Validacion en Python con Pydantic
Pydantic es el equivalente de Zod en Python. Permite definir modelos con validacion automatica:
# src/orderflow/application/commands/order/create_order_command.py
from dataclasses import dataclass
from typing import Self
from pydantic import BaseModel, Field, field_validator
import re
class OrderItemInput(BaseModel):
product_id: str = Field(pattern=r"^[0-9a-f-]{36}$")
quantity: int = Field(ge=1, le=100)
class AddressInput(BaseModel):
street: str = Field(min_length=5, max_length=200)
city: str = Field(min_length=2, max_length=100)
zip: str
country: str = Field(default="US", min_length=2, max_length=2)
@field_validator("zip")
@classmethod
def validate_zip(cls, v: str) -> str:
if not re.match(r"^\d{5}(-\d{4})?$", v):
raise ValueError("Invalid ZIP code")
return v
class CreateOrderInput(BaseModel):
customer_id: str = Field(pattern=r"^[0-9a-f-]{36}$")
items: list[OrderItemInput] = Field(min_length=1)
shipping_address: AddressInput
@dataclass(frozen=True)
class CreateOrderCommand(Command):
customer_id: str
items: tuple[OrderItemDTO, ...]
shipping_address: AddressDTO
@classmethod
def create(cls, data: dict) -> Self:
validated = CreateOrderInput.model_validate(data)
return cls(
customer_id=validated.customer_id,
items=tuple(
OrderItemDTO(i.product_id, i.quantity)
for i in validated.items
),
shipping_address=AddressDTO(
validated.shipping_address.street,
validated.shipping_address.city,
validated.shipping_address.zip,
validated.shipping_address.country,
)
)
Middleware de Validacion
Opcionalmente, puedes crear un middleware que envuelva al Command Bus y agregue funcionalidad transversal como logging o metricas:
// TypeScript - Validating Command Bus
export class ValidatingCommandBus implements CommandBus {
constructor(private readonly inner: CommandBus) {}
async dispatch<T extends Command>(command: T): Promise<void> {
// La validación ya ocurrió en el factory del Command
// Aquí podríamos agregar logging, métricas, etc.
console.log(`Dispatching ${command.constructor.name}`);
await this.inner.dispatch(command);
}
}
Manejo de Errores en la API
Cuando la validacion falla, debemos retornar errores claros al cliente:
// src/domain/shared/errors.ts
export class ValidationError extends Error {
constructor(
message: string,
readonly fieldErrors: Record<string, string[]>
) {
super(message);
this.name = "ValidationError";
}
}
// API Handler
app.post("/orders", async (req, res) => {
try {
const command = CreateOrderCommand.create(req.body);
await commandBus.dispatch(command);
res.status(201).json({ success: true });
} catch (error) {
if (error instanceof ValidationError) {
res.status(400).json({ errors: error.fieldErrors });
} else {
res.status(500).json({ error: "Internal error" });
}
}
});
Proximos Pasos
En el siguiente capitulo implementaremos la persistencia del Write Model.
Glosario
Schema (Esquema)
Definicion: Definicion formal de la estructura, tipos y restricciones que deben cumplir los datos. Es como un contrato que los datos deben satisfacer.
Por que es importante: Permite validar datos de forma declarativa. El schema documenta los requisitos y los aplica automaticamente.
Ejemplo practico: Un schema Zod que define que quantity debe ser un entero entre 1 y 100 rechazara automaticamente valores como -5, 0, 150 o “diez”.
Zod
Definicion: Libreria de validacion y parsing para TypeScript que permite definir schemas y validar datos en tiempo de ejecucion, con inferencia automatica de tipos.
Por que es importante: Proporciona validacion segura de tipos tanto en compilacion como en ejecucion. Los tipos TypeScript se derivan del schema, garantizando consistencia.
Ejemplo practico: z.string().email() crea un validador que rechaza cualquier string que no sea un email valido.
Pydantic
Definicion: Libreria de validacion de datos para Python que usa anotaciones de tipo para definir la estructura de datos y validarlos automaticamente.
Por que es importante: Es el estandar de facto para validacion en Python moderno. Integra perfectamente con FastAPI y otros frameworks.
Ejemplo practico: Un modelo Pydantic con email: EmailStr validara automaticamente que el campo contenga un email valido.
Validacion de Formato vs Validacion de Negocio
Definicion: Validacion de formato verifica que los datos tienen la estructura correcta (tipos, longitudes, patrones). Validacion de negocio verifica reglas del dominio (stock disponible, limites de credito).
Por que es importante: Separarlas mantiene el codigo organizado. La validacion de formato es rapida y puede hacerse sin base de datos. La validacion de negocio requiere contexto.
Ejemplo practico: Validacion de formato: “quantity es un numero positivo”. Validacion de negocio: “hay suficiente stock para esta quantity”.
Factory Method (Metodo Fabrica)
Definicion: Patron donde la creacion de objetos se hace a traves de un metodo estatico en lugar del constructor directo. Permite agregar logica de creacion como validacion.
Por que es importante: Garantiza que no se puedan crear instancias invalidas. El metodo fabrica puede validar y rechazar datos incorrectos.
Ejemplo practico: CreateOrderCommand.create(data) valida los datos y retorna un comando valido, o lanza un error. No puedes hacer new CreateOrderCommand(datosInvalidos).
Field Errors (Errores por Campo)
Definicion: Estructura que asocia cada campo con sus errores de validacion. Permite mostrar mensajes de error junto a cada campo del formulario.
Por que es importante: Mejora la experiencia de usuario al indicar exactamente que campos tienen problemas y por que.
Ejemplo practico: { "email": ["Invalid email format"], "quantity": ["Must be at least 1"] } indica claramente que campos corregir.