Loading home…
Loading home…
Loading article…
Controllers that know too much, entities full of ORM decorators, and tests that need a database. A practical guide to ports and adapters in NestJS.

No spam — just a note when I publish something new on backend, cloud, and architecture.
One email when a post goes live. Unsubscribe anytime.

When your UserService handles POST and GET, optimizations on one side break the other. Learn commands, queries, and handlers with @nestjs/cqrs.

If every new feature forces you to modify five different services, you probably have a coupling problem. Learn how Event-Driven Architecture helps decouple modules and scale NestJS applications.

If onboarding a new customer requires deploying a new application or duplicating an entire database, your SaaS architecture probably isn't ready to scale. Learn how to implement multi-tenancy in NestJS in a clean and maintainable way.
You start a NestJS project with good intentions. Six months later, users.service.ts imports TypeORM, sends emails, hashes passwords, and publishes Kafka events. Changing the database feels like surgery.
That is not a team problem. It is an architecture problem — and hexagonal architecture (ports and adapters) is one of the most effective fixes for backend teams that outgrow the classic controller → service → entity stack.
This article walks through the model used in the NestJS Enterprise Starter, with enough detail that you can apply it in your own codebase this week.
When business logic, HTTP, and persistence live in the same class:
@Post() handlers.Hexagonal architecture draws a line: the center is your business rules; everything else is a plugin you can replace.
Imagine your app as a hexagon:
HTTP → Application (use cases) → Domain
↑
TypeORM repo implements UserRepositoryPort
The rule is simple: dependencies point inward. core/ never imports @nestjs/common or typeorm.
src/
├── core/
│ ├── domain/
│ └── ports/output/persistence/
├── application/
│ └── commands/user/
├── adapters/
│ ├── primary/http/
│ └── secondary/persistence/typeorm/
└── modules/user/
NestJS modules become wiring only: they bind UserRepositoryPort → UserRepository. Handlers depend on the port; the module chooses the adapter.
The domain declares what it needs:
export abstract class UserRepositoryPort {
abstract findByEmail(email: string): Promise<User | null>;
abstract create(payload: CreateUserPayload): Promise<User>;
}
No @Injectable(), no Repository<UserOrmEntity>. The handler asks for a capability; NestJS injects the implementation at runtime.
The TypeORM entity stays in adapters/secondary:
@Entity('users')
export class UserOrmEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
// ...
}
A mapper translates ORM → domain:
static toDomain(orm: UserOrmEntity): User {
return { id: orm.id, email: orm.email, name: orm.name, ... };
}
Handlers never see UserOrmEntity. When you migrate to Prisma, you rewrite one adapter — not forty use cases.
Bad:
@Post()
async create(@Body() dto: CreateUserDto) {
const hash = await bcrypt.hash(dto.password, 10);
return this.repo.save({ ...dto, password: hash });
}
Good:
@Post()
create(@Body() dto: CreateUserDto) {
return this.commandBus.execute(new CreateUserCommand(dto));
}
Password rules, duplicate email checks, and welcome emails belong in a command handler in the application layer.
| Benefit | What it feels like day to day |
|---|---|
| Fast unit tests | Handlers tested with jest.fn() mocks — no Docker |
| Swappable infra | New queue or DB = new adapter + provider |
| Onboarding | Juniors know where code belongs |
| Reuse | Same handler from HTTP, cron, or BullMQ worker |
core/ — move them to adapters; map to plain types.UserRepository in handlers — inject UserRepositoryPort.Hexagonal layout pairs naturally with CQRS (separate read and write paths) and ports for queues (BullMQ behind IQueueService). In this series, we cover those patterns next — same repo, same dependency direction.
If you are building a NestJS API that must survive more than one hiring cycle, start by extracting one module (Users) into ports and adapters. You will feel the difference in the first week of tests.