Arquitectura Hexagonal
La arquitectura hexagonal es una forma práctica de aplicar Clean Architecture. La aplicación define puertos; la infraestructura aporta adaptadores.
El objetivo es que la lógica importante no dependa de cómo entra una petición ni de dónde se guardan los datos.
Capas
Las ventajas es tener código desacoplado y separar lógica de negocio de los detalles de implementación.

Presentación
Esta capa no aparece en el diagrama, pero iría por fuera de la Infraestructura. Es la capa donde viven las vistas, componentes y la UI en general.
Infraestructura
Capa de entrada/salida. Llamadas a BD, fetching de datos…
Aplicación
Donde viven los casos de uso. Aquí irían los métodos que hacen cosas
Dominio
El dominio es donde están las entidades, repositorios e interfaces. User, Event, Product…
Regla de dependencias
Basándonos en estas capas, las dependencias van de fuera a dentro. De Infra podemos importar de App y Dominio, pero de Dominio no podemos importar de fuera.
El dominio define lo que necesita. La infraestructura decide cómo hacerlo.
Puertos y adaptadores
Un puerto es una interfaz que expresa una capacidad:
- Buscar códigos postales.
- Guardar un usuario.
- Enviar un email.
- Leer un archivo.
Un adaptador es la implementación concreta:
- Repositorio con Prisma.
- Cliente HTTP.
- SDK de Stripe.
- Servicio SMTP.
La aplicación habla con puertos. La infraestructura aporta adaptadores.
Uso en NextJS
Es una movida pero lo he conseguido sacar y me gusta.

La estructura sería esta:
Dominio
Aquí establecemos interfaces y schemas.
import { PostalCode } from "./schemas/postalCode"
export interface PostalCodeRepository {
search(postalCode: string): Promise<PostalCode[]>
validate(postalCode: string): Promise<PostalCode>
}import { z } from "zod"
export const PostalCodeSchema = z.object({
code: z.string(),
})
export type PostalCode = z.infer<typeof PostalCodeSchema>Application
Aquí establecemos los ‘casos de uso’. Es decir, los métodos que aplican la lógica de negocio, como searchPostalCode o isValidPostalCode por ejemplo.
En este ejemplo concreto usamos una técnica llamada currying que consiste en despiezar una función que acepta múltiples argumentos en una serie de callbacks en las que cada una acepta un único argumento.
import { PostalCodeRepository } from "../domain/PostalCodeRepository"
export function searchPostalCode(postalCodeRepository: PostalCodeRepository) {
return async function (postalCode: string) {
return await postalCodeRepository.search(postalCode)
}
}Infrastructure
Aquí es donde van los ‘detalles de implementación’. Es decir, donde añadimos la implementación técnica en la que conectamos a una BD, hacemos un fetch y gestionamos entradas y salidas a través de puertos y adaptadores.
Testing
La ventaja práctica es que cada pieza se puede probar en el nivel adecuado:
- Dominio: tests unitarios puros, sin mocks ni framework.
- Application: tests de casos de uso con puertos fake o in-memory.
- Infrastructure: tests de integración contra base de datos, APIs o filesystem.
- Presentation: tests de interacción o E2E para flujos críticos.