🎯 Introducción
En el mundo del desarrollo frontend, especialmente con React, es común ver proyectos que comienzan con una estructura simple pero que, con el tiempo, se convierten en un código difícil de mantener, testear y escalar. La lógica de negocio se mezcla con componentes de UI, los servicios se acoplan directamente a las APIs, y los tests se vuelven complejos de escribir.
¿Te suena familiar? Si has experimentado estos desafíos, la Arquitectura Hexagonal (también conocida como Arquitectura de Puertos y Adaptadores) combinada con Vertical Slicing puede ser la solución que estás buscando.
En este artículo, te mostraré cómo implementar esta poderosa combinación arquitectónica en React con TypeScript, usando un proyecto real de gestión de tareas (Todos) como ejemplo. No solo hablaremos de teoría, sino que veremos código real, decisiones de diseño y ventajas prácticas que aplicarás desde el día uno.
📚 ¿Qué es la Arquitectura Hexagonal?
La Arquitectura Hexagonal fue propuesta por Alistair Cockburn en 2005. Su objetivo principal es desacoplar la lógica de negocio de los detalles técnicos de implementación.
Principios fundamentales:
- El dominio es el centro: La lógica de negocio no depende de nada externo
- Inversión de dependencias: Las capas externas dependen de las internas, nunca al revés
- Puertos y Adaptadores: Interfaces abstractas (puertos) y implementaciones concretas (adaptadores)
- Testeable por diseño: La lógica de negocio se puede probar sin UI, sin base de datos, sin APIs
Representación visual:
┌─────────────────────────────────────────────────┐
│ INFRASTRUCTURE LAYER │
│ ┌──────────────────────────────────────────┐ │
│ │ APPLICATION LAYER │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ DOMAIN LAYER │ │ │
│ │ │ • Entities │ │ │
│ │ │ • Business Rules │ │ │
│ │ │ • Domain Exceptions │ │ │
│ │ │ • Ports (Interfaces) │ │ │
│ │ └──────────────────────────────────┘ │ │
│ │ • Use Cases / Services │ │
│ │ • Application Logic │ │
│ └──────────────────────────────────────────┘ │
│ • Adapters (Repositories, API clients) │
│ • UI Components (React) │
│ • State Management (Zustand) │
│ • Frameworks & Libraries │
└─────────────────────────────────────────────────┘
🏗️ Estructura del Proyecto
Nuestra aplicación sigue esta estructura de carpetas:
src/
└── features/
└── todos/
├── domain/ # 🔵 Capa de Dominio
│ ├── entities/
│ │ └── todo.entity.ts
│ ├── enums/
│ │ └── todo-status.enum.ts
│ ├── exceptions/
│ │ ├── domain.exception.ts
│ │ ├── invalid-todo.exception.ts
│ │ ├── invalid-todo-status-transition.exception.ts
│ │ └── todo-not-found.exception.ts
│ └── ports/
│ └── todo.repository.ts
│
├── application/ # 🟢 Capa de Aplicación
│ └── services/
│ ├── todo.service.ts
│ └── dtos/
│ └── create-todo.dto.ts
│
└── infrastructure/ # 🟡 Capa de Infraestructura
├── adapters/
│ └── in-memory-todo.repository.ts
├── factories/
│ └── todo-service.factory.ts
├── stores/
│ └── todo.store.ts
└── ui/
├── components/
│ ├── TodoItem.tsx
│ ├── TodosList.tsx
│ └── TodoStatus.tsx
└── pages/
├── CreateTodoPage.tsx
├── TodoDetailPage.tsx
└── TodosPage.tsx
Esta estructura separa claramente las responsabilidades y facilita el mantenimiento a largo plazo.
� Vertical Slicing: Organización por Features
Además de la Arquitectura Hexagonal, este proyecto implementa Vertical Slicing (Corte Vertical), un patrón de organización que estructura el código por features completas en lugar de por capas técnicas.
¿Qué es Vertical Slicing?
En lugar de organizar el código por tipo técnico (todos los componentes juntos, todos los servicios juntos), lo organizamos por capacidad de negocio. Cada feature contiene todo lo necesario para funcionar de forma independiente:
❌ Horizontal (Tradicional): ✅ Vertical (Por Feature):
src/ src/
├── components/ └── features/
│ ├── TodoItem.tsx ├── todos/
│ ├── UserProfile.tsx │ ├── domain/
│ └── ProductCard.tsx │ ├── application/
├── services/ │ └── infrastructure/
│ ├── todoService.ts ├── users/
│ ├── userService.ts │ ├── domain/
│ └── productService.ts │ ├── application/
└── types/ │ └── infrastructure/
├── Todo.ts └── products/
├── User.ts ├── domain/
└── Product.ts ├── application/
└── infrastructure/
Ventajas de Vertical Slicing
1. Alta Cohesión, Bajo Acoplamiento
Todo lo relacionado con "Todos" está en un solo lugar. No tienes que saltar entre 10 carpetas diferentes para entender una funcionalidad.
2. Desarrollo en Paralelo Eficiente
Dos desarrolladores pueden trabajar en todos y users simultáneamente sin colisiones de merge, ya que están en carpetas completamente separadas.
3. Fácil de Eliminar o Extraer
¿Ya no necesitas la feature de todos? Elimina la carpeta features/todos/ y nada más se rompe. ¿Quieres convertirla en un micro-frontend? Extraela como un paquete independiente.
4. Onboarding Simplificado
Un nuevo desarrollador puede entender la feature completa navegando una sola carpeta, sin necesidad de conocer toda la aplicación.
5. Escalabilidad Real
Con 50+ features, es mucho más manejable tener 50 carpetas de features que 50 archivos en cada carpeta técnica global.
6. Decisiones Técnicas por Feature
Cada feature puede tener sus propias decisiones de implementación. Por ejemplo:
-
todosusa repositorio en memoria -
userspodría usar API REST -
analyticspodría usar WebSockets
Vertical Slicing + Hexagonal = 💪
La combinación de Vertical Slicing (organización por features) con Arquitectura Hexagonal (organización por capas dentro de cada feature) te da lo mejor de ambos mundos:
- Aislamiento de features (Vertical)
- Separación de responsabilidades (Hexagonal)
- Código predecible y consistente
�🔵 Capa 1: Dominio (El Corazón del Sistema)
El dominio contiene la lógica de negocio pura, sin dependencias externas. Es el código más importante y debe ser lo más estable posible.
1.1 Enums: Estados del Todo
// domain/enums/todo-status.enum.ts
export enum TodoStatus {
PENDING = "PENDING",
IN_PROGRESS = "IN_PROGRESS",
COMPLETED = "COMPLETED",
}
⚠️ Decisión de diseño: Usar enums en lugar de strings mágicos previene errores y hace el código más mantenible.
1.2 Entidad: Todo
La entidad Todo es una clase rica en comportamiento, no solo un contenedor de datos:
// domain/entities/todo.entity.ts
export interface TodoProps {
id: number;
title: string;
description: string;
status: TodoStatus;
createdAt: Date;
updatedAt: Date;
}
export default class Todo {
private readonly id: number;
private title: string;
private description: string;
private status: TodoStatus;
private readonly createdAt: Date;
private updatedAt: Date;
constructor(props: TodoProps) {
this.validateProps(props);
this.id = props.id;
this.title = props.title;
this.description = props.description;
this.status = props.status;
this.createdAt = props.createdAt;
this.updatedAt = props.updatedAt;
}
private validateProps(props: TodoProps): void {
if (!props.title.trim()) {
throw new InvalidTodoException("title cannot be empty.");
}
if (!props.description.trim()) {
throw new InvalidTodoException("description cannot be empty.");
}
}
// ✅ Getters para mantener inmutabilidad
getId(): number { return this.id; }
getTitle(): string { return this.title; }
getStatus(): TodoStatus { return this.status; }
// ✅ Setters con validación
setTitle(title: string): void {
if (!title.trim()) {
throw new InvalidTodoException("title cannot be empty.");
}
this.title = title;
this.touchUpdatedAt();
}
// ✅ Lógica de negocio: transiciones de estado
start(): void {
if (this.status === TodoStatus.IN_PROGRESS) {
throw new InvalidTodoStatusTransitionException(
this.status,
TodoStatus.IN_PROGRESS
);
}
if (this.status === TodoStatus.COMPLETED) {
throw new InvalidTodoStatusTransitionException(
this.status,
TodoStatus.IN_PROGRESS
);
}
this.status = TodoStatus.IN_PROGRESS;
this.touchUpdatedAt();
}
complete(): void {
if (this.status === TodoStatus.PENDING) {
throw new InvalidTodoStatusTransitionException(
this.status,
TodoStatus.COMPLETED
);
}
if (this.status === TodoStatus.COMPLETED) {
throw new InvalidTodoStatusTransitionException(
this.status,
TodoStatus.COMPLETED
);
}
this.status = TodoStatus.COMPLETED;
this.touchUpdatedAt();
}
// ✅ Clonación para evitar mutaciones
clone(): Todo {
return new Todo({
id: this.id,
title: this.title,
description: this.description,
status: this.status,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
});
}
private touchUpdatedAt(): void {
this.updatedAt = new Date();
}
}
🎨 Ventajas de esta implementación:
- Validación centralizada: Las reglas de negocio están en un solo lugar
-
Inmutabilidad: Propiedades
readonlyy métodoclone() - Encapsulación: Los campos son privados, acceso controlado por getters/setters
- Transiciones de estado seguras: No puedes pasar de PENDING a COMPLETED directamente
- Self-documenting: El código explica claramente qué operaciones son válidas
1.3 Excepciones de Dominio
Las excepciones son tipadas y descriptivas:
// domain/exceptions/domain.exception.ts
export class DomainException extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}
// domain/exceptions/invalid-todo-status-transition.exception.ts
export class InvalidTodoStatusTransitionException extends DomainException {
constructor(from: string, to: string) {
super(`Cannot transition from "${from}" to "${to}".`);
}
}
✅ Beneficio: Los errores son claros y pueden manejarse específicamente en capas superiores.
1.4 Puertos: Interfaces
Los puertos son contratos que define el dominio pero que implementa la infraestructura:
// domain/ports/todo.repository.ts
import type Todo from "../entities/todo.entity";
export default interface TodoRepository {
create(todo: Todo): Promise<void>;
update(todo: Todo): Promise<void>;
delete(id: number): Promise<void>;
list(): Promise<Todo[]>;
findById(id: number): Promise<Todo | null>;
}
🔑 Clave: El dominio define QUÉ necesita, no CÓMO se implementa. Esto es la inversión de dependencias en acción.
🟢 Capa 2: Aplicación (Casos de Uso)
La capa de aplicación orquesta la lógica de dominio. Aquí viven los servicios que implementan casos de uso.
2.1 DTOs (Data Transfer Objects)
Los DTOs definen la estructura de datos para comunicación entre capas:
// application/services/dtos/create-todo.dto.ts
export interface CreateTodoDto {
title: string;
description: string;
}
2.2 Servicio de Aplicación
// application/services/todo.service.ts
import Todo from "../../domain/entities/todo.entity";
import { TodoStatus } from "../../domain/enums/todo-status.enum";
import { TodoNotFoundException } from "../../domain/exceptions/todo-not-found.exception";
import type TodoRepository from "../../domain/ports/todo.repository";
import type { CreateTodoDto } from "./dtos/create-todo.dto";
export default class TodoService {
constructor(private readonly todoRepository: TodoRepository) {}
async list(): Promise<Todo[]> {
return this.todoRepository.list();
}
async findById(id: number): Promise<Todo | null> {
return this.todoRepository.findById(id);
}
async create(dto: CreateTodoDto): Promise<Todo> {
const now: Date = new Date();
const todo: Todo = new Todo({
id: now.getTime(),
title: dto.title,
description: dto.description,
status: TodoStatus.PENDING,
createdAt: now,
updatedAt: now,
});
await this.todoRepository.create(todo);
return todo;
}
async delete(id: number): Promise<void> {
const todo: Todo | null = await this.todoRepository.findById(id);
if (!todo) throw new TodoNotFoundException(id);
await this.todoRepository.delete(id);
}
async start(id: number): Promise<Todo> {
const todo: Todo | null = await this.todoRepository.findById(id);
if (!todo) throw new TodoNotFoundException(id);
const cloned: Todo = todo.clone(); // 🔑 Evitamos mutaciones
cloned.start();
await this.todoRepository.update(cloned);
return cloned;
}
async complete(id: number): Promise<Todo> {
const todo: Todo | null = await this.todoRepository.findById(id);
if (!todo) throw new TodoNotFoundException(id);
const cloned: Todo = todo.clone(); // 🔑 Evitamos mutaciones
cloned.complete();
await this.todoRepository.update(cloned);
return cloned;
}
}
💡 Observaciones importantes:
- Inyección de dependencias: El repositorio se inyecta por el constructor
- Uso de clones: Nunca mutamos directamente la entidad obtenida del repositorio
- Validaciones de existencia: Lanzamos excepciones si el todo no existe
- Capa delgada: Solo orquesta, no contiene lógica de negocio compleja
🟡 Capa 3: Infraestructura (Detalles Técnicos)
Esta capa contiene las implementaciones concretas de los puertos y toda la integración con el mundo exterior.
3.1 Adaptador: Repositorio en Memoria
// infrastructure/adapters/in-memory-todo.repository.ts
import Todo from "../../domain/entities/todo.entity";
import { TodoStatus } from "../../domain/enums/todo-status.enum";
import type TodoRepository from "../../domain/ports/todo.repository";
export default class InMemoryTodoRepository implements TodoRepository {
private todos: Todo[] = [
new Todo({
id: 1,
title: "Learn TypeScript",
description: "Study the basics of TypeScript and its features",
status: TodoStatus.PENDING,
createdAt: new Date(),
updatedAt: new Date(),
}),
// ... más todos de ejemplo
];
async create(todo: Todo): Promise<void> {
this.todos = [...this.todos, todo];
}
async update(updatedTodo: Todo): Promise<void> {
this.todos = this.todos.map((todo: Todo) =>
todo.getId() === updatedTodo.getId() ? updatedTodo : todo,
);
}
async delete(id: number): Promise<void> {
this.todos = this.todos.filter((todo: Todo) => todo.getId() !== id);
}
async list(): Promise<Todo[]> {
return this.todos;
}
async findById(id: number): Promise<Todo | null> {
const todo: Todo | undefined = this.todos.find(
(todo: Todo) => todo.getId() === id,
);
return todo ?? null;
}
}
🔄 Ventaja clave: Mañana puedes crear un ApiTodoRepository que haga llamadas HTTP y solo cambias la configuración de la factory. Tu dominio y aplicación no cambiarán ni una línea.
3.2 Factory Pattern: Inyección de Dependencias
// infrastructure/factories/todo-service.factory.ts
import TodoService from "../../application/services/todo.service";
import type TodoRepository from "../../domain/ports/todo.repository";
import InMemoryTodoRepository from "../adapters/in-memory-todo.repository";
export default class TodoServiceFactory {
private static instance: TodoService | null = null;
static getInstance(): TodoService {
if (!this.instance) {
const repository: TodoRepository = new InMemoryTodoRepository();
this.instance = new TodoService(repository);
}
return this.instance;
}
}
💡 Pattern aplicado: Singleton + Factory para gestionar la creación y configuración de servicios.
3.3 State Management con Zustand
// infrastructure/stores/todo.store.ts
import { create } from "zustand";
import type Todo from "../../domain/entities/todo.entity";
import type { CreateTodoDto } from "../../application/services/dtos/create-todo.dto";
import TodoServiceFactory from "../factories/todo-service.factory";
interface TodoStore {
todos: Todo[];
loadingTodos: boolean;
error: string | null;
listTodos: () => Promise<void>;
findTodoById: (id: number) => Promise<Todo | null>;
createTodo: (dto: CreateTodoDto) => Promise<void>;
deleteTodo: (id: number) => Promise<void>;
startTodo: (id: number) => Promise<void>;
completeTodo: (id: number) => Promise<void>;
}
const useTodoStore = create<TodoStore>()((set, get) => ({
todos: [],
loadingTodos: false,
error: null,
listTodos: async () => {
set({ loadingTodos: true, error: null });
try {
const todos: Todo[] = await TodoServiceFactory.getInstance().list();
set({ todos });
} catch (error) {
set({ error: (error as Error).message });
} finally {
set({ loadingTodos: false });
}
},
createTodo: async (dto: CreateTodoDto) => {
const todo: Todo = await TodoServiceFactory.getInstance().create(dto);
set({ todos: [...get().todos, todo] });
},
startTodo: async (id: number) => {
const updated: Todo = await TodoServiceFactory.getInstance().start(id);
set({
todos: get().todos.map((todo: Todo) =>
todo.getId() === id ? updated : todo,
),
});
},
completeTodo: async (id: number) => {
const updated: Todo = await TodoServiceFactory.getInstance().complete(id);
set({
todos: get().todos.map((todo: Todo) =>
todo.getId() === id ? updated : todo,
),
});
},
deleteTodo: async (id: number) => {
await TodoServiceFactory.getInstance().delete(id);
set({ todos: get().todos.filter((todo: Todo) => todo.getId() !== id) });
},
}));
export default useTodoStore;
✅ Zustand actúa como un adaptador entre React y nuestra aplicación. Los componentes no conocen la arquitectura interna, solo el store.
3.4 Componentes React
Los componentes UI son tontos y reutilizables:
// infrastructure/ui/components/TodoItem.tsx
export default function TodoItem({ todo }: TodoItemProps): JSX.Element {
const { startTodo, completeTodo, deleteTodo } = useTodoStore();
return (
<div className="group p-5 rounded-xl bg-white border border-slate-200">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<Link
to={`/todos/${todo.getId()}`}
className="text-lg font-semibold text-slate-800"
>
{todo.getTitle()}
</Link>
<p className="text-sm text-slate-500 mt-1">
{todo.getDescription()}
</p>
</div>
<TodoStatus status={todo.getStatus()} />
</div>
<div className="flex items-center gap-2 mt-4">
{todo.getStatus() === TodoStatus.PENDING && (
<button onClick={() => startTodo(todo.getId())}>
Start
</button>
)}
{todo.getStatus() === TodoStatus.IN_PROGRESS && (
<button onClick={() => completeTodo(todo.getId())}>
Complete
</button>
)}
<button onClick={() => deleteTodo(todo.getId())}>
Delete
</button>
</div>
</div>
);
}
🎯 Componentes como presentadores: Solo muestran datos y delegan acciones al store.
🧪 Testing: El Gran Beneficio
La Arquitectura Hexagonal brilla especialmente en testing. Cada capa se puede testear de forma aislada.
Test de Entidad (Dominio)
// tests/domain/todo.entity.test.ts
describe("Todo Entity", () => {
it("should transition from PENDING to IN_PROGRESS", () => {
const todo = new Todo({
id: 1,
title: "Test",
description: "Test description",
status: TodoStatus.PENDING,
createdAt: new Date(),
updatedAt: new Date(),
});
todo.start();
expect(todo.getStatus()).toBe(TodoStatus.IN_PROGRESS);
});
it("should throw error when transitioning from PENDING to COMPLETED", () => {
const todo = createTodo({ status: TodoStatus.PENDING });
expect(() => todo.complete()).toThrow(
InvalidTodoStatusTransitionException
);
});
});
✅ Sin mocks, sin setup complejo: Testing de lógica de negocio pura.
Test de Servicio (Aplicación)
// tests/application/todo.service.test.ts
describe("TodoService", () => {
let repository: TodoRepository;
let service: TodoService;
beforeEach(() => {
repository = new InMemoryTodoRepository();
service = new TodoService(repository);
});
it("should create a new todo with PENDING status", async () => {
await service.create({
title: "New Todo",
description: "New description"
});
const todos: Todo[] = await service.list();
expect(todos.length).toBe(4);
expect(todos[3].getStatus()).toBe(TodoStatus.PENDING);
});
it("should return a cloned instance when starting a todo", async () => {
const original: Todo | null = await service.findById(1);
const updated: Todo = await service.start(1);
expect(updated).not.toBe(original); // Diferentes instancias
expect(updated.getStatus()).toBe(TodoStatus.IN_PROGRESS);
});
});
✅ Test de integración ligero: Usamos el repositorio real (en memoria) sin necesidad de mocks complejos.
Test de UI (Infraestructura)
// tests/ui/todo-item.test.tsx
import { render, screen } from "@testing-library/react";
import { BrowserRouter } from "react-router";
import TodoItem from "../../features/todos/infrastructure/ui/components/TodoItem";
describe("TodoItem", () => {
it("should render todo with title and description", () => {
const todo = new Todo({
id: 1,
title: "Test Todo",
description: "Test Description",
status: TodoStatus.PENDING,
createdAt: new Date(),
updatedAt: new Date(),
});
render(
<BrowserRouter>
<TodoItem todo={todo} />
</BrowserRouter>
);
expect(screen.getByText("Test Todo")).toBeInTheDocument();
expect(screen.getByText("Test Description")).toBeInTheDocument();
});
});
🚀 Ventajas de Esta Arquitectura
1. Mantenibilidad
- Cada capa tiene una responsabilidad clara
- El código es fácil de entender y modificar
- Los cambios están contenidos en su capa
2. Testabilidad Superior
- Lógica de negocio sin dependencias externas
- Tests rápidos y confiables
- No necesitas montar toda la app para testear una regla de negocio
3. Flexibilidad de Implementación
- Cambiar de in-memory a API REST: Solo cambia el adaptador
- Migrar de Zustand a Redux: Solo afecta a infraestructura
- Reemplazar React por Vue: Dominio y aplicación intactos
4. Escalabilidad
- Agregar nuevas features sigue el mismo patrón
- Múltiples desarrolladores pueden trabajar en paralelo
- Estructura clara para proyectos grandes
5. Independencia de Frameworks
- React es solo un detalle de implementación
- La lógica de negocio no está acoplada a ningún framework
- Puedes reutilizar dominio y aplicación en otros contextos (mobile, backend, etc.)
6. Mejora la Colaboración
- Domain-Driven Design natural
- Lenguaje ubicuo entre negocio y desarrollo
- Documentación implícita en la estructura
7. Vertical Slicing: Organización Natural
- Código organizado por capacidades de negocio, no por tipo técnico
- Features autocontenidas y fáciles de navegar
- Desarrollo en paralelo sin conflictos
- Features desacopladas que se pueden eliminar o extraer sin romper nada
- Onboarding de desarrolladores más rápido y efectivo
📊 Comparación: Antes vs. Después
🔴 Arquitectura Tradicional de React
src/
├── components/
│ └── TodoItem.tsx # 500 líneas mezclando UI, lógica, validaciones
├── pages/
│ └── TodosPage.tsx # Llama directamente a fetch(), validaciones inline
├── utils/
│ └── api.ts # Funciones sueltas
└── types/
└── Todo.ts # Interface simple sin comportamiento
Problemas:
- ❌ Lógica de negocio dispersa en componentes
- ❌ Difícil de testear (necesitas montar React)
- ❌ Acoplamiento directo a APIs
- ❌ Validaciones duplicadas
- ❌ Cambios en backend requieren tocar muchos archivos
- ❌ Features mezcladas: para entender "todos" debes revisar múltiples carpetas
- ❌ Conflictos de merge constantes en los mismos archivos
🟢 Con Arquitectura Hexagonal
src/features/todos/
├── domain/ # La verdad de tu negocio
├── application/ # Orquestación de casos de uso
└── infrastructure/ # Detalles técnicos reemplazables
Beneficios:
- ✅ Lógica centralizada y testeable
- ✅ Componentes simples y reutilizables
- ✅ Cambios tecnológicos contenidos
- ✅ Validaciones consistentes
- ✅ Tests rápidos y confiables
- ✅ Features autocontenidas en
features/todos/ - ✅ Desarrollo en paralelo sin colisiones
- ✅ Fácil de eliminar o extraer features completas
🎯 Cuándo Usar Arquitectura Hexagonal + Vertical Slicing
✅ Ideal para:
- Aplicaciones empresariales con lógica de negocio compleja
- Proyectos que escalarán con el tiempo
- Equipos medianos a grandes (el Vertical Slicing facilita el trabajo en paralelo)
- Aplicaciones con múltiples integraciones (APIs, localStorage, WebSockets)
- Proyectos que requieren alta cobertura de tests
- Cuando la lógica de negocio debe ser compartida (web, mobile, desktop)
- Aplicaciones con múltiples features independientes que pueden crecer con el tiempo
⚠️ Considera alternativas si:
- Es un MVP rápido que probablemente se descartará
- Aplicación muy simple (landing page, blog estático)
- Equipo muy pequeño con plazos muy ajustados
- El overhead inicial es mayor que los beneficios esperados
🛠️ Stack Tecnológico del Proyecto
- React 19: UI components
- TypeScript 5.9: Type safety y mejor DX
- Vite 7: Build tool moderno y rápido
- Vitest 4: Testing framework compatible con Vite
- React Router 7: Client-side routing
- Zustand 5: State management minimalista
- TailwindCSS 4: Utility-first CSS (sin clases, pure CSS)
📝 Conclusiones
La combinación de Arquitectura Hexagonal + Vertical Slicing en React no es solo una moda pasajera; es una inversión en la sostenibilidad a largo plazo de tu aplicación. Aunque requiere más estructura inicial, los beneficios en mantenibilidad, testabilidad y flexibilidad son invaluables.
Key Takeaways:
- Domain First: Empieza por modelar tu dominio, no por la UI
- Inversión de Dependencias: Las capas externas dependen de las internas
- Puertos y Adaptadores: Define interfaces, implementa después
- Vertical Slicing: Organiza por features, no por tipo técnico
- Test, Test, Test: La arquitectura hace el testing natural
- Pragmatismo: Adapta la arquitectura a tu contexto, no al revés
Próximos Pasos:
Si quieres profundizar, considera explorar:
- Value Objects para modelar conceptos de dominio más complejos
- CQRS (Command Query Responsibility Segregation) para separar lecturas y escrituras
- Event Sourcing si necesitas auditoría completa
- Domain Events para comunicación entre agregados
- Specification Pattern para consultas complejas
🙏 Agradecimientos
Este artículo y el proyecto de ejemplo son el resultado de la experiencia aplicando Clean Architecture y Domain-Driven Design en proyectos reales de React.
Un agradecimiento especial a:
- Alistair Cockburn por concebir la Arquitectura Hexagonal
- Robert C. Martin (Uncle Bob) por popularizar Clean Architecture
- Eric Evans por Domain-Driven Design, que complementa perfectamente esta arquitectura
- La comunidad de React por crear un ecosistema increíble que permite implementar estos patrones
- Los desarrolladores de TypeScript por hacer que JavaScript sea predecible y seguro
- Todos los developers que comparten conocimiento y ayudan a elevar el nivel de la industria
- Y el más importante para mi hermosa familia que es mi motor. Mazikeen, Brian y Fátima los amo ❤️
Sobre el Autor
Carlos Martinez es Ingeniero en Sistemas Computacionales y Desarrollador Web Full Stack con 3 años de experiencia en la industria. Ha trabajado implementando arquitecturas limpias y escalables en proyectos de producción, con especial enfoque en React y TypeScript.
Este proyecto fue desarrollado como material educativo para demostrar la implementación práctica de Arquitectura Hexagonal en aplicaciones React modernas, basándose en experiencia real aplicando estos patrones en entornos profesionales. El código completo está disponible en GitHub bajo licencia MIT https://github.com/Carlosgmdev/react-hexagonal.
¿Preguntas o Feedback?
Si implementaste esta arquitectura en tu proyecto o tienes dudas, me encantaría escuchar tu experiencia. La mejor forma de aprender es compartiendo conocimiento.
Happy coding! 🚀👨💻
📚 Referencias y Recursos Adicionales
- Hexagonal Architecture - Alistair Cockburn
- Clean Architecture - Robert C. Martin
- Domain-Driven Design - Eric Evans
- React Documentation
- TypeScript Handbook
Última actualización: Febrero 2026
#React #TypeScript #CleanArchitecture #HexagonalArchitecture #VerticalSlicing #SoftwareEngineering #Frontend #DomainDrivenDesign
Top comments (0)