ecosystem-wikicómo extender los templates sin romper sus convenciones
‹ todas las recipes

Las cuatro capas, en dos minutos

template-api
último cambio 2026-09-25

La api está partida en cuatro capas. Lo único que hay que interiorizar es que el núcleo no mira hacia afuera: domain/ no depende de nadie y application/ solo depende de domain/. Las dos capas de fuera —presentation/ e infrastructure/— sí se ven entre ellas, y eso es normal.

presentation  →  application  →  domain
       ↓                            ↑
infrastructure  ────────────────────┘
   (implementa lo que domain y application declaran)
Capa Qué guarda Qué NO puede hacer
domain/ Entidades, interfaces de repositorio, errores de negocio Importar nada: ni NestJS, ni Prisma, ni librerías
application/ Use cases, ports, eventos, handlers Importar de infrastructure/ o de presentation/
infrastructure/ Prisma, MinIO, Redis, argon2, pdfmake, HTTP saliente Contener reglas de negocio
presentation/ Controllers, DTOs, guards, módulos de Nest Pedir datos de negocio sin pasar por un use case

Ojo con esa flecha de presentation a infrastructure: apunta hacia abajo porque sí existe, y conviene saber para qué, que si no parece una grieta. Son 5 ficheros de 169, y de dos tipos:

Lo que no ocurre nunca es que un controller pida datos de negocio saltándose la aplicación.

Las dos reglas que no se rompen

Estas dos no son aspiraciones, se cumplen hoy en todo el repo y conviene comprobarlas antes de dar por buena una feature:

  1. Ningún controller inyecta un repositorio. Si un controller necesita datos, pasa por un use case. Cero excepciones.
  2. Nada de application/ importa de infrastructure/ ni de presentation/. Un use case no sabe que existe Prisma, ni que existe HTTP. Cero excepciones.

Son fáciles de verificar y sirven de test de humo al revisar un cambio:

grep -rl "REPOSITORY" src/presentation/            # debe salir vacío
grep -rl "@/infrastructure\|@/presentation" src/application/   # debe salir vacío

¿Cómo se invierte la dependencia?

domain/ declara qué necesita, infrastructure/ implementa cómo, y Nest los une por un Symbol. Ese símbolo vive junto a la interfaz, nunca junto a la implementación:

// domain/pet-tags/pet-tag.repository.ts
export interface PetTagRepository {
  findByPublicCode(publicCode: string): Promise<PetTag | null>;
  create(tag: PetTag): Promise<PetTag>;
}
export const PET_TAG_REPOSITORY = Symbol("PetTagRepository");
// application/pet-tags/create-pet-tag.use-case.ts
constructor(
  @Inject(PET_TAG_REPOSITORY) private readonly petTags: PetTagRepository,
) {}

Fíjate en el detalle del import en el use case, porque es obligatorio y da un error raro si se olvida: la interfaz se importa con import type y el símbolo con un import normal.

import type { PetTagRepository } from "@/domain/pet-tags/pet-tag.repository";
import { PET_TAG_REPOSITORY } from "@/domain/pet-tags/pet-tag.repository";

Si importas la interfaz sin type, TypeScript lanza TS1272: con isolatedModules no puede saber que ese nombre desaparece al compilar, y el decorador @Inject intentaría emitir metadata sobre algo que no existe en tiempo de ejecución.

Por dónde sigue