Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds ➔
👥 Community & Social
•
Malware / Trojaner / VirenStreaming-Piraterie: Piraten wechseln vom Firestick zu Social Media(23.09.2026 um 19:09 Uhr)
•
KI & AI VideosUsing Claude Opus 5.5 as your daily driver(23.09.2026 um 18:41 Uhr)
•
Sicherheitslücken (CVE)Security Weekly - A CRA Resource: The AI Vulnerability That Isn’t AI(23.09.2026 um 19:00 Uhr)
•
IT Security ToolsGitHub Release: google/clusterfuzz v2.41.0 (23.09.2026)(23.09.2026 um 18:55 Uhr)
•
IT Security ToolsGitHub Release: trufflesecurity/trufflehog v3.97.7 (23.09.2026)(23.09.2026 um 18:56 Uhr)
•
IT Security NachrichtenHackers Say They Stole Thousands of Sensitive F.B.I. Personnel Records(23.09.2026 um 19:33 Uhr)
•
IT Security NachrichtenMikroTrick: Zwei RouterOS-Fehler kombinieren sich zu Admin-Übernahme(23.09.2026 um 18:53 Uhr)
••••
Malware / Trojaner / VirenStreaming-Piraterie: Piraten wechseln vom Firestick zu Social Media(23.09.2026 um 19:09 Uhr)
•
KI & AI VideosUsing Claude Opus 5.5 as your daily driver(23.09.2026 um 18:41 Uhr)
•
Sicherheitslücken (CVE)Security Weekly - A CRA Resource: The AI Vulnerability That Isn’t AI(23.09.2026 um 19:00 Uhr)
•
IT Security ToolsGitHub Release: google/clusterfuzz v2.41.0 (23.09.2026)(23.09.2026 um 18:55 Uhr)
•
IT Security ToolsGitHub Release: trufflesecurity/trufflehog v3.97.7 (23.09.2026)(23.09.2026 um 18:56 Uhr)
•
IT Security NachrichtenHackers Say They Stole Thousands of Sensitive F.B.I. Personnel Records(23.09.2026 um 19:33 Uhr)
•
IT Security NachrichtenMikroTrick: Zwei RouterOS-Fehler kombinieren sich zu Admin-Übernahme(23.09.2026 um 18:53 Uhr)
•••
Intelligence View
⚡ tsecurity.de Intelligence

Clean Architecture in NestJS — A Practical Guide

Building NestJS apps is easy. Building NestJS apps that are still easy to change six months later is a different story. Clean Architecture gives you a way to structure your code so that business logic doesn't bleed into HTTP handlers,…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

Building NestJS apps is easy. Building NestJS apps that are still easy to change six months later is a different story.



Clean Architecture gives you a way to structure your code so that business logic doesn't bleed into HTTP handlers, database queries don't dictate your domain model, and swapping infrastructure (say, Postgres → SQLite, or REST → gRPC) doesn't require rewriting half the app.



This guide is practical. We'll build a small feature — managing User entities — and walk through each layer with real code.









The core idea



Clean Architecture revolves around one rule: dependencies point inward.




┌──────────────────────────────────┐
│ Infrastructure │ HTTP, DB, external APIs
│ ┌──────────────────────────┐ │
│ │ Application │ Use cases / orchestration
│ │ ┌──────────────────┐ │ │
│ │ │ Domain │ Entities, ports
│ │ └──────────────────┘ │ │
│ └──────────────────────────┘ │
└──────────────────────────────────┘








  • Domain — pure business logic, no framework dependencies


  • Application — use cases that orchestrate domain objects


  • Infrastructure — NestJS controllers, TypeORM repositories, anything "real"



The domain knows nothing about NestJS. The application knows nothing about Express. Infrastructure wires everything together.









Folder structure






src/
users/
domain/
user.entity.ts
user.repository.ts ← port (interface)
application/
commands/
create-user.command.ts
create-user.handler.ts
queries/
get-user.query.ts
get-user.handler.ts
infrastructure/
persistence/
typeorm-user.repository.ts
user.orm-entity.ts
http/
users.controller.ts
dto/
create-user.dto.ts
users.module.ts







If you're using CQRS (and you should consider it for anything beyond CRUD), separate commands and queries from the start. Retrofitting this later is painful.










1. Domain layer



The domain layer holds your entities and the interfaces (ports) that infrastructure must implement.



domain/user.entity.ts




export class User {
constructor(
public readonly id: string,
public readonly email: string,
public readonly name: string,
private readonly createdAt: Date,
) {}

static create(email: string, name: string): User {
return new User(
crypto.randomUUID(),
email,
name,
new Date(),
);
}

isOlderThan(days: number): boolean {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
return this.createdAt < cutoff;
}
}






No decorators, no @Column(), no @Injectable(). Just a plain class with behavior. This is intentional — your domain entity should be testable with zero framework setup.



domain/user.repository.ts




export interface UserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
save(user: User): Promise<void>;
}

export const USER_REPOSITORY = Symbol('USER_REPOSITORY');






This is your port. The domain defines what it needs; infrastructure provides it.









2. Application layer



Use cases live here. Each handler does one thing: it receives a command or query, calls domain logic, and persists or returns the result.



application/commands/create-user.command.ts




export class CreateUserCommand {
constructor(
public readonly email: string,
public readonly name: string,
) {}
}






application/commands/create-user.handler.ts




import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { Inject } from '@nestjs/common';
import { CreateUserCommand } from './create-user.command';
import { User } from '../../domain/user.entity';
import { UserRepository, USER_REPOSITORY } from '../../domain/user.repository';

@CommandHandler(CreateUserCommand)
export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
constructor(
@Inject(USER_REPOSITORY)
private readonly userRepository: UserRepository,
) {}

async execute(command: CreateUserCommand): Promise<string> {
const existing = await this.userRepository.findByEmail(command.email);
if (existing) {
throw new Error(`User with email ${command.email} already exists`);
}

const user = User.create(command.email, command.name);
await this.userRepository.save(user);

return user.id;
}
}






Notice what's missing: no HTTP-specific concepts, no TypeORM, no NestJS response objects. This handler works identically whether triggered via REST, a CLI command, or a message queue consumer.









3. Infrastructure layer



Here's where NestJS, TypeORM, and the outside world come in.



infrastructure/persistence/user.orm-entity.ts




import { Entity, Column, PrimaryColumn, CreateDateColumn } from 'typeorm';

@Entity('users')
export class UserOrmEntity {
@PrimaryColumn('uuid')
id: string;

@Column({ unique: true })
email: string;

@Column()
name: string;

@CreateDateColumn()
createdAt: Date;
}






infrastructure/persistence/typeorm-user.repository.ts




import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserRepository } from '../../domain/user.repository';
import { User } from '../../domain/user.entity';
import { UserOrmEntity } from './user.orm-entity';

@Injectable()
export class TypeOrmUserRepository implements UserRepository {
constructor(
@InjectRepository(UserOrmEntity)
private readonly ormRepo: Repository<UserOrmEntity>,
) {}

async findById(id: string): Promise<User | null> {
const record = await this.ormRepo.findOneBy({ id });
return record ? this.toDomain(record) : null;
}

async findByEmail(email: string): Promise<User | null> {
const record = await this.ormRepo.findOneBy({ email });
return record ? this.toDomain(record) : null;
}

async save(user: User): Promise<void> {
await this.ormRepo.save(this.toOrm(user));
}

private toDomain(record: UserOrmEntity): User {
return new User(record.id, record.email, record.name, record.createdAt);
}

private toOrm(user: User): UserOrmEntity {
const entity = new UserOrmEntity();
entity.id = user.id;
entity.email = user.email;
entity.name = user.name;
return entity;
}
}






The mapper (toDomain / toOrm) is the key. It keeps your ORM schema and your domain model independent — you can change one without touching the other.



infrastructure/http/users.controller.ts




import { Controller, Post, Body, HttpCode } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs';
import { CreateUserDto } from './dto/create-user.dto';
import { CreateUserCommand } from '../../application/commands/create-user.command';

@Controller('users')
export class UsersController {
constructor(private readonly commandBus: CommandBus) {}

@Post()
@HttpCode(201)
async create(@Body() dto: CreateUserDto): Promise<{ id: string }> {
const id = await this.commandBus.execute(
new CreateUserCommand(dto.email, dto.name),
);
return { id };
}
}






The controller is intentionally thin. It validates the request (via DTOs + class-validator), dispatches a command, and returns a response. No business logic here.









4. Wiring it all together



users.module.ts




import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserOrmEntity } from './infrastructure/persistence/user.orm-entity';
import { TypeOrmUserRepository } from './infrastructure/persistence/typeorm-user.repository';
import { USER_REPOSITORY } from './domain/user.repository';
import { CreateUserHandler } from './application/commands/create-user.handler';
import { UsersController } from './infrastructure/http/users.controller';

const CommandHandlers = [CreateUserHandler];

@Module({
imports: [CqrsModule, TypeOrmModule.forFeature([UserOrmEntity])],
controllers: [UsersController],
providers: [
...CommandHandlers,
{
provide: USER_REPOSITORY,
useClass: TypeOrmUserRepository,
},
],
})
export class UsersModule {}






The provide/useClass pair is where Dependency Inversion happens. The application layer depends on the USER_REPOSITORY token (a symbol), not on TypeORM directly. Swap useClass to an in-memory repository for tests — zero changes to handlers.









Testing the application layer in isolation






describe('CreateUserHandler', () => {
let handler: CreateUserHandler;
let repo: InMemoryUserRepository;

beforeEach(() => {
repo = new InMemoryUserRepository();
handler = new CreateUserHandler(repo);
});

it('creates a user and returns its id', async () => {
const id = await handler.execute(
new CreateUserCommand('[email protected]', 'Kuba'),
);
expect(id).toBeDefined();
const user = await repo.findByEmail('[email protected]');
expect(user?.name).toBe('Kuba');
});

it('throws when email already taken', async () => {
await handler.execute(new CreateUserCommand('[email protected]', 'Kuba'));
await expect(
handler.execute(new CreateUserCommand('[email protected]', 'Other')),
).rejects.toThrow('already exists');
});
});






No NestJS test harness, no database, no HTTP — just fast, focused unit tests.









Common tradeoffs to be aware of



"This is a lot of boilerplate for CRUD." Yes. For a simple 3-field entity, the overhead is real. Clean Architecture pays off when your domain has actual rules — validation, state machines, business constraints. If it's just a thin wrapper over a DB table, CQRS + ports-and-adapters might be overkill.



"Do I need separate ORM and domain entities?" Only if they diverge. If your TypeORM entity maps 1:1 to your domain object with no behavior, merge them for now and split later when needed. The important thing is that your application layer never imports typeorm directly.



"What about aggregates and domain events?" Out of scope for this guide, but a natural next step. Once your User.create() starts needing to publish an event (e.g., to send a welcome email), domain events give you a clean way to do it without coupling to side effects.









Summary




























Layer Contains Can import
Domain Entities, value objects, repository interfaces Nothing external
Application Command/query handlers, use cases Domain only
Infrastructure Controllers, ORM repos, external clients Application + Domain + NestJS


Start with the domain. Define your ports. Let infrastructure implement them. Keep your handlers framework-free. Test the application layer without spinning up a full NestJS app.



The payoff isn't apparent on day one — it becomes obvious when you need to refactor and the blast radius is small.






If you're building something and applying these patterns, I'd love to see what you're working on — drop a link in the comments.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Clean Architecture in NestJS — A Practical Guide

Thematisch verwandte Begriffe: Clean, Architecture, NestJS, Practical · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-18181 | IBM Financial Transaction Manager (FTM) for RedHat OpenShift could allow…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel • Rechts: nächster Artikel • unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel TTP ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger • Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick