Server → internal: cross-domain via exported services, not repos or DB
**Scope: server → internal domain** (server-to-internal). One backend domain calling another inside your monolith — inject the exported service, not HTTP to `/api/...`, not the other domain's repository or tables. The repository is internal to the domain. The service is the public API.
nestjs-module-cross-module-via-services
Why it matters
| Stake | If ignored |
|---|---|
| API drift |
|
| Hidden coupling |
|
How to fix
If WorkspaceModule needs catalog information, it does not inject CatalogRepository, hit the DB directly, or fetch the catalog API. It injects the CatalogService exported by CatalogModule. Browser clients use a generated data-access client; external vendors use a dedicated integration client.
Examples
ts
// libs/workspace/backend-feature/src/lib/workflows.service.ts
@Injectable()
export class WorkflowsService {
constructor(
@InjectRepository(Item)
private catalogRepo: Repository<Item>, // ← reaching into catalog's repo
) {}
async attachToItem(workflowId: string, itemId: string) {
const item = await this.catalogRepo.findOne({ where: { id: itemId } });
// bypasses every CatalogService validation
}
}ts
// libs/catalog/backend-feature/src/lib/catalog.service.ts
@Injectable()
export class CatalogService {
async getItem(id: string): Promise<Item> { /* ... */ }
}
@Module({
providers: [CatalogService, CatalogRepository],
exports: [CatalogService], // ← repository is NOT exported
})
export class CatalogModule {}
// libs/workspace/backend-feature/src/lib/workflows.service.ts
@Injectable()
export class WorkflowsService {
constructor(private catalog: CatalogService) {} // through the front door
async attachToItem(workflowId: string, itemId: string) {
const item = await this.catalog.getItem(itemId);
// goes through CatalogService validation, authorization, logging
}
}