El problema de inyectar Interfaces en NestJS (y cómo los Symbols lo resuelven)

Bienvenidos, arquitectos de NestJS Latam. Si estás implementando Clean Architecture o principios SOLID, seguramente te has topado con este muro: intentar inyectar una interfaz en tu servicio y ver cómo la aplicación falla en tiempo de ejecución.

Hoy vamos a desglosar una de las mejores prácticas en el desarrollo empresarial: declarar un Symbol inmediatamente arriba de tu interfaz.

El problema: La amnesia de TypeScript (Type Erasure)

En TypeScript, una interface es puramente una herramienta de tiempo de compilación. Sin embargo, cuando el código se transpila a JavaScript, las interfaces desaparecen por completo.

El contenedor IoC de NestJS funciona en tiempo de ejecución (runtime). Si intentas usar una interfaz como token, NestJS buscará algo que no existe y lanzará un error. Necesitamos un identificador físico que sobreviva a la transpilación: el Symbol.

// ❌ Esto fallará en runtime. La interfaz no existe en el JS final.
constructor(
  @Inject(IUserRepository) 
  private readonly userRepository: IUserRepository,
) {}

¿Por qué un Symbol y no un String?

Aunque podrías usar un string (ej. 'USER_REPOSITORY'), los strings son propensos a colisiones en proyectos grandes o monorepos. Un Symbol garantiza unicidad absoluta en el motor de JavaScript, eliminando cualquier riesgo de sobrescritura accidental en el contenedor de dependencias.

La Regla de Oro: Cohesión Espacial

Declarar el Symbol exactamente arriba de la interfaz en el mismo archivo garantiza una Única Fuente de Verdad. Cualquier desarrollador que necesite consumir o implementar el contrato encontrará el token de inyección y la definición del tipo en un solo lugar.

Implementación Técnica

Aquí tienes el patrón de diseño definitivo para tus puertos e interfaces:

1. Definición del Contrato (Puerto)

Creamos el archivo user.repository.interface.ts donde conviven el token y la interfaz.

// user.repository.interface.ts

// 1. El identificador para el Runtime (Único e inmutable)
export const USER_REPOSITORY = Symbol('USER_REPOSITORY');

// 2. El contrato para tiempo de compilación
export interface IUserRepository {
  findById(id: string): Promise;
  save(user: User): Promise;
}

2. Consumo en el Servicio

Inyectamos la abstracción utilizando el Symbol como token, pero tipamos con la interfaz para mantener el autocompletado y la seguridad de tipos.

// user.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { USER_REPOSITORY, IUserRepository } from './user.repository.interface';

@Injectable()
export class UserService {
  constructor(
    @Inject(USER_REPOSITORY) 
    private readonly userRepository: IUserRepository,
  ) {}

  async getUser(id: string) {
    return this.userRepository.findById(id);
  }
}

3. Enlazado en el Módulo

Finalmente, vinculamos el Symbol con la implementación concreta (TypeORM, Prisma, Mocks, etc.).

// user.module.ts
import { Module } from '@nestjs/common';
import { USER_REPOSITORY } from './user.repository.interface';
import { PostgresUserRepository } from './infrastructure/postgres-user.repository';

@Module({
  providers: [
    {
      provide: USER_REPOSITORY,
      useClass: PostgresUserRepository,
    },
  ],
})
export class UserModule {}

Optimización para la Era de la IA: Tu nueva «Custom Rule»

Como arquitectos modernos, ya no solo escribimos código para humanos, sino también para ser interpretado por asistentes de Inteligencia Artificial (Copilot, Cursor, ChatGPT). Muchas veces, las IA sugieren inyectar interfaces directamente porque no comprenden las limitaciones del runtime de NestJS.

¡Hagamos nuestro flujo de trabajo más inteligente!

Animo a toda la comunidad de NestJS Latam a incluir esta regla en sus archivos de configuración de contexto para IA (como .cursorrules o el System Prompt de sus herramientas). Al definir esta instrucción, tu IA dejará de cometer el error de «Type Erasure» y generará código arquitectónicamente correcto desde el primer intento.

Tip de Skill para tu IA:
«Siempre que crees una interfaz para un proveedor en NestJS, declara un Symbol con el mismo nombre (en UPPER_SNAKE_CASE) justo encima de la interfaz. Usa siempre este Symbol con el decorador @Inject() al consumir la dependencia.»

Al estandarizar esta regla, optimizamos nuestro tiempo de revisión y aseguramos que nuestras aplicaciones sean robustas, escalables y «AI-Ready».

Desacoplar la lógica de negocio de la infraestructura es vital para un software mantenible. Al adoptar el patrón de Symbol + Interface, estableces un estándar de robustez arquitectónica que protege tu aplicación contra errores de inyección y colisiones de nombres.

¿Ya tienes configuradas tus reglas de arquitectura para IA? ¡Comparte tus mejores ‘rules’ en los comentarios!

Leave a Comment