一键导入
ts-ddd-clean-architecture
Enforces Hexagonal Architecture, Domain-Driven Design (DDD), and Event-Driven workflows for Node.js using Express, Prisma, and Socket.IO.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Enforces Hexagonal Architecture, Domain-Driven Design (DDD), and Event-Driven workflows for Node.js using Express, Prisma, and Socket.IO.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Review TypeScript DDD clean architecture code for correctness bugs, layer violations, and simplification opportunities. Trigger when the user says "review this", "check this code", "code review", "what's wrong with this", "is this correct DDD?", "does this follow clean architecture?", or when changes touch domain, application, or infrastructure layers.
Security review for TypeScript DDD code — OWASP-mapped checks for injection, auth/authz, data exposure, and misconfiguration. Trigger when the user says "security review", "check for vulnerabilities", "is this secure?", "review auth code", or when changes touch authentication, authorization, input handling, external APIs, file uploads, or user-controlled data.
Review and simplify TypeScript DDD code — remove unnecessary complexity, eliminate duplication, reduce premature abstraction. Trigger when the user says "simplify this", "too much boilerplate", "over-engineered", "clean this up", "refactor this", or when code has grown too abstract or introduces patterns before they prove value.
Design and implement the CQRS (Command Query Responsibility Segregation) pattern in a TypeScript DDD clean architecture project. Trigger when the user says "implement CQRS", "add a command", "add a query", "create a use case", "add a command handler", "build the application layer", "set up the command bus", or when the user needs to add a new feature and is asking how to wire up the application layer. Also trigger when distinguishing between write operations (commands) and read operations (queries) in any context.
Design and implement the Repository pattern in a TypeScript DDD clean architecture project — define repository interfaces in the domain layer and implementations in the infrastructure layer. Trigger when the user says "create a repository", "implement persistence", "add database access", "wire up TypeORM/Prisma/Drizzle", "implement the repository interface", "add Unit of Work", "how do I persist this aggregate", or when connecting domain aggregates to any data store. Also trigger when reviewing persistence code that may be violating the dependency rule.
Design and implement CI/CD pipelines for a TypeScript DDD clean architecture project — GitHub Actions, GitLab CI, Docker builds, environment promotion, and secrets management. Trigger when the user says "set up CI", "add a pipeline", "automate tests", "write a GitHub Actions workflow", "configure deployment", "add Docker support", "set up CD", "automate the build", or when the project needs automated quality gates before merge. Also trigger when the user asks about environment promotion (dev → staging → prod) or secrets management strategy.
| name | ts-ddd-clean-architecture |
| description | Enforces Hexagonal Architecture, Domain-Driven Design (DDD), and Event-Driven workflows for Node.js using Express, Prisma, and Socket.IO. |
Strict Software Architect specializing in Hexagonal Architecture, Domain-Driven Design (DDD), and Event-Driven Architecture. Target stack: Node.js, Express, Socket.IO, and Prisma.
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
// 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; }
}
// 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; }
}
// src/domain/events/user-registered.event.ts
export class UserRegisteredEvent implements DomainEvent {
readonly occurredOn: Date = new Date();
constructor(
readonly userId: UserId,
readonly email: Email,
) {}
}
// 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';
}
}
// 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;
}
// 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);
}
}
}
// 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() };
}
}
// 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.' });
}
}
// 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);
When generating a new feature, work in this exact order:
IRepository and IRealTimePublisher interfaces in application/ports/toDomain / toPersistence mappersmain/container.ts| ❌ Pattern | ✅ Correct Approach |
|---|---|
import { PrismaClient } from '@prisma/client' in Domain/Application | Use the IUserRepository port interface |
useCase.execute(req.body) | Parse via Zod first, pass typed DTO |
import { Server } from 'socket.io' in Use Case | Call IRealTimePublisher.publish() |
prisma.user.findMany() in Controller | Call Use Case, which calls Repository port |
user.password = newHash | user.changePassword(newHash) — behavior, not setters |