Implementando la Capa de Infraestructura en Arquitectura Hexagonal con TypeScript
1. El Rol de la Infraestructura (Los Adaptadores)
En la Arquitectura Hexagonal (también conocida como Puertos y Adaptadores), la capa de infrastructure es la única que tiene permiso para interactuar con tecnologías concretas. Aquí es donde viven librerías como Express para el enrutamiento HTTP o Prisma para la persistencia de datos.
El objetivo principal de esta capa es implementar los contratos (Puertos) que definimos en la capa de Dominio y exponer las acciones que definimos en la capa de Aplicación.
Estructura del Directorio de Infraestructura
Dentro de nuestro módulo funcional (siguiendo el ejemplo de un módulo de "Cursos"), la estructura de la infraestructura se divide según su responsabilidad técnica:
src/
└── modules/
└── courses/
├── domain/
├── application/
└── infrastructure/
├── controllers/ # Controladores HTTP
├── repositories/ # Implementaciones de la base de datos
└── routes.ts # Definición de rutas del módulo
2. Implementando el Repositorio (Persistencia)
En la capa de Dominio establecimos una interfaz pura (ej. CourseRepository). Ahora, en la infraestructura, creamos una clase que implemente esa interfaz utilizando nuestra herramienta de base de datos preferida. Al separar esto, si el día de mañana cambias de un ORM a otro, la lógica de negocio ni se entera.
PrismaCourseRepository.ts
// src/modules/courses/infrastructure/repositories/PrismaCourseRepository.ts
import { PrismaClient } from "@prisma/client";
import { Course } from "../../domain/Course";
import { CourseRepository } from "../../domain/CourseRepository";
// Esta clase ADAPTA Prisma a nuestro contrato del Dominio
export class PrismaCourseRepository implements CourseRepository {
constructor(private readonly prisma: PrismaClient) {}
public async save(course: Course): Promise<void> {
await this.prisma.course.create({
data: {
id: course.id,
title: course.title,
userId: course.userId, // Mantenemos la integridad del tipo numérico
startDate: course.startDate,
endDate: course.endDate,
},
});
}
// Implementación de otros métodos de la interfaz...
public async findById(id: string): Promise<Course | null> {
const data = await this.prisma.course.findUnique({ where: { id } });
if (!data) return null;
return new Course(data.id, data.title, data.userId, data.startDate, data.endDate);
}
}
3. Exponiendo la Aplicación: El Controlador
El controlador es el punto de entrada HTTP. Su única responsabilidad es recibir la petición web (Request), extraer los datos, pasárselos al Caso de Uso (Application) y devolver una respuesta web (Response). No debe contener validaciones de negocio.
CourseController.ts
// src/modules/courses/infrastructure/controllers/CourseController.ts
import { Request, Response } from "express";
import { CreateCourseUseCase } from "../../application/CreateCourseUseCase";
export class CourseController {
// Inyectamos el caso de uso
constructor(private readonly createCourseUseCase: CreateCourseUseCase) {}
// Usamos arrow functions para no perder el contexto de 'this' en las rutas
public createCourse = async (req: Request, res: Response): Promise<void> => {
try {
const { id, title, userId, startDate, endDate } = req.body;
// Delegamos la acción a la capa de Aplicación
await this.createCourseUseCase.execute({
id,
title,
userId,
startDate: new Date(startDate),
endDate: new Date(endDate),
});
res.status(201).json({ message: "Curso creado exitosamente" });
} catch (error) {
// Un manejo de errores básico. En un proyecto real, aquí conectaríamos
// con nuestros manejadores de errores de la carpeta 'core'.
if (error instanceof Error) {
res.status(400).json({ error: error.message });
} else {
res.status(500).json({ error: "Error interno del servidor" });
}
}
};
}
4. Ensamblando el Módulo: Las Rutas
Finalmente, conectamos todas las piezas de nuestro módulo en un archivo de rutas. Aquí es donde instanciamos las dependencias (Inyección de Dependencias manual).
routes.ts
// src/modules/courses/infrastructure/routes.ts
import { Router } from "express";
import { PrismaClient } from "@prisma/client";
import { PrismaCourseRepository } from "./repositories/PrismaCourseRepository";
import { CreateCourseUseCase } from "../application/CreateCourseUseCase";
import { CourseController } from "./controllers/CourseController";
const router = Router();
const prisma = new PrismaClient();
// 1. Instanciar Infraestructura (Repositorio)
const courseRepository = new PrismaCourseRepository(prisma);
// 2. Instanciar Aplicación (Caso de Uso) inyectando la Infraestructura
const createCourseUseCase = new CreateCourseUseCase(courseRepository);
// 3. Instanciar el Controlador inyectando el Caso de Uso
const courseController = new CourseController(createCourseUseCase);
// 4. Definir la ruta HTTP
router.post("/courses", courseController.createCourse);
export default router;
De esta manera, tu módulo queda completamente empaquetado y listo para ser importado en el archivo principal de tu servidor (app.ts o index.ts). Si el proyecto escala, puedes añadir carpetas para otros adaptadores, como colas de mensajería o clientes HTTP (como Axios) para consumir APIs de terceros.
Gracias por leer.