Saltar al contenido
CESAR ZANIS
Tech Reference
Esencial

Backend y Arquitectura

Clean Architecture, SOLID y los patrones que distinguen el código duradero de la deuda técnica. Cada patrón tiene un contexto y un costo.

"La mejor arquitectura es la que funciona, no la que está de moda. Pragmatismo antes que purismo."

Cesar Zanis
01

SOLID: los principios más útiles en el día a día

Los cinco principios importan. S, O y D son los que probablemente usarás cada semana. L e I también son importantes, pero se incumplen con menos frecuencia.

S

Single Responsibility

Una clase, un motivo para cambiar.

Una clase que envía correos, valida datos Y guarda en la base de datos será difícil de probar y todavía más difícil de cambiar. Separa las responsabilidades por el motivo del cambio, no por su tamaño.

Problemático
// ❌ Hace de todo: difícil de probar y cambiar
class OrderService {
  validate(order) { /* validación */ }
  save(order) { /* base de datos */ }
  sendEmail(order) { /* SMTP */ }
  generatePDF(order) { /* PDF */ }
} 
Correcto
// ✅ Cada clase tiene un motivo para existir
class OrderValidator { validate(order): Result<void> }
class OrderRepository { save(order): Promise<void> }
class OrderNotifier { notify(order): Promise<void> }
class OrderPdfGenerator { generate(order): Buffer }
O

Open/Closed

Abierto a la extensión, cerrado a la modificación.

No deberías editar una clase existente cada vez que aparece un nuevo método de pago. Usa interfaces y polimorfismo para ampliar el comportamiento sin cambiar lo que ya funciona.

Problemático
// ❌ Cada nuevo método de pago exige editar esta clase
class PaymentProcessor {
  process(method: string, amount: number) {
    if (method === 'pix') { /* ... */ }
    else if (method === 'credit') { /* ... */ }
    else if (method === 'boleto') { /* ... */ }
    // Un if más por método: riesgo de romper lo que ya funciona
  }
}
Correcto
// ✅ Nuevo método = nueva clase, sin tocar el núcleo
interface PaymentStrategy {
  process(amount: number): Promise<PaymentResult>
}

class PixPayment implements PaymentStrategy { ... }
class CreditCardPayment implements PaymentStrategy { ... }
class BoletoPayment implements PaymentStrategy { ... }

class PaymentProcessor {
  constructor(private strategy: PaymentStrategy) {}
  process(amount: number) { return this.strategy.process(amount) }
}
D

Dependency Inversion

Depende de abstracciones, no de implementaciones.

Tu servicio de pedidos debería depender de una interfaz IPaymentGateway en lugar de importar Stripe directamente. Así puedes sustituir Stripe por PagSeguro sin cambiar la lógica de negocio y hacer pruebas sin solicitudes reales.

Problemático
// ❌ Acoplado a la implementación: las pruebas llaman directamente a Stripe
class OrderService {
  private stripe = new StripeClient(process.env.STRIPE_KEY)
  
  async charge(order: Order) {
    return this.stripe.charges.create({ amount: order.total })
  }
}
Correcto
// ✅ Depende de una abstracción: se puede probar y sustituir
interface IPaymentGateway {
  charge(amount: number, currency: string): Promise<ChargeResult>
}

class OrderService {
  constructor(private payment: IPaymentGateway) {}
  
  async charge(order: Order) {
    return this.payment.charge(order.total, order.currency)
  }
}

// En producción: new OrderService(new StripeGateway())
// Em teste:    new OrderService(new MockPaymentGateway())
02

Patrones que resuelven problemas reales

Result Pattern: errores sin excepciones

Un patrón que facilita la lectura de APIs complejas.

Las excepciones no deberían controlar resultados de negocio esperados, como un correo ya registrado o saldo insuficiente. Resérvalas para fallos inesperados, como una base de datos caída o un disco lleno. Usa Result<T> para devolver éxito o fallo de forma explícita y obligar al llamador a gestionar ambos.

TypeScript
// El tipo
type Result<T, E = string> = 
  | { ok: true; value: T }
  | { ok: false; error: E }

// El servicio: sin throw para resultados del negocio
class UserService {
  async register(email: string, password: string): Promise<Result<User>> {
    const exists = await this.repo.findByEmail(email)
    if (exists) {
      return { ok: false, error: 'EMAIL_ALREADY_REGISTERED' }
    }
    
    const user = await this.repo.create({ email, password: await hash(password) })
    return { ok: true, value: user }
  }
}

// El controlador: obligado a manejar el error
async register(req, res) {
  const result = await this.userService.register(req.body.email, req.body.password)
  
  if (!result.ok) {
    return res.status(409).json({ error: result.error })
  }
  
  return res.status(201).json(result.value)
}

Repository Pattern: la base de datos como detalle

La capa de datos no es tu dominio. Trátala como un detalle de implementación.

Los servicios de negocio no deberían necesitar saber si los datos vienen de PostgreSQL, Redis o una API externa. Un Repository abstrae ese acceso. Puedes cambiar la base de datos sin cambiar la lógica y probarla sin una base de datos real.

TypeScript
// La interfaz: en el dominio, sin dependencia de base de datos
interface IOrderRepository {
  findById(id: string): Promise<Order | null>
  findByCustomer(customerId: string): Promise<Order[]>
  save(order: Order): Promise<void>
  delete(id: string): Promise<void>
}

// La implementación: un detalle de infraestructura
class PostgresOrderRepository implements IOrderRepository {
  async findById(id: string): Promise<Order | null> {
    const row = await this.db.query(
      'SELECT * FROM orders WHERE public_id = $1 AND deleted_at IS NULL',
      [id]
    )
    return row ? OrderMapper.toDomain(row) : null
  }
  // ...
}

// El servicio: independiente de la base de datos
class OrderService {
  constructor(private orders: IOrderRepository) {}
  
  async cancel(orderId: string): Promise<Result<void>> {
    const order = await this.orders.findById(orderId)
    if (!order) return { ok: false, error: 'ORDER_NOT_FOUND' }
    
    order.cancel() // lógica de dominio
    await this.orders.save(order)
    return { ok: true, value: undefined }
  }
}
03

OWASP Top 10: problemas frecuentes

La seguridad forma parte de los cimientos. Estos cinco problemas merecen especial atención en las revisiones de backend.

A01

Broken Access Control

Comprueba la autorización en el backend en CADA solicitud. Nunca dependas del frontend para controlar el acceso. Prueba qué ocurre cuando un usuario sin permiso intenta acceder a un recurso ajeno.

A02

Cryptographic Failures

Usa bcrypt o Argon2 para las contraseñas, nunca MD5 ni SHA sin salt. Usa TLS en todas las conexiones. Nunca registres datos sensibles como tokens, contraseñas o documentos de identidad.

A03

SQL Injection

Usa siempre consultas parametrizadas. Nunca concatenes la entrada del usuario en SQL. Los ORM ayudan, pero revisa el SQL que generan.

A07

Auth Failures

Usa JWT de corta duración (15-60 minutos) y refresh tokens con rotación. Limita las solicitudes a /login. Las respuestas de recuperación de contraseña no deben revelar si existe un correo.

A09

Registros insuficientes

Registra lo suficiente para reconstruir un incidente. Registra el evento, no los datos sensibles: 'El usuario X accedió al recurso Y', sin guardar su contenido.

Regla práctica: antes de aplicar un patrón, pregunta: '¿Qué problema concreto resuelve hoy?'. Si la respuesta es 'Ninguno, pero podría ser útil después', estás añadiendo complejidad por adelantado. Espera a que aparezca el problema.

¿Necesitas ayuda para diseñar la arquitectura de un proyecto real?

Hablar con Cesar Zanis