no-portless-adapter
A driven adapter exists to implement a port: every class in driven/<technology>/adapters/ extends one, and every port is declared by the domain.
- Rule
layers/no-portless-adapter- Category
- Layers: what each layer may depend on
- Reports
- A driven adapter that extends no port, a port declared outside the domain
- Applies to
- Every class in
driven/**/adapters/, and every abstract class that extendsPort - Turn off
"layers/no-portless-adapter": "off"
Why
A Mailer class in driven/smtp/adapters/ sends emails, and the handler calls it directly. The application now depends on SMTP: testing a use case sends mail, and moving to an email API means rewriting the handler.
The fix
The domain says what it needs in its own words, as a port; the adapter extends that port. The handler only knows the port, so the adapter can be swapped in a test or when the technology changes.
What it checks
driven/**/adapters/ folder extends a Port, directly or through a repository, Outbox, EventPublisher, UnitOfWork, Clock or IdGenerator.Port is declared in domain/ports/ or domain/repositories/.Abstract base classes for your adapters may also live in driven/**/adapters/, as long as they extend a port.
What it reports
src/ordering/driven/smtp/adapters/mailer.adapter.ts:1
layers/no-portless-adapter: Mailer is a driven adapter but
extends no Port: extend the port it implements.
src/ordering/application/commands/notifications.ts:3
layers/no-portless-adapter: The port Notifications is declared
outside the domain: move it to domain/ports/ or
domain/repositories/.Fix it
Extend the port the adapter implements
So that the application depends on what it needs and not on the technology, declare a port in the domain and make the adapter extend it. Name the adapter after its technology and its port.
export class Mailer {
async send(to: string, body: string): Promise<void> {}
}import { Notifications } from "../../../domain/ports/notifications.port";
export class SmtpNotifications extends Notifications {
async orderPlaced(email: string): Promise<void> {}
}Declare ports in the domain
So that the domain knows every dependency it relies on, a port declared in the application or next to an adapter moves to domain/ports/, or to domain/repositories/ for a repository.
export abstract class Notifications extends Port {
abstract orderPlaced(email: string): Promise<void>;
}export abstract class Notifications extends Port {
abstract orderPlaced(email: string): Promise<void>;
}Turn it off
rules: { "layers/no-portless-adapter": "off" },On an existing project, prefer a baseline: new adapters extend a port while you wrap the old ones.
See also
- Ports, what an adapter implements
- Project layout: layers
tactical/no-misplaced-class, for the folder and file name of an adapter- Rules, every rule by category