Arquitectura Cliente-Servidor

Fundamentos Security Notes Jan 9, 2026 JAVASCRIPT
arquitectura rest separacion-de-responsabilidades escalabilidad

Definición

La arquitectura cliente-servidor es una de las restricciones fundamentales de REST. Exige una separación clara de responsabilidades entre el cliente (la interfaz de usuario o aplicación que hace solicitudes) y el servidor (el sistema que proporciona recursos y procesamiento de lógica). Esta separación significa que el cliente no necesita saber nada sobre almacenamiento de datos, lógica de negocio o infraestructura del servidor, mientras que el servidor no necesita saber nada sobre interfaces de usuario, estado del usuario o cómo el cliente usará los datos.

La belleza de esta separación es que permite que cliente y servidor evolucionen independientemente. Puedes rediseñar completamente la UI de tu aplicación móvil sin tocar el servidor. Puedes escalar tu infraestructura backend, cambiar bases de datos o reescribir lógica de negocio sin romper aplicaciones cliente. Mientras el contrato (la interfaz API) permanezca consistente, ambos lados pueden cambiar libremente.

En las APIs REST, esta restricción se hace cumplir a través de una interfaz uniforme (métodos HTTP, códigos de estado, URLs de recursos) que actúa como puente entre cliente y servidor. El cliente envía solicitudes, el servidor envía respuestas, y ningún lado necesita entender el funcionamiento interno del otro.

Ejemplo

Streaming de Netflix: La aplicación móvil de Netflix (cliente) envía solicitudes a los servidores API de Netflix. El cliente maneja la UI, reproducción de video e interacciones del usuario. El servidor gestiona autenticación de usuarios, catálogos de contenido, recomendaciones y codificación de video. Netflix puede rediseñar completamente la UI de su app (lo cual hacen regularmente) sin cambiar código del servidor. También pueden escalar su infraestructura de servidores globalmente, cambiar proveedores CDN u optimizar sus algoritmos de recomendación sin lanzar nuevas versiones de la app.

Plataforma de E-commerce (Amazon): Tu app de compras de Amazon es el cliente - muestra productos, gestiona la UI del carrito y maneja navegación. Los servidores de Amazon gestionan gestión de inventario, precios, procesamiento de pedidos y pagos. Cuando Amazon rediseña su aplicación móvil, sus servidores siguen funcionando con versiones antiguas de la app. Cuando actualizan sus sistemas de gestión de almacenes, tu app continúa funcionando sin actualizaciones.

Aplicación de Clima: Una app de clima (cliente) llama a la API de OpenWeatherMap (servidor). La app decide cómo mostrar datos meteorológicos - gráficos, mapas, notificaciones. El servidor gestiona recolección de datos meteorológicos, almacenamiento, modelos de pronóstico. El desarrollador de la app puede crear versiones iOS, Android y web (clientes diferentes) todos usando la misma API de servidor. OpenWeatherMap puede mejorar sus modelos de pronóstico sin romper ninguna aplicación cliente.

Aplicación Bancaria: Tu aplicación de banca móvil es el cliente que presenta transacciones, permite transferencias y muestra balances. Los servidores del banco gestionan cuentas, procesan transacciones, hacen cumplir políticas de seguridad y mantienen libros contables. El banco puede migrar de mainframe a infraestructura en la nube (cambios de servidor) sin forzar a los clientes a actualizar sus apps. Los clientes pueden usar interfaces web, móvil o ATM (clientes diferentes) todos hablando con los mismos servidores backend.

Analogía

Restaurante con Servicio de Mesa: Tú (el cliente) te sientas en una mesa y haces pedidos desde un menú. La cocina (servidor) prepara tu comida. No necesitas saber cómo está organizada la cocina, qué equipo usan o dónde obtienen ingredientes. La cocina no necesita saber cómo organizarás la comida en tu plato o cómo la comerás. El menú (contrato API) es la interfaz - pides “ensalada César” y obtienes ensalada César, independientemente de si la cocina tiene un nuevo chef o nuevos electrodomésticos.

ATM y Banco: Un cajero automático (cliente) es la interfaz que usas para retirar efectivo. Los sistemas del banco (servidor) gestionan tu saldo de cuenta, registros de transacciones y seguridad. El ATM no almacena tu balance ni procesa la transacción - solo envía tu solicitud al servidor. El banco puede actualizar su base de datos, cambiar a nuevos sistemas de seguridad o consolidar sucursales sin reemplazar ATMs. Diferentes marcas de ATM (clientes diferentes) pueden acceder a los mismos sistemas bancarios a través de la interfaz estándar.

Biblioteca y Sistema de Catálogo: Usas la terminal de computadora de la biblioteca (cliente) para buscar libros. El sistema de base de datos de la biblioteca (servidor) almacena ubicaciones de libros, disponibilidad y registros de miembros. La terminal solo muestra resultados y acepta entrada. La biblioteca puede actualizar su base de datos, cambiar sistemas de almacenamiento o reorganizar estantes (cambios de servidor) sin cambiar las terminales. Pueden añadir aplicaciones móviles o búsqueda en sitio web (nuevos clientes) usando la misma base de datos backend.

Red Eléctrica: Tus dispositivos (clientes) se conectan a enchufes de pared y solicitan electricidad. La red eléctrica (servidor) genera, distribuye y gestiona el suministro de electricidad. Tus dispositivos no necesitan saber si la energía proviene de plantas de carbón, solar o nuclear. La compañía eléctrica puede cambiar métodos de generación, actualizar infraestructura o añadir capacidad sin que reemplaces tus dispositivos. Diferentes dispositivos (laptop, refrigerador, cargador de teléfono - clientes diferentes) todos usan la misma interfaz estandarizada (el enchufe).

Ejemplo de Código

// Separación Cliente-Servidor en la práctica
// El cliente y servidor son completamente independientes

// ===== CÓDIGO CLIENTE (App React) =====
// El cliente maneja UI, renderizado, interacciones del usuario
// El cliente NO sabe sobre base de datos, lógica de negocio o infraestructura del servidor

import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // El cliente hace una solicitud al servidor
    // Solo conoce el endpoint de la API, no cómo se almacenan o procesan los datos
    fetch(`https://api.example.com/users/${userId}`, {
      headers: {
        'Authorization': `Bearer ${localStorage.getItem('token')}`,
        'Accept': 'application/json'
      }
    })
      .then(response => response.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(error => {
        console.error('Error fetching user:', error);
        setLoading(false);
      });
  }, [userId]);

  // El cliente decide CÓMO mostrar los datos (responsabilidad de UI)
  if (loading) return <div>Loading...</div>;

  return (
    <div className="user-profile">
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

// ===== CÓDIGO SERVIDOR (Node.js/Express) =====
// El servidor maneja almacenamiento de datos, lógica de negocio, autenticación
// El servidor NO sabe sobre UI, cómo los clientes mostrarán datos o preferencias del usuario

const express = require('express');
const app = express();
const db = require('./database'); // Responsabilidad interna del servidor
const auth = require('./auth'); // Responsabilidad interna del servidor

app.get('/users/:id', auth.requireAuth, async (req, res) => {
  try {
    // El servidor maneja autenticación (lógica de negocio)
    const userId = req.params.id;

    // Verificar que el usuario tiene permiso para ver este perfil
    if (req.user.id !== userId && !req.user.isAdmin) {
      return res.status(403).json({ error: 'Forbidden' });
    }

    // El servidor maneja recuperación de datos (responsabilidad de almacenamiento de datos)
    // El cliente no sabe si esto es PostgreSQL, MongoDB o microservicios
    const user = await db.query(
      'SELECT id, name, email, avatar FROM users WHERE id = $1',
      [userId]
    );

    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }

    // El servidor envía representación de datos
    // No le importa si el cliente es móvil, web o CLI
    res.json({
      id: user.id,
      name: user.name,
      email: user.email,
      avatar: user.avatar
    });

  } catch (error) {
    console.error('Server error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// ===== BENEFICIOS DE LA SEPARACIÓN =====

// 1. Evolución independiente:
//    - El cliente puede cambiar de React a Vue sin cambios en el servidor
//    - El servidor puede cambiar de PostgreSQL a MongoDB sin cambios en el cliente

// 2. Múltiples clientes:
//    - App iOS, app Android, app web todas usan la misma API de servidor
//    - Cada cliente optimiza la UI para su plataforma

// 3. Escalabilidad:
//    - El servidor puede ser replicado, balanceado en carga, cacheado
//    - El cliente no necesita saber sobre la topología del servidor

// 4. Seguridad:
//    - El cliente nunca ve credenciales de base de datos o lógica de negocio
//    - El servidor hace cumplir la autorización independientemente del cliente

Diagrama

graph TB
    subgraph Client["Cliente (Responsabilidades de Interfaz de Usuario)"]
        UI[Renderizado de UI]
        State[Gestión de Estado]
        UX[Experiencia de Usuario]
        Display[Lógica de Visualización de Datos]
    end

    subgraph Interface["Interfaz API (Contrato)"]
        HTTP[Métodos HTTP
GET, POST, PUT, DELETE] URLs[URLs de Recursos
/users, /products] Headers[Cabeceras
Auth, Content-Type] Status[Códigos de Estado
200, 404, 500] end subgraph Server["Servidor (Responsabilidades de Datos y Lógica)"] Auth[Autenticación] BizLogic[Lógica de Negocio] DB[Almacenamiento de Datos] Processing[Procesamiento de Datos] end Client <-->|Solicitudes| Interface Interface <-->|Respuestas| Server subgraph Benefits["Beneficios de la Separación"] Ind[Evolución
Independiente] Multi[Múltiples Clientes
Un Servidor] Scale[Infraestructura
Escalable] Secure[Aislamiento de
Seguridad] end Interface -.-> Benefits

Notas de Seguridad

SECURITY NOTES

Requisitos Principales:

  • La separación cliente-servidor hace cumplir límites de seguridad.
  • Nunca confíes en la entrada del cliente - siempre valida y sanitiza en el servidor.
  • No implementes lógica de seguridad en código del cliente ya que puede ser evitada o inspeccionada.
  • El servidor debe autenticar cada solicitud independientemente (sin dependencia del estado del cliente).
  • Usa HTTPS para encriptar la comunicación cliente-servidor.
  • Implementa limitación de tasa y restricción de solicitudes en el servidor para prevenir abusos.

Mejores Prácticas:

  • Nunca expongas detalles de implementación interna del servidor (esquemas de base de datos, rutas de archivos, nombres de servicios internos) en respuestas API o mensajes de error.
  • Usa políticas CORS apropiadas para controlar qué clientes pueden acceder al servidor.
  • Implementa cabeceras Content-Security-Policy.
  • Almacena secretos y claves API solo del lado del servidor.
  • El código del lado del cliente es público y puede ser descompilado o inspeccionado.
  • Usa validación de sesión del lado del servidor incluso si el cliente envía un token que parece válido..

Mejores Prácticas

  1. Contrato API claro: Define la interfaz (endpoints, métodos, payloads) explícitamente usando OpenAPI o estándares similares
  2. Nunca confíes en el cliente: Valida todas las entradas, hace cumplir reglas de negocio y verifica permisos del lado del servidor
  3. Comunicación sin estado: El servidor no debe almacenar estado del cliente entre solicitudes (usa tokens, no sesiones)
  4. Versiona tu API: Permite que el servidor evolucione mientras soporta clientes antiguos elegantemente
  5. Retorna respuestas consistentes: Usa formatos estándar (JSON, XML) y códigos de estado HTTP
  6. Implementa manejo de errores apropiado: Retorna mensajes de error significativos sin exponer internos del servidor
  7. Soporta múltiples clientes: Diseña APIs para servir web, móvil y otras plataformas sin endpoints específicos del cliente
  8. Habilita despliegue independiente: Cliente y servidor deben desplegarse por separado sin coordinación
  9. Documenta exhaustivamente: Proporciona documentación API clara para que desarrolladores de clientes entiendan el contrato
  10. Usa API gateways: Centraliza responsabilidades transversales (autenticación, limitación de tasa, logging) en una capa de gateway

Errores Comunes

Acoplamiento fuerte: Crear endpoints adaptados a pantallas UI específicas (/getHomePageData) en lugar de APIs orientadas a recursos (/products, /users).

Lógica de negocio del lado del cliente: Implementar reglas de validación o autorización en código del cliente que deberían estar del lado del servidor.

Exponer internos del servidor: Retornar mensajes de error de base de datos o stack traces internos a los clientes.

Sesiones con estado: Almacenar estado del usuario en el servidor vinculado a sesiones del cliente viola la ausencia de estado REST y limita la escalabilidad.

Endpoints específicos del cliente: Crear APIs separadas para móvil vs web en lugar de una API unificada que sirva a todos los clientes.

Cambios que rompen compatibilidad: Modificar contratos API sin versionado, forzando a todos los clientes a actualizar simultáneamente.

Confiar en datos del cliente: Aceptar totales calculados por el cliente, permisos o datos sensibles sin verificación del lado del servidor.

Sin separación de responsabilidades: Mezclar lógica de presentación (renderizado HTML) con APIs de datos, previniendo clientes headless o móviles.

Estándares y RFCs

Términos Relacionados