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é».
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áticosPrincipio: 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 comunesAhora 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! 🚀
