{}const=>[]async()letfn</>var
DesarrolloWeb

Cómo construir una arquitectura de front-end para que el proyecto no muera en un año

¿Por qué algunos proyectos se convierten en código espagueti después de seis meses, mientras que otros viven y se desarrollan durante años? Analizamos los principios probados de la arquitectura de front-end: desde la estructura de carpetas hasta las capas de abstracción. Aprende a crear un código que sea fácil de escalar, probar y mantener.

К

Kodik

Autor

9 min de lectura

Imagina: estás comenzando un nuevo proyecto. El entusiasmo está en su apogeo, el código se escribe rápidamente. Después de un par de meses, ya has olvidado por qué necesitas esa extraña función en utils.js. Después de seis meses, añadir una nueva característica se convierte en una misión. Un año después, el proyecto se convierte en un pantano, donde cada cambio puede romperlo todo.

¿Te suena? Este es el resultado clásico de la falta de una arquitectura bien pensada. Veamos cómo construir proyectos de front-end que vivirán y se desarrollarán durante años.

¿Por qué mueren los proyectos?

Antes de hablar de soluciones, entendamos las principales causas de muerte de los proyectos:

Código espagueti. Todo está conectado con todo. Un cambio en un lugar rompe los otros tres componentes.

Falta de estructura. Los archivos están dispersos de forma caótica. Encontrar el componente correcto es una expedición arqueológica.

Duplicación de la lógica. La misma funcionalidad se implementa en cinco lugares diferentes de cinco maneras diferentes.

No hay documentación. Incluso tú mismo no recordarás cómo funciona tu código en un mes.

Deuda técnica. «Lo arreglaré más tarde» se convierte en «nunca lo arreglaré».

🔥 100.000+ estudiantes ya están con nosotros

¿Cansado de leer teoría?
¡Hora de programar!

Kodik — una app donde aprendes a programar con práctica. Mentor IA, lecciones interactivas, proyectos reales.

🤖 IA 24/7
🎓 Certificados
💰 Gratis
🚀 Empezar
Se unieron hoy

Fundamentos: estructura de carpetas correcta

Una buena arquitectura comienza con la organización de archivos. Aquí hay una estructura probada para la mayoría de los proyectos:

src/
├── components/          # Componentes reutilizados
│   ├── ui/             # Elementos básicos de la interfaz de usuario (botones, entradas)
│   ├── layout/         # Componentes de diseño (encabezado, pie de página)
│   └── features/       # Componentes de negocio
├── pages/              # Páginas de la aplicación
├── services/           # Trabajar con la API
├── store/              # Estado global (Vuex, Redux, Pinia)
├── utils/              # Funciones auxiliares
├── hooks/              # Ganchos personalizados (para React)
├── composables/        # Funciones compuestas (para Vue)
├── types/              # Tipos de TypeScript
├── constants/          # Constantes de la aplicación
└── assets/             # Recursos estáticos

Principio: cada carpeta es responsable de un área de responsabilidad. Cuando necesitas un componente de interfaz de usuario, vas a components/ui. Necesitas una función para trabajar con la API: en services.

Principio de responsabilidad única

Cada módulo debe hacer una cosa, pero hacerlo bien.

Mal:

// UserCard.js - lo hace todo a la vez
function UserCard({ userId }) {
  const [user, setUser] = useState(null);
  
  // Recopilación de datos
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]);
  
  // Validación
  const isValid = user?.email && user?.name;
  
  // Formato
  const formattedDate = new Date(user?.created).toLocaleDateString();
  
  // Y más renderizado...
  return <div>...</div>;
}

Bien:

// services/userService.js
export const fetchUser = async (userId) => {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
};

// utils/validation.js
export const validateUser = (user) => {
  return user?.email && user?.name;
};

// utils/dateFormatter.js
export const formatDate = (date) => {
  return new Date(date).toLocaleDateString();
};

// components/UserCard.js - solo visualización
function UserCard({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);
  
  if (!validateUser(user)) return null;
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <span>{formatDate(user.created)}</span>
    </div>
  );
}

Ahora cada función se prueba por separado, se utiliza en diferentes lugares y se modifica fácilmente.

Capas de abstracción: divide y vencerás

La buena arquitectura se construye en capas, como una cebolla:

1. Capa de datos (Data Layer)

Responsable de recibir y enviar datos. Aquí viven todas las solicitudes de API.

// services/api/userApi.js
const API_BASE = 'https://api.example.com';

export const userApi = {
  getUser: (id) => fetch(`${API_BASE}/users/${id}`).then(r => r.json()),
  updateUser: (id, data) => fetch(`${API_BASE}/users/${id}`, {
    method: 'PUT',
    body: JSON.stringify(data)
  }),
  deleteUser: (id) => fetch(`${API_BASE}/users/${id}`, { method: 'DELETE' })
};

2. Capa de lógica empresarial (Business Logic Layer)

Procesa datos, aplica reglas de lógica empresarial.

// services/userService.js
import { userApi } from './api/userApi';

export const userService = {
  async getActiveUsers() {
    const users = await userApi.getUsers();
    return users.filter(user => user.isActive);
  },
  
  async promoteToAdmin(userId) {
    const user = await userApi.getUser(userId);
    if (!user.email.endsWith('@company.com')) {
      throw new Error('Only company emails can be admins');
    }
    return userApi.updateUser(userId, { role: 'admin' });
  }
};

3. Capa de estado (State Layer)

Gestiona el estado global de la aplicación.

// store/userStore.js (Pinia/Vue)
export const useUserStore = defineStore('user', {
  state: () => ({
    currentUser: null,
    users: []
  }),
  
  actions: {
    async loadUser(id) {
      this.currentUser = await userService.getUser(id);
    }
  }
});

4. Capa de presentación (Presentation Layer)

Componentes que muestran datos al usuario.

// components/UserProfile.vue
<script setup>
import { useUserStore } from '@/store/userStore';

const userStore = useUserStore();
const { currentUser } = storeToRefs(userStore);

onMounted(() => {
  userStore.loadUser(route.params.id);
});
</script>

<template>
  <div v-if="currentUser">
    <h1>{{ currentUser.name }}</h1>
    <p>{{ currentUser.email }}</p>
  </div>
</template>

Regla de oro: las capas superiores pueden usar las inferiores, pero no al revés. Los componentes usan store, store usa services, pero services nunca importa componentes.

Composición en lugar de herencia

En el front-end moderno, la composición supera a la herencia. En lugar de clases base gigantes, crea pequeñas funciones reutilizables.

Ejemplo con React Hooks:

// hooks/useLocalStorage.js
export function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });
  
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);
  
  return [value, setValue];
}

// hooks/useDebounce.js
export function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(handler);
  }, [value, delay]);
  
  return debouncedValue;
}

// Uso
function SearchComponent() {
  const [query, setQuery] = useLocalStorage('searchQuery', '');
  const debouncedQuery = useDebounce(query, 500);
  
  // Ahora tienes una búsqueda con debounce y almacenamiento en localStorage
  useEffect(() => {
    if (debouncedQuery) {
      searchApi(debouncedQuery);
    }
  }, [debouncedQuery]);
}

La tipificación es tu mejor amiga

TypeScript puede parecer redundante para los principiantes, pero salvará el proyecto de muchos problemas.

// types/user.ts
export interface User {
  id: number;
  name: string;
  email: string;
  role: 'user' | 'admin' | 'moderator';
  createdAt: Date;
}

export interface ApiResponse<T> {
  data: T;
  error?: string;
  status: number;
}

// services/userService.ts
export async function getUser(id: number): Promise<ApiResponse<User>> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

Ahora el IDE sugerirá los campos disponibles y TypeScript detectará errores en la fase de desarrollo, no en producción.

Configuración y constantes

No esparzas números y cadenas mágicas por todo el código. Reúnalos en un solo lugar.

// constants/config.js
export const API_CONFIG = {
  BASE_URL: process.env.VITE_API_URL || 'https://api.example.com',
  TIMEOUT: 5000,
  RETRY_ATTEMPTS: 3
};

export const UI_CONSTANTS = {
  ITEMS_PER_PAGE: 20,
  DEBOUNCE_DELAY: 300,
  TOAST_DURATION: 3000
};

export const ROUTES = {
  HOME: '/',
  PROFILE: '/profile',
  SETTINGS: '/settings'
};

Cuando necesites cambiar el número de elementos en una página, sabrás dónde hacerlo.

Tratamiento de errores

Un enfoque sistemático de los errores es un signo de una arquitectura madura.

// utils/errorHandler.js
export class ApiError extends Error {
  constructor(message, status, data) {
    super(message);
    this.status = status;
    this.data = data;
  }
}

export async function handleApiCall(apiFunction) {
  try {
    return await apiFunction();
  } catch (error) {
    if (error instanceof ApiError) {
      // Mostramos al usuario un mensaje claro
      toast.error(error.message);
      
      // Registro para desarrolladores
      console.error('API Error:', error.status, error.data);
    } else {
      // Error inesperado
      toast.error('Something went wrong. Please try again later.');
      console.error('Unexpected error:', error);
    }
    throw error;
  }
}

// Uso
async function loadUserData(userId) {
  await handleApiCall(async () => {
    const user = await userApi.getUser(userId);
    if (!user) {
      throw new ApiError('User not found', 404);
    }
    return user;
  });
}

Documenta las soluciones arquitectónicas

Crea un archivo ARCHITECTURE.md en la raíz del proyecto:

# Arquitectura del proyecto

# # Estructura
- `/components` - переиспользуемые компоненты
- `/pages` - страницы приложения
- `/services` - бизнес-логика и API

# # Acuerdos
- Компоненты именуются в PascalCase
- Утилиты и сервисы в camelCase
- Константы в SCREAMING_SNAKE_CASE

# # Capas
1. API Layer (services/api/)
2. Business Logic (services/)
3. State Management (store/)
4. UI Components (components/)

# # Decisiones importantes
- Используем Pinia для состояния
- Axios для HTTP запросов
- День.js для работы с датами

Revisión de código y linting

Configura ESLint y Prettier a la vez. Esto evitará el 90% de los problemas de legibilidad del código.

// .eslintrc.js
module.exports = {
  rules: {
    'no-console': 'warn',
    'no-unused-vars': 'error',
    'complexity': ['error', 10], // Advierte sobre funciones complejas
    'max-lines-per-function': ['warn', 50]
  }
};

Pruebas de arquitectura

Una buena arquitectura es fácil de probar.

// userService.test.js
import { userService } from './userService';
import { userApi } from './api/userApi';

jest.mock('./api/userApi');

test('promoteToAdmin rejects external emails', async () => {
  userApi.getUser.mockResolvedValue({
    id: 1,
    email: 'external@gmail.com'
  });
  
  await expect(
    userService.promoteToAdmin(1)
  ).rejects.toThrow('Only company emails');
});

Si tus funciones son difíciles de probar, es una señal de que la arquitectura es deficiente.

Escalabilidad: estructura basada en características

Cuando el proyecto crezca, agrupa el código por características, no por tipos de archivos:

src/
├── features/
│   ├── auth/
│   │   ├── components/
│   │   ├── services/
│   │   ├── store/
│   │   └── types/
│   ├── products/
│   │   ├── components/
│   │   ├── services/
│   │   └── store/
│   └── cart/
│       ├── components/
│       └── store/
└── shared/              # Componentes y utilidades comunes

Ahora todo lo relacionado con la autorización está en una carpeta. Fácil de encontrar, fácil de eliminar, fácil de transferir a otro desarrollador.

¡Errores frecuentes de los principiantes!

Optimización prematura

No es necesario construir de inmediato una arquitectura para un millón de usuarios. Comienza con una solución simple pero ampliable.

Abstracción excesiva

Si solo tienes un botón, no necesitas crear un sistema de cinco clases base para él.

Ignorar las convenciones

Utiliza las prácticas comunes de tu marco. No reinventes la rueda.

Ausencia de refactorización

Dedique tiempo a mejorar la arquitectura. La deuda técnica se acumula sin que nos demos cuenta.

Consejos prácticos.

Empieza con el README. Describa cómo funciona el proyecto antes de escribir el código. Esto le hará pensar en la arquitectura.

Refactoriza regularmente. Reserva una hora a la semana para mejorar el código existente.

Aprende de los demás. Estudia proyectos de código abierto, mira cómo están organizados.

No tengas miedo de rehacer. Si la estructura no funciona, es mejor corregirla ahora que vivir con ella durante años.

Herramientas de ayuda.

  • ESLint/Prettier — formato automático del código

  • Husky - verificación del código antes de la confirmación

  • TypeScript - tipificación para grandes proyectos

  • Storybook — desarrollo de componentes de forma aislada

  • Jest/Vitest — pruebas de lógica

Conclusión

Una buena arquitectura no es un código perfecto desde la primera vez. Es un enfoque sistémico que permite que el proyecto evolucione. Comienza con una estructura simple pero lógica. Sigue el principio de responsabilidad única. Documente las decisiones importantes. Y lo más importante, revise y mejore su código con regularidad.

Dentro de un año, te agradecerás a ti mismo por cada minuto invertido en arquitectura. Tu proyecto vivirá, se desarrollará y traerá alegría, y no se convertirá en una bola de espagueti que da miedo tocar.

Esto y mucho más se puede estudiar en Codice — analizar todo en detalle y consolidar la práctica con tareas. No solo te enseñamos a escribir código, sino a crear aplicaciones correctas y escalables que durarán muchos años.

Y si necesitas ayuda, ya tenemos más de 2000 personas con ideas afines en un canal activo de Telegram, donde puedes hacer cualquier pregunta, discutir soluciones arquitectónicas y obtener una revisión de tu código por parte de desarrolladores experimentados.

¡Únete a la comunidad donde crecen los verdaderos profesionales! 🚀

🎯Deja de postergar

¿Te gustó el artículo?
¡Hora de practicar!

En Kodik no solo lees — escribes código de inmediato. Teoría + práctica = habilidades reales.

Práctica instantánea
🧠IA explica código
🏆Certificado

Sin registro • Sin tarjeta