Inicio / Artículos / DI de frontend agnóstico al framework con InversifyJS y una raíz de composición

DI de frontend agnóstico al framework con InversifyJS y una raíz de composición

Cómo elegir un contenedor DI para TypeScript, empaquetar cada dominio como un ContainerModule, conectar todo en una raíz de composición y vincularlo a React, Vue y Angular.

3230 palabras

Los grandes conjuntos de código frontend rara vez fallan debido a un único componente defectuoso; fallan porque las reglas de negocio se entrelazan silenciosamente con los hooks de React, los clientes HTTP y las APIs de almacenamiento hasta el punto en que ya no se puede probar ni reutilizar nada por separado. La inyección de dependencias es el mecanismo a nivel bajo que mantiene esas reglas separadas, pero solo si la estructura de conexiones se diseña intencionadamente. Esta guía explica cómo elegir un contenedor para un monorepo de TypeScript multidominio, definir tokens seguros desde el punto de vista tipológico, aislar cada dominio detrás de un único módulo, armar el grafo en una raíz de composición y exponerlo a React, Vue y Angular mediante puentes de unas doce líneas cada uno.

Por qué la isolación debe decidirse antes de modelar el dominio

El consejo clásico del diseño orientado a dominios indica que los detalles de implementación deben posponerse: primero se modelan las entidades y los casos de uso, y luego se eligen las herramientas. En la práctica, un equipo que inicia un gran cambio arquitectónico se beneficia al resolver de antemano una cuestión técnica, a saber, cómo se aplicará realmente el aislamiento de módulos. Sin un mecanismo acordado, los primeros dominios se estructuran de forma improvisada y las convenciones nunca se restablecen.

El principio subyacente es el Principio de Inversión de Dependencias: las políticas de alto nivel deben depender de abstracciones, y los detalles concretos deben depender de esas mismas abstracciones. La inyección de dependencias es la técnica práctica que lo hace posible. Las clases solicitan tokens abstractos en lugar de implementaciones concretas, y algo externo decide a qué se resuelven esos tokens. En DDD y Arquitectura Limpia esto no es un detalle opcional; el dominio no debe saber con qué framework de interfaz de usuario, cliente HTTP o capa de persistencia funciona.

La mayoría de los equipos front-end recurren primero a React Context (o a provide/inject en Vue). Parece suficiente: se declara un valor cerca de la raíz y se puede leer en cualquier parte más abajo. Sin embargo, las limitaciones aparecen rápidamente: no hay gestión de ciclo de vida, ni concepto de fábricas o ámbitos, y cada consumidor del valor queda vinculado al entorno de ejecución de React, por lo que la lógica no puede ejecutarse ni probarse sin él. El camino desde ese punto de partida hasta una verdadera raíz de composición es lo que aborda el resto de esta guía. Esporádicamente vale la pena crear un contenedor personalizado, por lo que el primer paso es evaluar con honestidad las opciones de IoC existentes.

Criterios para un contenedor DI front-end

Los frameworks backend como NestJS, Spring y .NET incluyen la inyección de dependencias como una solución estándar ya implementada. El código frontend tiene restricciones adicionales: costo en tiempo de ejecución, límites de tamaño del paquete y la necesidad de funcionar con varios frameworks de interfaz de usuario. Cinco criterios guían el proceso de evaluación:

  1. Agnosticismo hacia el framework. El contenedor debe ejecutarse con TypeScript puro, sin dependencias en tiempo de ejecución de React, Vue o Angular, y funcionar tanto en el navegador como en Node.js para SSR, e incluso en React Native.
  2. Modularidad. Cada dominio debe poder entregar un módulo preconfigurado con sus vinculaciones encapsuladas, de modo que la aplicación solo lo cargue. Esto evita tener un archivo main.ts de 500 líneas donde se registren manualmente todos los servicios.
  3. Gestión del ciclo de vida. Los singulares, las instancias transitorias y la resolución por ámbito deben ser funcionalidades de primer nivel.
  • Practicabilidad de pruebas. Sustituir una dependencia por un mock o falso debería tomar solo una línea, sin necesidad de reconstruir el contenedor.
  • Efecto en el paquete. La carga adicional en producción debe ser mínima, tanto en su versión original como minificada y comprimida con gzip.
  • Los candidatos

    InversifyJS

    InversifyJS ha sido durante mucho tiempo la opción por defecto para grandes proyectos en TypeScript y es el contenedor IoC más completo del ecosistema. Es independiente de frameworks y declara las dependencias mediante decoradores. Su ContainerModule agrupa los enlaces en unidades lógicas, y admite inSingletonScope, inTransientScope y inRequestScope, contenedores hijos, carga y descarga de módulos en tiempo de ejecución, así como ganchos de activación. El precio es su peso: fue la opción con mayor carga, y describir las versiones actuales como ligeras sería engañoso.

    TSyringe

    TSyringe, mantenido por Microsoft, sacrifica la amplitud de funcionalidades en favor de la conveniencia. Resuelve automáticamente las dependencias de constructores a partir del metadato de TypeScript, ofrece un amplio conjunto de decoradores y abarca los ciclos de vida habituales (Singleton, Transient, ResolutionScoped, ContainerScoped). Para una aplicación de tamaño medio, representa una configuración atractiva y sencilla. Sin embargo, presenta dos desventajas en arquitecturas multidominio: no existe un concepto de módulo de primera clase, por lo que la composición debe construirse manualmente mediante funciones de registro o createChildContainer(), y depende del polyfill reflect-metadata, lo que añade un peso considerable.

    Awilix

    Awilix evita por completo los decoradores. Utiliza la API ES6 Proxy para asociar las dependencias con los nombres de los parámetros del constructor o con las claves del objeto de argumentos de la fábrica. Esto lo hace una opción ideal para equipos que no desean usar decoradores, y fue el contenedor independiente más pequeño evaluado. Soporta modos de vida SINGLETON, SCOPED y TRANSIENT; los módulos se organizan mediante funciones de registro personalizadas.

    Existe una trampa específica del frontend que merece atención. En el modo de inyección CLASSIC, Awilix lee los nombres de los argumentos del constructor como cadenas, y los minificadores renombran esos argumentos, lo que provoca errores en entornos de producción. Solo el modo predeterminado PROXY resiste este procesamiento agresivo.

    El inyector integrado de Angular

    Angular cuenta con uno de los sistemas de inyección dependencial jerárquica más potentes en el desarrollo frontend, con inyección basada en tokens, proveedores con ámbito definido y carga diferida integrados. Si pudiera utilizarse sin el entorno de ejecución de Angular, sería un competidor serio para la capa de dominio. No es posible, y ese es el problema.

    React Context

    Contexto en realidad no es un contenedor de inyección dependencial; es una forma de evitar el problema del “prop drilling”. Un proveedor se encuentra cerca de la raíz y los componentes lo acceden mediante useContext(). Cualquier cambio en el valor proporcionado hace que se vuelvan a renderizar todos los consumidores, y no existe ciclo de vida, ni vinculación de fábrica ni aislamiento de ámbito. Su única ventaja real es que no añade costo adicional al paquete.

    Vue provide e inject

    El mecanismo de Vue transmite valores a lo largo del árbol de componentes y comparte las limitaciones arquitectónicas del Context: no constituye una base práctica para un grafo de servicios de dominio. Sin embargo, tiene una ventaja desde el punto de vista ergonómico, ya que las dependencias pueden registrarse globalmente a través de un plugin en lugar de envolver el árbol en proveedores anidados. La función moderna inject() de Angular se basa en una idea similar de un contexto de inyección activo.

    Inyección directa mediante constructores

    El enfoque sin bibliotecas transmite explícitamente cada dependencia a través de los constructores. Es completamente seguro desde el punto de vista tipológico y no genera sobrecarga en tiempo de ejecución. La desventaja es la escalabilidad: una vez que una aplicación cuenta con entre 10 y 15 servicios de dominio, conectar manualmente el grafo en el punto de entrada se convierte en un archivo grande y frágil al que todos temen tocar.

    Lo que muestra la comparación

    Se compararon los tamaños de los paquetes agrupando un archivo de entrada que importa la API pública de cada paquete con esbuild --bundle --minify --platform=browser y comprimiendo el resultado con gzip -9, lo que permite aislar la carga adicional del contenedor del código de la aplicación. Se obtuvieron cuatro conclusiones:

    1. El DI de frameworks pertenece a la interfaz de usuario, no al dominio. Incluir useContext() o @Injectable() de Angular en las bibliotecas del dominio une las reglas de negocio a un único framework, incumpliendo así el primer criterio.
    2. La inyección manual solo funciona en proyectos pequeños. En un monorepo empresarial grande, el punto de composición se convierte en miles de líneas de conexiones escritas a mano.
  • InversifyJS ganó por su enfoque modular. Es el único candidato en el que un módulo (ContainerModule) constituye un valor de primera clase que una biblioteca de dominio puede crear, encapsular y exportar como una sola unidad.
  • La configuración de compilación es importante. Tanto InversifyJS como TSyringe requieren experimentalDecorators: true, emitDecoratorMetadata: true y target: ES2022 en tsconfig.json. InversifyJS v8 ya no necesita un polyfill separado de reflect-metadata. Con herramientas basadas en esbuild o SWC (Vite, Next.js, Bun), hay que asegurarse de que la fase de transformación soporte los metadatos de decoradores antiguos, generalmente a través de un plugin, ya que esbuild por sí solo no emite dichos metadatos.
  • Cuatro reglas para evitar que el contenedor se convierta en un localizador de servicios

    Un contenedor por sí solo no aísla nada; si se utiliza de forma descuidada, se convierte en una bolsa global de servicios a la que cualquier archivo puede acceder. Por lo tanto, la arquitectura se basa en cuatro convenciones:

    • Tokens abstractos de inyección de dependencias creados con Symbol.for y una propiedad $.
    • Cada biblioteca de dominio exporta únicamente su módulo de contenedor, nunca sus clases concretas.
    • Un único punto de composición reúne el grafo de la aplicación.
    • Un puente ligero para cada framework de UI expone el contenedor a los componentes.

    Tokens seguros desde el punto de vista tipológico con Symbol.for y $

    Un contenedor necesita identificadores en tiempo de ejecución para asociar abstracciones con implementaciones. Las interfaces de TypeScript desaparecen en tiempo de compilación, por lo que no pueden cumplir esa función directamente. El patrón utilizado aquí empareja cada interfaz en un paquete @my-app/*-contracts con una constante del mismo nombre cuya propiedad $ contiene un Symbol.

    // libs/auth/contracts/src/lib/interfaces/auth.facade.ts
    import type { ServiceIdentifier } from 'inversify';
    
    export interface AuthFacade {
      getAccessToken(): Promise<string | null>;
      login(): Promise<void>;
      logout(): Promise<void>;
    }
    export const AuthFacade = {
      $: Symbol.for('AuthFacade') as ServiceIdentifier<AuthFacade>,
    };
    
    // libs/auth/contracts/src/lib/interfaces/pin.facade.ts
    export interface PinFacade {
      setupPin(pin: string): Promise<void>;
      verifyPin(sub: string, pin: string): Promise<PinVerifyResult>;
      changePin(sub: string, currentPin: string, newPin: string): Promise<PinVerifyResult>;
    }
    export const PinFacade = {
      $: Symbol.for('PinFacade') as ServiceIdentifier<PinFacade>,
    };
    

    Ese fragmento contiene tres decisiones clave:

    1. Un único nombre para el tipo y el valor. TypeScript mantiene los tipos y los valores en nombres de espacio separados, por lo que AuthFacade puede ser tanto la interfaz como el portador del token. El compilador selecciona el significado adecuado según el contexto, y nadie necesita inventar nombres como AuthFacadeToken.
  • Symbol.for en lugar de Symbol(). Cada llamada a Symbol() devuelve un valor completamente nuevo, mientras que Symbol.for() busca la clave en el registro global de símbolos del entorno de ejecución. Si un paquete se duplica en dos bundles, como puede ocurrir en un monorepo o con Module Federation, ambas copias siguen generando el mismo token y las búsquedas continúan funcionando en lugar de fallar misteriosamente. La desventaja es que las claves del registro son cadenas globales, por lo que deben ser únicas en toda la aplicación.
  • El casteo a ServiceIdentifier<AuthFacade>. Al escribir $ como ServiceIdentifier<T> de Inversify, el token se vincula a su interfaz en tiempo de compilación, de modo que los sitios de resolución deducen el tipo correcto sin necesidad de generics explícitos.
  • El siguiente fragmento muestra ambos aspectos: una clase que recibe la fachada mediante inyección por constructor, y una llamada directa a container.get cuyo tipo de retorno se infiere del token.

    import { inject, injectable } from 'inversify';
    import { AuthFacade } from '@my-app/auth-contracts';
    
    @injectable()
    export class LoginPageComponent {
      // Strongly-typed injection via token
      constructor(@inject(AuthFacade.$) private readonly auth: AuthFacade) {}
    }
    
    // Resolution site automatically infers return type as AuthFacade
    const facade = container.get(AuthFacade.$); // AuthFacade
    

    El consumidor solo hace referencia al contrato. No tiene idea de qué hay dentro de @my-app/auth-core, y ese es precisamente el objetivo.

    Un dominio, un ContainerModule

    La biblioteca central de cada dominio expone una sola cosa: su módulo DI. Los casos de uso, entidades, puertos y repositorios permanecen privados. Dentro del módulo, los casos de uso internos están vinculados entre sí, mientras que la fachada pública está vinculada a su token de contrato.

    // libs/auth/core/src/lib/auth-container.module.ts
    import { ContainerModule, type ContainerModuleLoadOptions } from 'inversify';
    import { AuthFacade } from '@my-app/auth-contracts';
    import { CoreAuthFacade } from './facades/core-auth.facade';
    import { LoginUseCase } from './use-cases/login.use-case';
    import { LogoutUseCase } from './use-cases/logout.use-case';
    import { GetAccessTokenUseCase } from './use-cases/get-access-token.use-case';
    
    export const authContainerModule = new ContainerModule((options: ContainerModuleLoadOptions) => {
      // Private use cases (registered to self within domain)
      options.bind(LoginUseCase).toSelf().inSingletonScope();
      options.bind(GetAccessTokenUseCase).toSelf().inSingletonScope();
      options.bind(LogoutUseCase).toSelf().inSingletonScope();
      // Public contract implementation bound to token
      options.bind(AuthFacade.$).to(CoreAuthFacade).inSingletonScope();
    });
    
    // libs/auth/core/src/index.ts - ONLY the ContainerModule is re-exported!
    export * from './lib/auth-container.module';
    

    La encapsulación se aplica en dos ocasiones:

    1. API pública. El archivo index.ts de la biblioteca solo vuelve a exportar authContainerModule. Clases como LoginUseCase o CoreAuthFacade simplemente no son accesibles desde otros paquetes.
    2. Reglas de linting. La regla ESLint @nx/enforce-module-boundaries de Nx verifica las etiquetas del proyecto; por ejemplo, un proyecto etiquetado como type:core no puede importar el proyecto de otro dominio con la misma etiqueta type:core. Consulte la documentación sobre los límites de módulos de Nx para saber cómo se declaran las etiquetas y las restricciones.

    El límite de exportación evita importaciones accidentales; la regla de linting impide atajos intencionados a través de rutas complejas. Juntos, hacen que la arquitectura sea algo que verifica el CI en lugar de algo que los revisores deben recordar.

    La raíz de composición

    La Raíz de Composición es el único lugar donde se crea el grafo de dependencias, típicamente apps/my-app/src/composition-root.ts o una función llamada desde main.ts. Existe una regla que la rige:

    La Raíz de Composición es el único lugar en la base de código al que se le permite importar implementaciones concretas, adaptadores de infraestructura y módulos centrales del dominio.

    La función a continuación primero vincula la infraestructura global (un cliente HTTP basado en Axios y un registrador creado mediante una fábrica para que pueda recibir transportes configurados), y luego carga los módulos del dominio.

    // apps/my-app/src/composition-root.ts
    import { Container } from 'inversify';
    import { HttpClientPort, LoggerPort } from '@my-app/shared-contracts';
    import { AxiosHttpClient, BrowserLogger, ConsoleLogTransport, FileLogTransport } from '@my-app/shared-infrastructure';
    
    import { authContainerModule } from '@my-app/auth-core';
    import { paymentsContainerModule } from '@my-app/payments-core';
    export const initAppContainer = (): Container => {
      const container = new Container();
    
      // 1. Bind global infrastructure adapters
      container.bind(HttpClientPort.$).to(AxiosHttpClient).inSingletonScope();
      container.bind(LoggerPort.$).toDynamicValue(() => new BrowserLogger({
        transports: [
          new ConsoleLogTransport(),
          new FileLogTransport(),
        ],
      })).inSingletonScope();
    
      // 2. Load domain modules
      container.load(authContainerModule);
      container.load(paymentsContainerModule);
    
      return container;
    };
    

    Para mayor claridad, el adaptador BrowserLogger es una clase ordinaria que implementa LoggerPort y distribuye cada mensaje a sus transportes correspondientes. Cabe señalar que no contiene ningún decorador; como se crea dentro de toDynamicValue, el contenedor nunca necesita inspeccionar su constructor.

    // libs/shared/infrastructure/src/lib/logging/browser-logger.ts
    import type { LoggerPort } from '@my-app/shared-contracts';
    import type { LogTransport } from './interfaces/log-transport.interface';
    
    export interface BrowserLoggerOptions {
      transports: LogTransport[];
    }
    
    export class BrowserLogger implements LoggerPort {
      constructor(private readonly options: BrowserLoggerOptions) {}
    
      info(message: string, ...args: unknown[]): void {
        this.options.transports.forEach(t => t.write('INFO', message, args));
      }
    }
    

    Varias aplicaciones desde un núcleo de dominio

    Dado que la infraestructura se elige en la Raíz de Composición, diferentes aplicaciones pueden reutilizar los mismos paquetes del dominio con adaptadores distintos y una selección diferente de módulos. Una aplicación React Native podría utilizar almacenamiento basado en AsyncStorage, mientras que una aplicación administrativa web emplea IndexedDB y carga un dominio de auditoría en lugar de uno relacionado con pagos.

    // apps/mobile/src/composition-root.ts — React Native target
    container.bind(StoragePort.$).to(AsyncStorageAdapter).inSingletonScope();
    container.bind(LoggerPort.$).to(RnLogger).inSingletonScope();
    container.load(authContainerModule);
    container.load(paymentsContainerModule);
    
    // apps/admin/src/composition-root.ts - Web Admin target
    container.bind(StoragePort.$).to(IndexedDbAdapter).inSingletonScope();
    container.bind(LoggerPort.$).to(BrowserLogger).inSingletonScope();
    container.load(authContainerModule);
    container.load(auditContainerModule); // Payments domain excluded entirely
    

    @my-app/auth-core es idéntico, carácter por carácter, en las versiones para móvil, administrador y escritorio. Nunca determina si el almacenamiento es AsyncStorage o IndexedDB.

    Permitir que un dominio utilice otro

    Supongamos que payments-core necesita un token de acceso gestionado por auth-core. Importar @my-app/auth-core está prohibido por las reglas de delimitación. En su lugar, el caso de uso de pagos depende del token de contrato del dominio de autenticación, que se encuentra en el paquete contracts y está permitido.

    // libs/payments/core/src/lib/use-cases/create-payment.use-case.ts
    import { inject, injectable } from 'inversify';
    
    // Import from contracts, NOT core — permitted by boundary rules
    import { AuthFacade } from '@my-app/auth-contracts';
    import { PaymentsGatewayPort } from '../ports/payments-gateway.port';
    
    @injectable()
    export class CreatePaymentUseCase {
      constructor(
        @inject(AuthFacade.$) private readonly auth: AuthFacade,
        @inject(PaymentsGatewayPort.$) private readonly gateway: PaymentsGatewayPort,
      ) {}
    
      async execute(amount: number): Promise<void> {
        const token = await this.auth.getAccessToken();
        return this.gateway.processPayment(amount, token);
      }
    }
    

    paymentsContainerModule no enlaza AuthFacade.$; solo lo consume. El enlace se logra cuando la raíz de composición carga ambos módulos en el mismo contenedor. La consecuencia práctica es que, si una aplicación carga las funciones de pago sin autenticación, la resolución falla en tiempo de ejecución; por eso vale la pena realizar una prueba básica para cada destino de aplicación que resuelva cada token público al menos una vez.

    Vinculando el contenedor a los frameworks de UI

    Dado que el contenedor no sabe nada sobre ninguna biblioteca de UI, cada framework recibe un pequeño adaptador de aproximadamente 10 a 15 líneas de código.

    React

    El contenedor se crea una sola vez al iniciar y nunca se reemplaza, por lo que el valor del contexto nunca cambia. Esto evita la cascada de recálculo normalmente asociada con Context: los consumidores obtienen una referencia estable. useInjection memoriza la búsqueda por contenedor y token, y lanza un error claro si falta el proveedor.

    // libs/shared/react-di/src/lib/di-context.tsx
    import React, { createContext, useContext, useMemo, type ReactNode } from 'react';
    import type { Container, ServiceIdentifier } from 'inversify';
    
    export interface DIProviderProps {
      container: Container;
      children: ReactNode;
    }
    
    const DIContext = createContext<Container | null>(null);
    
    export const DIProvider = ({ container, children }: DIProviderProps) => (
      <DIContext.Provider value={container}>
        {children}
      </DIContext.Provider>
    );
    
    export const useInjection = <T,>(token: ServiceIdentifier<T>): T => {
      const container = useContext(DIContext);
    
      return useMemo(() => {
        if (!container) {
          throw new Error('useInjection must be used within a DIProvider');
        }
    
        return container.get(token);
      }, [container, token]);
    };
    

    Vue

    El sistema de plugins de Vue y un InjectionKey tipado hacen que el adaptador sea aún más breve. El plugin proporciona el contenedor a nivel de aplicación, y un componente composable obtiene los tokens de él.

    // libs/shared/vue-di/src/lib/di-plugin.ts
    import type { App, InjectionKey } from 'vue';
    import { inject } from 'vue';
    import type { Container, ServiceIdentifier } from 'inversify';
    
    export const DI_CONTAINER: InjectionKey<Container> = Symbol('DI_CONTAINER');
    
    export const diPlugin = {
      install(app: App, { container }: { container: Container }) {
        app.provide(DI_CONTAINER, container);
      },
    };
    
    export const useInjection = <T>(token: ServiceIdentifier<T>): T => {
      const container = inject(DI_CONTAINER);
    
      if (!container) {
        throw new Error('diPlugin is not installed');
      }
    
      return container.get(token);
    };
    

    Angular

    El inyector jerárquico propio de Angular puede delegar en el contenedor del dominio a través de proveedores de fábrica. Al pasar el contenedor mediante un InjectionToken y crear un contenedor nuevo en cada inicio, se evita que las solicitudes procesadas simultáneamente compartan estado.

    // apps/angular-app/src/app/domain.providers.ts
    import { InjectionToken, type Provider } from '@angular/core';
    import type { Container } from 'inversify';
    import { AuthFacade } from '@my-app/auth-contracts';
    import { initAppContainer } from './composition-root';
    
    export const DI_CONTAINER = new InjectionToken<Container>('DI_CONTAINER');
    export const AUTH_FACADE = new InjectionToken<AuthFacade>('AUTH_FACADE');
    
    export const provideDomainContainer = (container: Container): Provider[] => [
      {
        provide: DI_CONTAINER,
        useValue: container,
      },
      {
        provide: AUTH_FACADE,
        useFactory: (c: Container) => c.get(AuthFacade.$),
        deps: [DI_CONTAINER],
      },
    ];
    
    // main.ts - fresh container instance created per bootstrap
    bootstrapApplication(AppComponent, {
      providers: [...provideDomainContainer(initAppContainer())],
    });
    

    Cada componente adicional de Angular que utiliza una fachada necesita su propio InjectionToken y proveedor de fábrica, por lo que la lista crece junto con la API pública; generarla a partir de un mapa pequeño es una opción cuando esta se vuelve larga.

    Costos y cuándo omitirlo

    La configuración añade aproximadamente 21 KB comprimidos y requiere el uso de emitDecoratorMetadata durante la compilación. En un monorepo empresarial con diez o más dominios, ese costo se compensa rápidamente. En una aplicación con dos o tres pantallas, el aislamiento representa una sobrecarga innecesaria; la inyección directa mediante constructor o incluso los singulares a nivel de módulo son opciones más adecuadas.

    Puntos clave

    • Mantenga la inyección de dependencias del framework (Context, provide/inject, proveedores de Angular) en los bordes de la interfaz de usuario; el núcleo del dominio debe depender únicamente de tokens de contrato.
  • Elige primero un contenedor para la historia de su módulo: un dominio que exporte un ContainerModule sellado es lo que mantiene el punto de entrada pequeño.
  • Utiliza tokens Symbol.for de tipo ServiceIdentifier<T> para que tanto los paquetes duplicados como la inferencia de tipos sigan funcionando.
  • Impone límites mediante exportaciones y reglas de lint, no solo por convenciones.
  • Deja que exactamente un archivo, la Raíz de Composición, interactúe con las implementaciones concretas; así, las compilaciones multiplataforma se convierten en una cuestión de intercambio de enlaces.
  • Recuerda que la inyección es solo una herramienta. El objetivo es la inversión: si puedes reemplazar React por Vue, SQLite por IndexedDB o Axios por Fetch sin editar la capa de dominio, la estrategia ha cumplido su función.
  • Lecturas relacionadas

  • Elegir una estructura de carpetas en React: siete diseños y sus puntos límite — Compare los diseños plano, basado en tipos, basado en características, Atomic Design, DDD, Feature-Sliced Design y monorepo para React, y conozca a qué escala deja de funcionar cada uno.
  • Mover Angular HttpClient al backend Fetch con withFetch() — Entienda por qué el HttpClient de Angular está pasando de XMLHttpRequest a fetch, cómo withFetch() permite SSR en los bordes y transmisión por streaming, y qué implica esto para sus interceptores.
  • Cómo Angular Dependency Injection resuelve servicios con inject() — Entienda cómo Angular DI suministra servicios a través de inject(), providers e injectores jerárquicos, y cómo delimitar instancias y evitar errores por falta de proveedores.
  • SOLID como una pregunta sobre el cambio: Refactoreo de un servicio de Spring Boot — Aprenda a aplicar cada principio SOLID en un backend de Spring Boot al preguntarse qué es probable que cambie y cómo evitar la sobreingeniería que SOLID a menudo provoca.