| name | infra-import-refactor |
| description | Refactor direct infrastructure imports into port interfaces with dependency injection |
| user_invocable | true |
Infrastructure Import Refactoring Assistant
Guide the refactoring of direct infrastructure imports in application/presentation layers into proper port interfaces with tsyringe dependency injection.
When to Use
- When
check-layer-violations.sh hook flags an infrastructure import
- When you notice
from.*infrastructure/ imports in application or presentation code
- During tech debt cleanup of Clean Architecture violations
- Before adding new functionality to code that has existing violations
Steps
1. Find All Violations
Scan for direct infrastructure imports in forbidden layers:
grep -rn "from.*infrastructure/" packages/core/src/application/ | grep -v "import type" | grep -v "di/container"
grep -rn "from.*infrastructure/" src/presentation/ | grep -v "import type" | grep -v "di/container" | grep -v "server-container"
2. Categorize Each Violation
For each import, determine the category:
| Category | Description | Action |
|---|
| A: Needs new port | No existing interface covers this service | Create interface + adapter |
| B: Has port, not wired | Interface exists but consumer uses direct import | Switch to DI injection |
| C: Type-only | Only types are imported, no runtime dependency | Convert to import type |
3. Category C — Convert to Type-Only Import
Simplest fix. Change:
import { SomeType } from '@/infrastructure/services/some-service';
import type { SomeType } from '@/infrastructure/services/some-service';
Type-only imports are allowed since they're erased at runtime and don't create a dependency.
4. Category B — Wire Existing Port Interface
If a port interface already exists in packages/core/src/application/ports/output/:
import { GitService } from '@/infrastructure/services/git-service';
export class SomeUseCase {
private gitService = new GitService();
}
import type { IGitPrService } from '../ports/output/services/git-pr-service.interface';
@injectable()
export class SomeUseCase {
constructor(
@inject('IGitPrService') private readonly gitPrService: IGitPrService,
) {}
}
5. Category A — Create New Port Interface
5a. Define the interface
Create in packages/core/src/application/ports/output/services/ or repositories/:
export interface IMyService {
doSomething(input: SomeInput): Promise<SomeOutput>;
}
Naming conventions:
- File:
kebab-case.interface.ts
- Interface:
I prefix + PascalCase (e.g., IMyService)
- Location:
services/ for stateless operations, repositories/ for data persistence
5b. Make the infrastructure class implement it
import { injectable } from 'tsyringe';
import type { IMyService } from '@/application/ports/output/services/my-service.interface';
@injectable()
export class MyService implements IMyService {
async doSomething(input: SomeInput): Promise<SomeOutput> {
}
}
5c. Register in DI container
Add to packages/core/src/infrastructure/di/container.ts:
container.register<IMyService>('IMyService', { useClass: MyService });
5d. Update the consumer
@injectable()
export class SomeUseCase {
constructor(
@inject('IMyService') private readonly myService: IMyService,
) {}
}
6. Update Tests
For each refactored consumer, update its test to inject a mock:
const mockMyService: IMyService = {
doSomething: vi.fn().mockResolvedValue(expectedOutput),
};
const useCase = new SomeUseCase(mockMyService);
Consider using /mock-factory if the interface is used in 3+ test files.
7. Verify
pnpm typecheck
pnpm test:unit
pnpm validate
grep -rn "from.*infrastructure/" packages/core/src/application/ | grep -v "import type" | grep -v "di/container"
grep -rn "from.*infrastructure/" src/presentation/ | grep -v "import type" | grep -v "di/container" | grep -v "server-container"
Example: Refactoring a FileSystem Import
Before — presentation layer directly imports infrastructure:
import { FileSystemService } from '@/infrastructure/services/file-system-service';
const fs = new FileSystemService();
const exists = await fs.directoryExists(path);
After — proper port interface with DI:
export interface IFileSystemService {
directoryExists(path: string): Promise<boolean>;
readFile(path: string): Promise<string>;
writeFile(path: string, content: string): Promise<void>;
}
@injectable()
export class FileSystemService implements IFileSystemService { ... }
container.register<IFileSystemService>('IFileSystemService', { useClass: FileSystemService });
@injectable()
export class CheckDirectoryUseCase {
constructor(
@inject('IFileSystemService') private readonly fs: IFileSystemService,
) {}
async execute(path: string): Promise<boolean> {
return this.fs.directoryExists(path);
}
}
const exists = await checkDirectoryUseCase.execute(path);