agentsclimarketplace

Ts ddd clean architecture

Skill Methasit-Pun/ts-ddd-clean-architecture/skills/ts-ddd-clean-architecture

Enforces Hexagonal Architecture, Domain-Driven Design (DDD), and Event-Driven workflows for Node.js using Express, Prisma, and Socket.IO.From its SKILL.md

Install
npx -y skills add Methasit-Pun/ts-ddd-clean-architecture --skill ts-ddd-clean-architecture

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

7.5 KB, ~1.7k tokens by cl100k_base, as published. Nobody here has run it

TypeScript DDD & Clean Architecture Expert

Strict Software Architect specializing in Hexagonal Architecture, Domain-Driven Design (DDD), and Event-Driven Architecture. Target stack: Node.js, Express, Socket.IO, and Prisma.

When to Activate

  • Creating a new feature end-to-end (Entity → Use Case → Controller)
  • Designing or reviewing domain models, aggregates, or value objects
  • Adding a new Use Case (command or query)
  • Wiring up a new port (repository interface or publisher interface)
  • Reviewing code for layer boundary violations (Prisma bleed, HTTP bleed)
  • Mapping Prisma models to DDD Entities inside the Infrastructure layer
  • Generating Domain Events for significant business actions

Directory Structure

src/
├── domain/           # Pure DDD Core — zero external dependencies
│   ├── entities/
│   ├── value-objects/
│   ├── events/
│   └── exceptions/
├── application/      # Use Cases, Ports (interfaces), DTOs, Event Handlers
│   ├── use-cases/
│   ├── ports/
│   └── dtos/
├── infrastructure/   # Adapters: Prisma repos, Socket.IO publisher
│   ├── repositories/
│   └── publishers/
├── presentation/     # Express controllers, Socket.IO listeners
│   ├── controllers/
│   └── sockets/
└── main/             # DI container + server bootstrap

Domain Layer

Value Objects

// src/domain/value-objects/email.vo.ts
export class Email {
  private constructor(private readonly value: string) {}

  static create(raw: string): Email {
    if (!raw.includes('@')) throw new InvalidEmailException(raw);
    return new Email(raw.toLowerCase().trim());
  }

  toString(): string { return this.value; }
  equals(other: Email): boolean { return this.value === other.value; }
}

Entities / Aggregates

// src/domain/entities/user.entity.ts
import { UserRegisteredEvent } from '../events/user-registered.event';

export class User {
  private readonly domainEvents: DomainEvent[] = [];

  private constructor(
    private readonly id: UserId,
    private readonly email: Email,
    private passwordHash: string,
  ) {}

  static register(id: UserId, email: Email, passwordHash: string): User {
    const user = new User(id, email, passwordHash);
    user.domainEvents.push(new UserRegisteredEvent(id, email));
    return user;
  }

  changePassword(newHash: string): void {
    if (!newHash) throw new WeakPasswordException();
    this.passwordHash = newHash;
  }

  pullDomainEvents(): DomainEvent[] {
    const events = [...this.domainEvents];
    this.domainEvents.length = 0;
    return events;
  }

  getId(): UserId { return this.id; }
  getEmail(): Email { return this.email; }
}

Domain Events

// src/domain/events/user-registered.event.ts
export class UserRegisteredEvent implements DomainEvent {
  readonly occurredOn: Date = new Date();
  constructor(
    readonly userId: UserId,
    readonly email: Email,
  ) {}
}

Domain Exceptions

// src/domain/exceptions/invalid-email.exception.ts
export class InvalidEmailException extends Error {
  constructor(raw: string) {
    super(`"${raw}" is not a valid email address.`);
    this.name = 'InvalidEmailException';
  }
}

Application Layer

Ports (Interfaces)

// src/application/ports/user-repository.port.ts
export interface IUserRepository {
  findById(id: UserId): Promise<User | null>;
  findByEmail(email: Email): Promise<User | null>;
  save(user: User): Promise<void>;
}

// src/application/ports/realtime-publisher.port.ts
export interface IRealTimePublisher {
  publish(event: string, payload: unknown): void;
}

Use Cases

// src/application/use-cases/register-user.use-case.ts
export class RegisterUserUseCase {
  constructor(
    private readonly userRepo: IUserRepository,
    private readonly publisher: IRealTimePublisher,
  ) {}

  async execute(dto: RegisterUserDto): Promise<void> {
    const email = Email.create(dto.email);

    const existing = await this.userRepo.findByEmail(email);
    if (existing) throw new UserAlreadyExistsException();

    const id = UserId.generate();
    const hash = await hashPassword(dto.password);
    const user = User.register(id, email, hash);

    await this.userRepo.save(user);

    for (const event of user.pullDomainEvents()) {
      this.publisher.publish('user.registered', event);
    }
  }
}

Infrastructure Layer

Prisma Repository Adapter

// src/infrastructure/repositories/prisma-user.repository.ts
export class PrismaUserRepository implements IUserRepository {
  constructor(private readonly db: PrismaClient) {}

  async findById(id: UserId): Promise<User | null> {
    const record = await this.db.user.findUnique({ where: { id: id.toString() } });
    if (!record) return null;
    return this.toDomain(record);       // ← always map; never leak Prisma types
  }

  async save(user: User): Promise<void> {
    await this.db.user.upsert({
      where: { id: user.getId().toString() },
      create: this.toPersistence(user),
      update: this.toPersistence(user),
    });
  }

  private toDomain(record: PrismaUser): User {
    return User.reconstitute(
      UserId.from(record.id),
      Email.create(record.email),
      record.passwordHash,
    );
  }

  private toPersistence(user: User) {
    return { id: user.getId().toString(), email: user.getEmail().toString() };
  }
}

Presentation Layer

Express Controller

// src/presentation/controllers/register-user.controller.ts
export class RegisterUserController {
  constructor(private readonly useCase: RegisterUserUseCase) {}

  async handle(req: Request, res: Response): Promise<void> {
    const dto = RegisterUserSchema.parse(req.body); // Zod — see zod-express-validation skill
    await this.useCase.execute(dto);
    res.status(201).json({ message: 'User registered.' });
  }
}

Dependency Injection (main/)

// src/main/container.ts
const prisma = new PrismaClient();
const userRepo = new PrismaUserRepository(prisma);
const publisher = new SocketIOPublisher(io);
const registerUserUseCase = new RegisterUserUseCase(userRepo, publisher);
export const registerUserController = new RegisterUserController(registerUserUseCase);

Step-by-Step Execution Workflow

When generating a new feature, work in this exact order:

  1. Domain — Value Objects → Entity/Aggregate → Domain Events → Domain Exceptions
  2. PortsIRepository and IRealTimePublisher interfaces in application/ports/
  3. Use Case — Orchestrates fetch → method call → save → publish
  4. Infrastructure — Prisma Repository adapter with toDomain / toPersistence mappers
  5. Presentation — Express Controller parses HTTP → DTO → calls Use Case
  6. DI — Wire up in main/container.ts

Strict Anti-Patterns

❌ Pattern✅ Correct Approach
import { PrismaClient } from '@prisma/client' in Domain/ApplicationUse the IUserRepository port interface
useCase.execute(req.body)Parse via Zod first, pass typed DTO
import { Server } from 'socket.io' in Use CaseCall IRealTimePublisher.publish()
prisma.user.findMany() in ControllerCall Use Case, which calls Repository port
user.password = newHashuser.changePassword(newHash) — behavior, not setters

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,679. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.