Fundamentos de Arquitectura Web

Fundamentos de Arquitectura Web

article author avatar

raulanto

Desarrollador Full Stack

ArquitecturaWeb Development

29 Sep 2025

13 min read

Principios, Patrones y Protocolos

1. Principios de Arquitectura de Software

SOLID Principles

S - Single Responsibility Principle (SRP)

Una clase debe tener una sola razón para cambiar

// ❌ Viola SRP - maneja tanto persistencia como lógica de negocio
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
  
  save() {
    // Lógica de base de datos
    database.save(this);
  }
  
  sendEmail() {
    // Lógica de email
    emailService.send(this.email);
  }
  
  validateEmail() {
    // Lógica de validación
    return this.email.includes('@');
  }
}

// ✅ Respeta SRP - cada clase tiene una responsabilidad
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
}

class UserRepository {
  save(user) {
    database.save(user);
  }
}

class EmailService {
  send(email, message) {
    // Lógica de envío
  }
}

class EmailValidator {
  validate(email) {
    return email.includes('@') && email.includes('.');
  }
}

O - Open/Closed Principle (OCP)

Las entidades deben estar abiertas para extensión, cerradas para modificación

// ❌ Viola OCP - necesitas modificar la clase para agregar nuevos tipos
class PaymentProcessor {
  process(payment, type) {
    if (type === 'credit_card') {
      return this.processCreditCard(payment);
    } else if (type === 'paypal') {
      return this.processPaypal(payment);
    }
    // Para agregar Bitcoin, necesitas modificar esta clase
  }
}

// ✅ Respeta OCP - puedes agregar nuevos procesadores sin modificar código existente
class PaymentProcessor {
  constructor() {
    this.processors = new Map();
  }
  
  registerProcessor(type, processor) {
    this.processors.set(type, processor);
  }
  
  process(payment, type) {
    const processor = this.processors.get(type);
    return processor.process(payment);
  }
}

class CreditCardProcessor {
  process(payment) {
    // Lógica específica para tarjetas
  }
}

class BitcoinProcessor {
  process(payment) {
    // Nueva funcionalidad sin modificar código existente
  }
}

L - Liskov Substitution Principle (LSP)

Los objetos derivados deben poder sustituir a sus objetos base

// ❌ Viola LSP - el subtipo no puede sustituir al tipo base
class Bird {
  fly() {
    return "Flying high!";
  }
}

class Penguin extends Bird {
  fly() {
    throw new Error("Penguins can't fly!");
  }
}

// ✅ Respeta LSP - diseño por comportamiento común
class Bird {
  move() {
    return "Moving around";
  }
}

class FlyingBird extends Bird {
  fly() {
    return "Flying high!";
  }
  
  move() {
    return this.fly();
  }
}

class SwimmingBird extends Bird {
  swim() {
    return "Swimming gracefully";
  }
  
  move() {
    return this.swim();
  }
}

I - Interface Segregation Principle (ISP)

Los clientes no deben depender de interfaces que no usan

// ❌ Viola ISP - interfaz demasiado grande
class MultiFunctionPrinter {
  print(document) {}
  scan(document) {}
  fax(document) {}
  email(document) {}
}

class SimplePrinter extends MultiFunctionPrinter {
  print(document) {
    // Implementación real
  }
  
  scan(document) {
    throw new Error("Not supported");
  }
  
  fax(document) {
    throw new Error("Not supported");
  }
}

// ✅ Respeta ISP - interfaces específicas y pequeñas
class Printer {
  print(document) {}
}

class Scanner {
  scan(document) {}
}

class FaxMachine {
  fax(document) {}
}

class AdvancedPrinter extends Printer {
  print(document) {
    // Solo implementa lo que necesita
  }
}

class AllInOnePrinter {
  constructor(printer, scanner, fax) {
    this.printer = printer;
    this.scanner = scanner;
    this.fax = fax;
  }
}

D - Dependency Inversion Principle (DIP)

Depende de abstracciones, no de concreciones

// ❌ Viola DIP - depende directamente de implementaciones concretas
class OrderService {
  constructor() {
    this.emailService = new GmailService();
    this.database = new MySQLDatabase();
  }
  
  createOrder(order) {
    this.database.save(order);
    this.emailService.send(order.customerEmail, "Order confirmed");
  }
}

// ✅ Respeta DIP - depende de abstracciones
class OrderService {
  constructor(emailService, database) {
    this.emailService = emailService;
    this.database = database;
  }
  
  createOrder(order) {
    this.database.save(order);
    this.emailService.send(order.customerEmail, "Order confirmed");
  }
}

// Inyección de dependencias
const orderService = new OrderService(
  new GmailService(), // Fácil de cambiar por SendGridService
  new MySQLDatabase() // Fácil de cambiar por PostgreSQLDatabase
);

DRY (Don't Repeat Yourself)

// ❌ Viola DRY - código duplicado
class UserController {
  createUser(userData) {
    if (!userData.email || !userData.email.includes('@')) {
      throw new Error('Invalid email');
    }
    if (!userData.password || userData.password.length < 8) {
      throw new Error('Password too short');
    }
    // Crear usuario
  }
  
  updateUser(userData) {
    if (!userData.email || !userData.email.includes('@')) {
      throw new Error('Invalid email');
    }
    if (!userData.password || userData.password.length < 8) {
      throw new Error('Password too short');
    }
    // Actualizar usuario
  }
}

// ✅ Respeta DRY - validación centralizada
class UserValidator {
  static validate(userData) {
    const errors = [];
    
    if (!userData.email || !userData.email.includes('@')) {
      errors.push('Invalid email');
    }
    
    if (!userData.password || userData.password.length < 8) {
      errors.push('Password too short');
    }
    
    if (errors.length > 0) {
      throw new Error(errors.join(', '));
    }
  }
}

class UserController {
  createUser(userData) {
    UserValidator.validate(userData);
    // Crear usuario
  }
  
  updateUser(userData) {
    UserValidator.validate(userData);
    // Actualizar usuario
  }
}

2. Patrones de Diseño Aplicados a Web

MVC (Model-View-Controller)

// MODEL - Maneja datos y lógica de negocio
class UserModel {
  constructor(database) {
    this.database = database;
  }
  
  async createUser(userData) {
    const user = {
      id: generateId(),
      ...userData,
      createdAt: new Date()
    };
    
    await this.database.users.insert(user);
    return user;
  }
  
  async getUserById(id) {
    return await this.database.users.findById(id);
  }
  
  async validateUser(userData) {
    // Lógica de validación de negocio
    if (!userData.email) throw new Error('Email required');
    return true;
  }
}

// VIEW - Maneja presentación (en el contexto web, a menudo JSON/HTML)
class UserView {
  renderUser(user) {
    return {
      id: user.id,
      name: user.name,
      email: user.email,
      // No exponer datos sensibles como passwords
    };
  }
  
  renderError(error) {
    return {
      error: true,
      message: error.message
    };
  }
  
  renderUserList(users) {
    return {
      users: users.map(user => this.renderUser(user)),
      total: users.length
    };
  }
}

// CONTROLLER - Coordina Model y View, maneja requests
class UserController {
  constructor(userModel, userView) {
    this.userModel = userModel;
    this.userView = userView;
  }
  
  async createUser(req, res) {
    try {
      await this.userModel.validateUser(req.body);
      const user = await this.userModel.createUser(req.body);
      const response = this.userView.renderUser(user);
      
      res.status(201).json(response);
    } catch (error) {
      const errorResponse = this.userView.renderError(error);
      res.status(400).json(errorResponse);
    }
  }
  
  async getUser(req, res) {
    try {
      const user = await this.userModel.getUserById(req.params.id);
      if (!user) {
        return res.status(404).json({ error: 'User not found' });
      }
      
      const response = this.userView.renderUser(user);
      res.json(response);
    } catch (error) {
      const errorResponse = this.userView.renderError(error);
      res.status(500).json(errorResponse);
    }
  }
}

Repository Pattern

// Abstracción del repositorio
class UserRepository {
  async save(user) {
    throw new Error('Method not implemented');
  }
  
  async findById(id) {
    throw new Error('Method not implemented');
  }
  
  async findByEmail(email) {
    throw new Error('Method not implemented');
  }
  
  async delete(id) {
    throw new Error('Method not implemented');
  }
}

// Implementación concreta para MongoDB
class MongoUserRepository extends UserRepository {
  constructor(mongoClient) {
    super();
    this.collection = mongoClient.db('myapp').collection('users');
  }
  
  async save(user) {
    if (user.id) {
      await this.collection.updateOne(
        { _id: user.id },
        { $set: user }
      );
    } else {
      const result = await this.collection.insertOne(user);
      user.id = result.insertedId;
    }
    return user;
  }
  
  async findById(id) {
    return await this.collection.findOne({ _id: id });
  }
  
  async findByEmail(email) {
    return await this.collection.findOne({ email });
  }
}

// Implementación para PostgreSQL
class PostgresUserRepository extends UserRepository {
  constructor(pgClient) {
    super();
    this.client = pgClient;
  }
  
  async save(user) {
    if (user.id) {
      const query = 'UPDATE users SET name = $1, email = $2 WHERE id = $3';
      await this.client.query(query, [user.name, user.email, user.id]);
    } else {
      const query = 'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id';
      const result = await this.client.query(query, [user.name, user.email]);
      user.id = result.rows[0].id;
    }
    return user;
  }
  
  async findById(id) {
    const query = 'SELECT * FROM users WHERE id = $1';
    const result = await this.client.query(query, [id]);
    return result.rows[0];
  }
}

// Uso del patrón - fácil intercambio de implementaciones
class UserService {
  constructor(userRepository) {
    this.userRepository = userRepository; // Puede ser Mongo, Postgres, etc.
  }
  
  async createUser(userData) {
    const existingUser = await this.userRepository.findByEmail(userData.email);
    if (existingUser) {
      throw new Error('User already exists');
    }
    
    return await this.userRepository.save(userData);
  }
}

Factory Pattern

// Factory para diferentes tipos de notificaciones
class NotificationFactory {
  static create(type, config) {
    switch (type) {
      case 'email':
        return new EmailNotification(config);
      case 'sms':
        return new SMSNotification(config);
      case 'push':
        return new PushNotification(config);
      case 'webhook':
        return new WebhookNotification(config);
      default:
        throw new Error(`Notification type ${type} not supported`);
    }
  }
}

class EmailNotification {
  constructor(config) {
    this.smtpConfig = config.smtp;
  }
  
  async send(recipient, message) {
    // Lógica de envío por email
    console.log(`Email sent to ${recipient}: ${message}`);
  }
}

class SMSNotification {
  constructor(config) {
    this.apiKey = config.apiKey;
    this.provider = config.provider;
  }
  
  async send(phoneNumber, message) {
    // Lógica de envío por SMS
    console.log(`SMS sent to ${phoneNumber}: ${message}`);
  }
}

// Uso del factory
class NotificationService {
  constructor() {
    this.notifications = [];
  }
  
  addNotificationMethod(type, config) {
    const notification = NotificationFactory.create(type, config);
    this.notifications.push(notification);
  }
  
  async sendToAll(recipient, message) {
    const promises = this.notifications.map(notification => 
      notification.send(recipient, message)
    );
    await Promise.all(promises);
  }
}

// Configuración
const notificationService = new NotificationService();
notificationService.addNotificationMethod('email', { 
  smtp: { host: 'smtp.gmail.com', port: 587 } 
});
notificationService.addNotificationMethod('sms', { 
  apiKey: 'xxx', provider: 'twilio' 
});

Observer Pattern (Pub/Sub)

// Sistema de eventos para aplicaciones web
class EventEmitter {
  constructor() {
    this.events = new Map();
  }
  
  subscribe(eventName, callback) {
    if (!this.events.has(eventName)) {
      this.events.set(eventName, []);
    }
    
    this.events.get(eventName).push(callback);
    
    // Retorna función de unsuscribe
    return () => {
      const callbacks = this.events.get(eventName);
      const index = callbacks.indexOf(callback);
      if (index > -1) {
        callbacks.splice(index, 1);
      }
    };
  }
  
  emit(eventName, data) {
    if (this.events.has(eventName)) {
      this.events.get(eventName).forEach(callback => {
        callback(data);
      });
    }
  }
}

// Ejemplo de uso en una aplicación de e-commerce
class OrderService {
  constructor(eventEmitter) {
    this.eventEmitter = eventEmitter;
  }
  
  async createOrder(orderData) {
    // Crear la orden
    const order = await this.saveOrder(orderData);
    
    // Emitir evento para que otros servicios reaccionen
    this.eventEmitter.emit('order.created', {
      orderId: order.id,
      customerId: order.customerId,
      total: order.total,
      items: order.items
    });
    
    return order;
  }
}

// Servicios que escuchan eventos
class EmailService {
  constructor(eventEmitter) {
    eventEmitter.subscribe('order.created', this.sendConfirmationEmail.bind(this));
    eventEmitter.subscribe('order.shipped', this.sendShippingEmail.bind(this));
  }
  
  async sendConfirmationEmail(orderData) {
    console.log(`Sending confirmation email for order ${orderData.orderId}`);
  }
}

class InventoryService {
  constructor(eventEmitter) {
    eventEmitter.subscribe('order.created', this.updateInventory.bind(this));
  }
  
  async updateInventory(orderData) {
    console.log(`Updating inventory for order ${orderData.orderId}`);
    // Reducir stock de los items
  }
}

class AnalyticsService {
  constructor(eventEmitter) {
    eventEmitter.subscribe('order.created', this.trackSale.bind(this));
    eventEmitter.subscribe('user.registered', this.trackNewUser.bind(this));
  }
  
  async trackSale(orderData) {
    console.log(`Tracking sale: $${orderData.total}`);
  }
}

3. Protocolos y Estándares Web

HTTP/HTTPS Profundo

Headers Importantes y Su Uso

// Ejemplo de servidor Express con headers importantes
const express = require('express');
const app = express();

// Middleware para headers de seguridad
app.use((req, res, next) => {
  // Previene ataques XSS
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-XSS-Protection', '1; mode=block');
  
  // Content Security Policy
  res.setHeader('Content-Security-Policy', 
    "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
  );
  
  // HSTS para HTTPS
  if (req.secure) {
    res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  }
  
  next();
});

// Ejemplo de manejo de caché
app.get('/api/users/:id', (req, res) => {
  const userId = req.params.id;
  
  // ETag para validación de caché
  const user = getUserById(userId);
  const etag = generateETag(user);
  
  res.setHeader('ETag', etag);
  res.setHeader('Cache-Control', 'private, max-age=300'); // 5 minutos
  
  // Verificar If-None-Match header del cliente
  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end(); // Not Modified
  }
  
  res.json(user);
});

// Manejo de CORS
app.use((req, res, next) => {
  const origin = req.headers.origin;
  const allowedOrigins = ['https://myapp.com', 'https://admin.myapp.com'];
  
  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
  }
  
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  res.setHeader('Access-Control-Max-Age', '86400'); // 24 horas
  
  if (req.method === 'OPTIONS') {
    return res.status(200).end();
  }
  
  next();
});

Códigos de Estado HTTP y Su Uso Correcto

class APIController {
  // 200 - Success (GET, PUT)
  async getUser(req, res) {
    const user = await userService.findById(req.params.id);
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    res.status(200).json(user);
  }
  
  // 201 - Created (POST)
  async createUser(req, res) {
    try {
      const user = await userService.create(req.body);
      res.status(201)
         .location(`/api/users/${user.id}`)
         .json(user);
    } catch (error) {
      if (error.name === 'ValidationError') {
        return res.status(400).json({ error: error.message });
      }
      res.status(500).json({ error: 'Internal server error' });
    }
  }
  
  // 204 - No Content (DELETE)
  async deleteUser(req, res) {
    const deleted = await userService.delete(req.params.id);
    if (!deleted) {
      return res.status(404).json({ error: 'User not found' });
    }
    res.status(204).end();
  }
  
  // 400 - Bad Request
  async updateUser(req, res) {
    if (!req.body.name && !req.body.email) {
      return res.status(400).json({ 
        error: 'At least one field (name or email) is required' 
      });
    }
    
    // ... resto de la lógica
  }
  
  // 401 - Unauthorized
  async protectedRoute(req, res) {
    if (!req.headers.authorization) {
      return res.status(401).json({ error: 'Authorization header required' });
    }
    // ... verificar token
  }
  
  // 403 - Forbidden
  async adminOnly(req, res) {
    if (req.user.role !== 'admin') {
      return res.status(403).json({ error: 'Admin access required' });
    }
    // ... lógica de admin
  }
  
  // 409 - Conflict
  async createUniqueResource(req, res) {
    try {
      const resource = await service.create(req.body);
      res.status(201).json(resource);
    } catch (error) {
      if (error.code === 'DUPLICATE_KEY') {
        return res.status(409).json({ 
          error: 'Resource already exists' 
        });
      }
      throw error;
    }
  }
  
  // 422 - Unprocessable Entity
  async validateAndCreate(req, res) {
    const validation = validateInput(req.body);
    if (!validation.valid) {
      return res.status(422).json({
        error: 'Validation failed',
        details: validation.errors
      });
    }
    // ... crear recurso
  }
  
  // 429 - Too Many Requests
  async rateLimitedEndpoint(req, res) {
    const rateLimitInfo = await rateLimiter.check(req.ip);
    
    res.setHeader('X-RateLimit-Limit', rateLimitInfo.limit);
    res.setHeader('X-RateLimit-Remaining', rateLimitInfo.remaining);
    res.setHeader('X-RateLimit-Reset', rateLimitInfo.resetTime);
    
    if (rateLimitInfo.exceeded) {
      return res.status(429).json({ 
        error: 'Rate limit exceeded',
        retryAfter: rateLimitInfo.retryAfter
      });
    }
    
    // ... lógica normal
  }
}

REST vs GraphQL vs gRPC

REST API Design

// Diseño RESTful correcto
class RESTfulAPI {
  // Recursos como sustantivos, acciones como verbos HTTP
  
  // GET /api/users - Listar usuarios
  async listUsers(req, res) {
    const { page = 1, limit = 10, sort = 'name', filter } = req.query;
    
    const users = await userService.list({
      page: parseInt(page),
      limit: parseInt(limit),
      sort,
      filter
    });
    
    res.json({
      data: users,
      pagination: {
        page,
        limit,
        total: users.total,
        pages: Math.ceil(users.total / limit)
      },
      links: {
        self: `/api/users?page=${page}&limit=${limit}`,
        next: page < users.totalPages ? `/api/users?page=${page + 1}&limit=${limit}` : null,
        prev: page > 1 ? `/api/users?page=${page - 1}&limit=${limit}` : null
      }
    });
  }
  
  // GET /api/users/:id - Obtener usuario específico
  async getUser(req, res) {
    const user = await userService.findById(req.params.id);
    if (!user) return res.status(404).json({ error: 'User not found' });
    
    res.json({
      data: user,
      links: {
        self: `/api/users/${user.id}`,
        posts: `/api/users/${user.id}/posts`,
        comments: `/api/users/${user.id}/comments`
      }
    });
  }
  
  // POST /api/users - Crear usuario
  async createUser(req, res) {
    const user = await userService.create(req.body);
    
    res.status(201)
       .location(`/api/users/${user.id}`)
       .json({ data: user });
  }
  
  // PUT /api/users/:id - Actualizar usuario completo
  async updateUser(req, res) {
    const user = await userService.update(req.params.id, req.body);
    res.json({ data: user });
  }
  
  // PATCH /api/users/:id - Actualización parcial
  async partialUpdateUser(req, res) {
    const user = await userService.partialUpdate(req.params.id, req.body);
    res.json({ data: user });
  }
  
  // DELETE /api/users/:id - Eliminar usuario
  async deleteUser(req, res) {
    await userService.delete(req.params.id);
    res.status(204).end();
  }
  
  // Recursos anidados
  // GET /api/users/:id/posts - Posts de un usuario
  async getUserPosts(req, res) {
    const posts = await postService.findByUserId(req.params.id);
    res.json({ data: posts });
  }
}

GraphQL Schema y Resolvers

// Definición del schema GraphQL
const typeDefs = `
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
    createdAt: String!
  }
  
  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    comments: [Comment!]!
    publishedAt: String
  }
  
  type Comment {
    id: ID!
    content: String!
    author: User!
    post: Post!
    createdAt: String!
  }
  
  type Query {
    user(id: ID!): User
    users(first: Int, after: String): UserConnection!
    post(id: ID!): Post
    posts(authorId: ID, published: Boolean): [Post!]!
  }
  
  type Mutation {
    createUser(input: CreateUserInput!): User!
    updateUser(id: ID!, input: UpdateUserInput!): User!
    deleteUser(id: ID!): Boolean!
    
    createPost(input: CreatePostInput!): Post!
    publishPost(id: ID!): Post!
  }
  
  input CreateUserInput {
    name: String!
    email: String!
  }
  
  input UpdateUserInput {
    name: String
    email: String
  }
  
  input CreatePostInput {
    title: String!
    content: String!
    authorId: ID!
  }
  
  type UserConnection {
    edges: [UserEdge!]!
    pageInfo: PageInfo!
  }
  
  type UserEdge {
    node: User!
    cursor: String!
  }
  
  type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    startCursor: String
    endCursor: String
  }
`;

// Resolvers
const resolvers = {
  Query: {
    user: async (parent, { id }, context) => {
      return await context.dataSources.userService.findById(id);
    },
    
    users: async (parent, { first = 10, after }, context) => {
      const result = await context.dataSources.userService.findMany({
        first,
        after
      });
      
      return {
        edges: result.users.map(user => ({
          node: user,
          cursor: Buffer.from(`user:${user.id}`).toString('base64')
        })),
        pageInfo: {
          hasNextPage: result.hasNextPage,
          hasPreviousPage: result.hasPreviousPage,
          startCursor: result.startCursor,
          endCursor: result.endCursor
        }
      };
    },
    
    posts: async (parent, { authorId, published }, context) => {
      return await context.dataSources.postService.findMany({
        authorId,
        published
      });
    }
  },
  
  Mutation: {
    createUser: async (parent, { input }, context) => {
      // Validación
      if (!input.email.includes('@')) {
        throw new Error('Invalid email format');
      }
      
      return await context.dataSources.userService.create(input);
    },
    
    createPost: async (parent, { input }, context) => {
      // Verificar autorización
      if (!context.user || context.user.id !== input.authorId) {
        throw new Error('Unauthorized');
      }
      
      return await context.dataSources.postService.create(input);
    }
  },
  
  // Resolvers de campo - evitan el problema N+1
  User: {
    posts: async (user, args, context) => {
      return await context.dataSources.postService.findByAuthorId(user.id);
    }
  },
  
  Post: {
    author: async (post, args, context) => {
      // DataLoader para batching automático
      return await context.dataSources.userLoader.load(post.authorId);
    },
    
    comments: async (post, args, context) => {
      return await context.dataSources.commentService.findByPostId(post.id);
    }
  }
};

rauantodev