@qazuor/claude-code-config 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +395 -50
- package/dist/bin.cjs +3207 -165
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +3207 -165
- package/dist/bin.js.map +1 -1
- package/dist/index.cjs +75 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +284 -1
- package/dist/index.d.ts +284 -1
- package/dist/index.js +75 -58
- package/dist/index.js.map +1 -1
- package/package.json +24 -24
- package/templates/CLAUDE.md.template +60 -5
- package/templates/agents/README.md +58 -39
- package/templates/agents/_registry.json +43 -202
- package/templates/agents/engineering/{hono-engineer.md → api-engineer.md} +61 -70
- package/templates/agents/engineering/database-engineer.md +253 -0
- package/templates/agents/engineering/frontend-engineer.md +302 -0
- package/templates/docs/_registry.json +54 -0
- package/templates/docs/standards/code-standards.md +20 -0
- package/templates/docs/standards/design-standards.md +13 -0
- package/templates/docs/standards/documentation-standards.md +13 -0
- package/templates/docs/standards/performance-standards.md +524 -0
- package/templates/docs/standards/security-standards.md +496 -0
- package/templates/docs/standards/testing-standards.md +15 -0
- package/templates/hooks/on-notification.sh +0 -0
- package/templates/scripts/add-changelogs.sh +0 -0
- package/templates/scripts/generate-code-registry.ts +0 -0
- package/templates/scripts/health-check.sh +0 -0
- package/templates/scripts/sync-registry.sh +0 -0
- package/templates/scripts/telemetry-report.ts +0 -0
- package/templates/scripts/validate-docs.sh +0 -0
- package/templates/scripts/validate-registry.sh +0 -0
- package/templates/scripts/validate-structure.sh +0 -0
- package/templates/scripts/worktree-cleanup.sh +0 -0
- package/templates/scripts/worktree-create.sh +0 -0
- package/templates/skills/README.md +99 -90
- package/templates/skills/_registry.json +323 -16
- package/templates/skills/api-frameworks/express-patterns.md +411 -0
- package/templates/skills/api-frameworks/fastify-patterns.md +419 -0
- package/templates/skills/api-frameworks/hono-patterns.md +388 -0
- package/templates/skills/api-frameworks/nestjs-patterns.md +497 -0
- package/templates/skills/database/drizzle-patterns.md +449 -0
- package/templates/skills/database/mongoose-patterns.md +503 -0
- package/templates/skills/database/prisma-patterns.md +487 -0
- package/templates/skills/frontend-frameworks/astro-patterns.md +415 -0
- package/templates/skills/frontend-frameworks/nextjs-patterns.md +470 -0
- package/templates/skills/frontend-frameworks/react-patterns.md +516 -0
- package/templates/skills/frontend-frameworks/tanstack-start-patterns.md +469 -0
- package/templates/skills/patterns/atdd-methodology.md +364 -0
- package/templates/skills/patterns/bdd-methodology.md +281 -0
- package/templates/skills/patterns/clean-architecture.md +444 -0
- package/templates/skills/patterns/hexagonal-architecture.md +567 -0
- package/templates/skills/patterns/vertical-slice-architecture.md +502 -0
- package/templates/agents/engineering/astro-engineer.md +0 -293
- package/templates/agents/engineering/db-drizzle-engineer.md +0 -360
- package/templates/agents/engineering/express-engineer.md +0 -316
- package/templates/agents/engineering/fastify-engineer.md +0 -399
- package/templates/agents/engineering/mongoose-engineer.md +0 -473
- package/templates/agents/engineering/nestjs-engineer.md +0 -429
- package/templates/agents/engineering/nextjs-engineer.md +0 -451
- package/templates/agents/engineering/prisma-engineer.md +0 -432
- package/templates/agents/engineering/react-senior-dev.md +0 -394
- package/templates/agents/engineering/tanstack-start-engineer.md +0 -447
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hexagonal-architecture
|
|
3
|
+
category: patterns
|
|
4
|
+
description: Hexagonal Architecture (Ports and Adapters) for loosely coupled, testable applications
|
|
5
|
+
usage: Use when building applications that need to be independent of external systems and highly testable
|
|
6
|
+
input: Application requirements, domain models, external system integrations
|
|
7
|
+
output: Core domain with ports (interfaces) and adapters (implementations) for all external interactions
|
|
8
|
+
config_required:
|
|
9
|
+
- CORE_DIR: "Directory for core domain (e.g., src/core/)"
|
|
10
|
+
- PORTS_DIR: "Directory for port interfaces (e.g., src/core/ports/)"
|
|
11
|
+
- ADAPTERS_DIR: "Directory for adapters (e.g., src/adapters/)"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Hexagonal Architecture (Ports and Adapters)
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
Hexagonal Architecture, introduced by Alistair Cockburn, creates loosely coupled application components that can be connected to their software environment via ports and adapters. The application core is isolated from external concerns.
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
| Setting | Description | Example |
|
|
23
|
+
|---------|-------------|---------|
|
|
24
|
+
| CORE_DIR | Core domain logic | `src/core/`, `domain/` |
|
|
25
|
+
| PORTS_DIR | Port interfaces | `src/core/ports/`, `ports/` |
|
|
26
|
+
| ADAPTERS_DIR | Adapter implementations | `src/adapters/`, `infrastructure/` |
|
|
27
|
+
| DRIVING_DIR | Driving (primary) adapters | `src/adapters/driving/` |
|
|
28
|
+
| DRIVEN_DIR | Driven (secondary) adapters | `src/adapters/driven/` |
|
|
29
|
+
|
|
30
|
+
## The Hexagonal Model
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
┌─────────────────┐
|
|
34
|
+
│ REST API │
|
|
35
|
+
│ (Driving) │
|
|
36
|
+
└────────┬────────┘
|
|
37
|
+
│
|
|
38
|
+
┌────────────────────▼────────────────────┐
|
|
39
|
+
│ INPUT PORT │
|
|
40
|
+
│ (Primary Interface) │
|
|
41
|
+
├──────────────────────────────────────────┤
|
|
42
|
+
│ │
|
|
43
|
+
│ APPLICATION CORE │
|
|
44
|
+
│ │
|
|
45
|
+
│ ┌────────────────────────────┐ │
|
|
46
|
+
│ │ Domain Services │ │
|
|
47
|
+
│ │ │ │
|
|
48
|
+
│ │ ┌──────────────────┐ │ │
|
|
49
|
+
│ │ │ Domain Entities │ │ │
|
|
50
|
+
│ │ └──────────────────┘ │ │
|
|
51
|
+
│ └────────────────────────────┘ │
|
|
52
|
+
│ │
|
|
53
|
+
├──────────────────────────────────────────┤
|
|
54
|
+
│ OUTPUT PORT │
|
|
55
|
+
│ (Secondary Interface) │
|
|
56
|
+
└────────────────┬─────────────────────────┘
|
|
57
|
+
│
|
|
58
|
+
┌──────────────┴──────────────┐
|
|
59
|
+
│ │
|
|
60
|
+
┌──────▼──────┐ ┌────────▼────────┐
|
|
61
|
+
│ Database │ │ External API │
|
|
62
|
+
│ (Driven) │ │ (Driven) │
|
|
63
|
+
└─────────────┘ └─────────────────┘
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Port Types
|
|
67
|
+
|
|
68
|
+
### Driving Ports (Primary/Inbound)
|
|
69
|
+
|
|
70
|
+
**Purpose:** Define how the outside world can use the application.
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// core/ports/driving/OrderService.ts
|
|
74
|
+
import type { Order, CreateOrderData } from '../../domain/Order';
|
|
75
|
+
|
|
76
|
+
export interface OrderService {
|
|
77
|
+
createOrder(data: CreateOrderData): Promise<Order>;
|
|
78
|
+
getOrder(id: string): Promise<Order | null>;
|
|
79
|
+
listUserOrders(userId: string): Promise<Order[]>;
|
|
80
|
+
cancelOrder(id: string): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Driven Ports (Secondary/Outbound)
|
|
85
|
+
|
|
86
|
+
**Purpose:** Define how the application interacts with external systems.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// core/ports/driven/OrderRepository.ts
|
|
90
|
+
import type { Order, CreateOrderData } from '../../domain/Order';
|
|
91
|
+
|
|
92
|
+
export interface OrderRepository {
|
|
93
|
+
save(order: Order): Promise<Order>;
|
|
94
|
+
findById(id: string): Promise<Order | null>;
|
|
95
|
+
findByUserId(userId: string): Promise<Order[]>;
|
|
96
|
+
delete(id: string): Promise<void>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// core/ports/driven/PaymentGateway.ts
|
|
100
|
+
export interface PaymentGateway {
|
|
101
|
+
processPayment(amount: number, token: string): Promise<PaymentResult>;
|
|
102
|
+
refund(transactionId: string): Promise<RefundResult>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// core/ports/driven/NotificationSender.ts
|
|
106
|
+
export interface NotificationSender {
|
|
107
|
+
sendEmail(to: string, subject: string, body: string): Promise<void>;
|
|
108
|
+
sendSMS(phone: string, message: string): Promise<void>;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Adapter Types
|
|
113
|
+
|
|
114
|
+
### Driving Adapters (Primary)
|
|
115
|
+
|
|
116
|
+
**Purpose:** Adapt external requests to port calls.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// adapters/driving/rest/OrderController.ts
|
|
120
|
+
import type { Request, Response } from 'express';
|
|
121
|
+
import type { OrderService } from '../../../core/ports/driving/OrderService';
|
|
122
|
+
|
|
123
|
+
export class OrderRestController {
|
|
124
|
+
constructor(private orderService: OrderService) {}
|
|
125
|
+
|
|
126
|
+
async create(req: Request, res: Response): Promise<void> {
|
|
127
|
+
const order = await this.orderService.createOrder(req.body);
|
|
128
|
+
res.status(201).json(order);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async get(req: Request, res: Response): Promise<void> {
|
|
132
|
+
const order = await this.orderService.getOrder(req.params.id);
|
|
133
|
+
if (!order) {
|
|
134
|
+
res.status(404).json({ error: 'Order not found' });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
res.json(order);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// adapters/driving/cli/OrderCLI.ts
|
|
142
|
+
import type { OrderService } from '../../../core/ports/driving/OrderService';
|
|
143
|
+
|
|
144
|
+
export class OrderCLI {
|
|
145
|
+
constructor(private orderService: OrderService) {}
|
|
146
|
+
|
|
147
|
+
async handleCommand(args: string[]): Promise<void> {
|
|
148
|
+
const [command, ...rest] = args;
|
|
149
|
+
|
|
150
|
+
switch (command) {
|
|
151
|
+
case 'create':
|
|
152
|
+
const order = await this.orderService.createOrder({
|
|
153
|
+
userId: rest[0],
|
|
154
|
+
items: JSON.parse(rest[1]),
|
|
155
|
+
});
|
|
156
|
+
console.log(`Created order: ${order.id}`);
|
|
157
|
+
break;
|
|
158
|
+
case 'get':
|
|
159
|
+
const found = await this.orderService.getOrder(rest[0]);
|
|
160
|
+
console.log(found || 'Order not found');
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// adapters/driving/graphql/OrderResolver.ts
|
|
167
|
+
import type { OrderService } from '../../../core/ports/driving/OrderService';
|
|
168
|
+
|
|
169
|
+
export class OrderResolver {
|
|
170
|
+
constructor(private orderService: OrderService) {}
|
|
171
|
+
|
|
172
|
+
async order(_: unknown, { id }: { id: string }) {
|
|
173
|
+
return this.orderService.getOrder(id);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async createOrder(_: unknown, { input }: { input: CreateOrderData }) {
|
|
177
|
+
return this.orderService.createOrder(input);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Driven Adapters (Secondary)
|
|
183
|
+
|
|
184
|
+
**Purpose:** Implement ports for external systems.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// adapters/driven/persistence/PostgresOrderRepository.ts
|
|
188
|
+
import type { Order } from '../../../core/domain/Order';
|
|
189
|
+
import type { OrderRepository } from '../../../core/ports/driven/OrderRepository';
|
|
190
|
+
import { pool } from './database';
|
|
191
|
+
|
|
192
|
+
export class PostgresOrderRepository implements OrderRepository {
|
|
193
|
+
async save(order: Order): Promise<Order> {
|
|
194
|
+
const result = await pool.query(
|
|
195
|
+
`INSERT INTO orders (id, user_id, items, total, status, created_at)
|
|
196
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
197
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
198
|
+
items = EXCLUDED.items,
|
|
199
|
+
total = EXCLUDED.total,
|
|
200
|
+
status = EXCLUDED.status
|
|
201
|
+
RETURNING *`,
|
|
202
|
+
[order.id, order.userId, order.items, order.total, order.status, order.createdAt]
|
|
203
|
+
);
|
|
204
|
+
return this.toDomain(result.rows[0]);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async findById(id: string): Promise<Order | null> {
|
|
208
|
+
const result = await pool.query('SELECT * FROM orders WHERE id = $1', [id]);
|
|
209
|
+
return result.rows[0] ? this.toDomain(result.rows[0]) : null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async findByUserId(userId: string): Promise<Order[]> {
|
|
213
|
+
const result = await pool.query('SELECT * FROM orders WHERE user_id = $1', [userId]);
|
|
214
|
+
return result.rows.map(this.toDomain);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async delete(id: string): Promise<void> {
|
|
218
|
+
await pool.query('DELETE FROM orders WHERE id = $1', [id]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private toDomain(row: any): Order {
|
|
222
|
+
return {
|
|
223
|
+
id: row.id,
|
|
224
|
+
userId: row.user_id,
|
|
225
|
+
items: row.items,
|
|
226
|
+
total: parseFloat(row.total),
|
|
227
|
+
status: row.status,
|
|
228
|
+
createdAt: new Date(row.created_at),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// adapters/driven/payment/StripePaymentGateway.ts
|
|
234
|
+
import type { PaymentGateway, PaymentResult } from '../../../core/ports/driven/PaymentGateway';
|
|
235
|
+
import Stripe from 'stripe';
|
|
236
|
+
|
|
237
|
+
export class StripePaymentGateway implements PaymentGateway {
|
|
238
|
+
private stripe: Stripe;
|
|
239
|
+
|
|
240
|
+
constructor(apiKey: string) {
|
|
241
|
+
this.stripe = new Stripe(apiKey);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async processPayment(amount: number, token: string): Promise<PaymentResult> {
|
|
245
|
+
const charge = await this.stripe.charges.create({
|
|
246
|
+
amount: Math.round(amount * 100),
|
|
247
|
+
currency: 'usd',
|
|
248
|
+
source: token,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
success: charge.status === 'succeeded',
|
|
253
|
+
transactionId: charge.id,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async refund(transactionId: string): Promise<RefundResult> {
|
|
258
|
+
const refund = await this.stripe.refunds.create({
|
|
259
|
+
charge: transactionId,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
success: refund.status === 'succeeded',
|
|
264
|
+
refundId: refund.id,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// adapters/driven/notification/SendGridNotificationSender.ts
|
|
270
|
+
import type { NotificationSender } from '../../../core/ports/driven/NotificationSender';
|
|
271
|
+
import sgMail from '@sendgrid/mail';
|
|
272
|
+
|
|
273
|
+
export class SendGridNotificationSender implements NotificationSender {
|
|
274
|
+
constructor(apiKey: string) {
|
|
275
|
+
sgMail.setApiKey(apiKey);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async sendEmail(to: string, subject: string, body: string): Promise<void> {
|
|
279
|
+
await sgMail.send({
|
|
280
|
+
to,
|
|
281
|
+
from: 'noreply@example.com',
|
|
282
|
+
subject,
|
|
283
|
+
text: body,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async sendSMS(phone: string, message: string): Promise<void> {
|
|
288
|
+
// Implementation with Twilio or similar
|
|
289
|
+
console.log(`SMS to ${phone}: ${message}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Application Core
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
// core/domain/Order.ts
|
|
298
|
+
export interface Order {
|
|
299
|
+
id: string;
|
|
300
|
+
userId: string;
|
|
301
|
+
items: OrderItem[];
|
|
302
|
+
total: number;
|
|
303
|
+
status: OrderStatus;
|
|
304
|
+
createdAt: Date;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export type OrderStatus = 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled';
|
|
308
|
+
|
|
309
|
+
export interface OrderItem {
|
|
310
|
+
productId: string;
|
|
311
|
+
quantity: number;
|
|
312
|
+
price: number;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface CreateOrderData {
|
|
316
|
+
userId: string;
|
|
317
|
+
items: Omit<OrderItem, 'price'>[];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// core/services/OrderServiceImpl.ts
|
|
321
|
+
import type { Order, CreateOrderData } from '../domain/Order';
|
|
322
|
+
import type { OrderService } from '../ports/driving/OrderService';
|
|
323
|
+
import type { OrderRepository } from '../ports/driven/OrderRepository';
|
|
324
|
+
import type { PaymentGateway } from '../ports/driven/PaymentGateway';
|
|
325
|
+
import type { NotificationSender } from '../ports/driven/NotificationSender';
|
|
326
|
+
import { v4 as uuid } from 'uuid';
|
|
327
|
+
|
|
328
|
+
export class OrderServiceImpl implements OrderService {
|
|
329
|
+
constructor(
|
|
330
|
+
private orderRepo: OrderRepository,
|
|
331
|
+
private paymentGateway: PaymentGateway,
|
|
332
|
+
private notificationSender: NotificationSender
|
|
333
|
+
) {}
|
|
334
|
+
|
|
335
|
+
async createOrder(data: CreateOrderData): Promise<Order> {
|
|
336
|
+
// Calculate prices
|
|
337
|
+
const items = await this.enrichWithPrices(data.items);
|
|
338
|
+
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
|
339
|
+
|
|
340
|
+
const order: Order = {
|
|
341
|
+
id: uuid(),
|
|
342
|
+
userId: data.userId,
|
|
343
|
+
items,
|
|
344
|
+
total,
|
|
345
|
+
status: 'pending',
|
|
346
|
+
createdAt: new Date(),
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const savedOrder = await this.orderRepo.save(order);
|
|
350
|
+
|
|
351
|
+
await this.notificationSender.sendEmail(
|
|
352
|
+
data.userId,
|
|
353
|
+
'Order Created',
|
|
354
|
+
`Your order ${order.id} has been created.`
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
return savedOrder;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async getOrder(id: string): Promise<Order | null> {
|
|
361
|
+
return this.orderRepo.findById(id);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async listUserOrders(userId: string): Promise<Order[]> {
|
|
365
|
+
return this.orderRepo.findByUserId(userId);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async cancelOrder(id: string): Promise<void> {
|
|
369
|
+
const order = await this.orderRepo.findById(id);
|
|
370
|
+
if (!order) throw new Error('Order not found');
|
|
371
|
+
if (order.status !== 'pending') throw new Error('Cannot cancel non-pending order');
|
|
372
|
+
|
|
373
|
+
order.status = 'cancelled';
|
|
374
|
+
await this.orderRepo.save(order);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private async enrichWithPrices(items: Omit<OrderItem, 'price'>[]): Promise<OrderItem[]> {
|
|
378
|
+
// Fetch prices from catalog service
|
|
379
|
+
return items.map((item) => ({ ...item, price: 10 }));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Project Structure
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
src/
|
|
388
|
+
├── core/ # Application Core (Hexagon)
|
|
389
|
+
│ ├── domain/ # Domain entities and value objects
|
|
390
|
+
│ │ ├── Order.ts
|
|
391
|
+
│ │ ├── User.ts
|
|
392
|
+
│ │ └── Product.ts
|
|
393
|
+
│ ├── ports/
|
|
394
|
+
│ │ ├── driving/ # Inbound ports (use cases)
|
|
395
|
+
│ │ │ ├── OrderService.ts
|
|
396
|
+
│ │ │ └── UserService.ts
|
|
397
|
+
│ │ └── driven/ # Outbound ports (repositories, gateways)
|
|
398
|
+
│ │ ├── OrderRepository.ts
|
|
399
|
+
│ │ ├── PaymentGateway.ts
|
|
400
|
+
│ │ └── NotificationSender.ts
|
|
401
|
+
│ └── services/ # Domain services implementing driving ports
|
|
402
|
+
│ ├── OrderServiceImpl.ts
|
|
403
|
+
│ └── UserServiceImpl.ts
|
|
404
|
+
└── adapters/
|
|
405
|
+
├── driving/ # Primary/Inbound adapters
|
|
406
|
+
│ ├── rest/
|
|
407
|
+
│ │ ├── OrderController.ts
|
|
408
|
+
│ │ └── routes.ts
|
|
409
|
+
│ ├── graphql/
|
|
410
|
+
│ │ └── OrderResolver.ts
|
|
411
|
+
│ └── cli/
|
|
412
|
+
│ └── OrderCLI.ts
|
|
413
|
+
└── driven/ # Secondary/Outbound adapters
|
|
414
|
+
├── persistence/
|
|
415
|
+
│ ├── PostgresOrderRepository.ts
|
|
416
|
+
│ └── database.ts
|
|
417
|
+
├── payment/
|
|
418
|
+
│ └── StripePaymentGateway.ts
|
|
419
|
+
└── notification/
|
|
420
|
+
└── SendGridNotificationSender.ts
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Composition Root
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
// main.ts - Application composition
|
|
427
|
+
import { OrderServiceImpl } from './core/services/OrderServiceImpl';
|
|
428
|
+
import { PostgresOrderRepository } from './adapters/driven/persistence/PostgresOrderRepository';
|
|
429
|
+
import { StripePaymentGateway } from './adapters/driven/payment/StripePaymentGateway';
|
|
430
|
+
import { SendGridNotificationSender } from './adapters/driven/notification/SendGridNotificationSender';
|
|
431
|
+
import { OrderRestController } from './adapters/driving/rest/OrderController';
|
|
432
|
+
import express from 'express';
|
|
433
|
+
|
|
434
|
+
// Create driven adapters (outbound)
|
|
435
|
+
const orderRepo = new PostgresOrderRepository();
|
|
436
|
+
const paymentGateway = new StripePaymentGateway(process.env.STRIPE_KEY!);
|
|
437
|
+
const notificationSender = new SendGridNotificationSender(process.env.SENDGRID_KEY!);
|
|
438
|
+
|
|
439
|
+
// Create core service
|
|
440
|
+
const orderService = new OrderServiceImpl(orderRepo, paymentGateway, notificationSender);
|
|
441
|
+
|
|
442
|
+
// Create driving adapters (inbound)
|
|
443
|
+
const orderController = new OrderRestController(orderService);
|
|
444
|
+
|
|
445
|
+
// Wire up Express
|
|
446
|
+
const app = express();
|
|
447
|
+
app.post('/orders', (req, res) => orderController.create(req, res));
|
|
448
|
+
app.get('/orders/:id', (req, res) => orderController.get(req, res));
|
|
449
|
+
|
|
450
|
+
app.listen(3000);
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
## Testing with Test Adapters
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
// tests/core/services/OrderServiceImpl.test.ts
|
|
457
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
458
|
+
import { OrderServiceImpl } from '../../../src/core/services/OrderServiceImpl';
|
|
459
|
+
|
|
460
|
+
// In-memory test adapters
|
|
461
|
+
class InMemoryOrderRepository {
|
|
462
|
+
private orders: Map<string, Order> = new Map();
|
|
463
|
+
|
|
464
|
+
async save(order: Order): Promise<Order> {
|
|
465
|
+
this.orders.set(order.id, order);
|
|
466
|
+
return order;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async findById(id: string): Promise<Order | null> {
|
|
470
|
+
return this.orders.get(id) || null;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async findByUserId(userId: string): Promise<Order[]> {
|
|
474
|
+
return Array.from(this.orders.values()).filter((o) => o.userId === userId);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
class MockPaymentGateway {
|
|
479
|
+
processPayment = vi.fn().mockResolvedValue({ success: true, transactionId: 'tx-1' });
|
|
480
|
+
refund = vi.fn().mockResolvedValue({ success: true, refundId: 'ref-1' });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
class MockNotificationSender {
|
|
484
|
+
sendEmail = vi.fn().mockResolvedValue(undefined);
|
|
485
|
+
sendSMS = vi.fn().mockResolvedValue(undefined);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
describe('OrderServiceImpl', () => {
|
|
489
|
+
it('should create an order', async () => {
|
|
490
|
+
const orderRepo = new InMemoryOrderRepository();
|
|
491
|
+
const paymentGateway = new MockPaymentGateway();
|
|
492
|
+
const notificationSender = new MockNotificationSender();
|
|
493
|
+
|
|
494
|
+
const service = new OrderServiceImpl(orderRepo, paymentGateway, notificationSender);
|
|
495
|
+
|
|
496
|
+
const order = await service.createOrder({
|
|
497
|
+
userId: 'user-1',
|
|
498
|
+
items: [{ productId: 'prod-1', quantity: 2 }],
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
expect(order.id).toBeDefined();
|
|
502
|
+
expect(order.userId).toBe('user-1');
|
|
503
|
+
expect(notificationSender.sendEmail).toHaveBeenCalled();
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
## Best Practices
|
|
509
|
+
|
|
510
|
+
### Do's
|
|
511
|
+
|
|
512
|
+
- Define ports before implementing adapters
|
|
513
|
+
- Keep the core domain free of framework code
|
|
514
|
+
- Use dependency injection for all adapters
|
|
515
|
+
- Create test adapters for unit testing
|
|
516
|
+
- Name ports after domain concepts, not technology
|
|
517
|
+
|
|
518
|
+
### Don'ts
|
|
519
|
+
|
|
520
|
+
- Don't let the core import from adapters
|
|
521
|
+
- Don't put business logic in adapters
|
|
522
|
+
- Don't create circular dependencies
|
|
523
|
+
- Don't skip ports for "simple" integrations
|
|
524
|
+
- Don't expose adapter types in ports
|
|
525
|
+
|
|
526
|
+
## Hexagonal vs Clean Architecture
|
|
527
|
+
|
|
528
|
+
| Aspect | Hexagonal | Clean Architecture |
|
|
529
|
+
|--------|-----------|-------------------|
|
|
530
|
+
| Focus | Ports & Adapters | Concentric layers |
|
|
531
|
+
| Terminology | Driving/Driven | Use Cases/Entities |
|
|
532
|
+
| Structure | Inside/Outside | Layers (4+) |
|
|
533
|
+
| Interfaces | Ports only | Multiple per layer |
|
|
534
|
+
| Complexity | Simpler | More structured |
|
|
535
|
+
|
|
536
|
+
## When to Use Hexagonal
|
|
537
|
+
|
|
538
|
+
**Good for:**
|
|
539
|
+
|
|
540
|
+
- Applications with multiple entry points (REST, CLI, GraphQL)
|
|
541
|
+
- Integration-heavy applications
|
|
542
|
+
- Systems needing easy adapter swapping
|
|
543
|
+
- Microservices architecture
|
|
544
|
+
- Applications requiring high testability
|
|
545
|
+
|
|
546
|
+
**Less suitable for:**
|
|
547
|
+
|
|
548
|
+
- Simple CRUD applications
|
|
549
|
+
- Prototypes with unclear requirements
|
|
550
|
+
- Very small applications
|
|
551
|
+
- Teams unfamiliar with the pattern
|
|
552
|
+
|
|
553
|
+
## Output
|
|
554
|
+
|
|
555
|
+
**Produces:**
|
|
556
|
+
|
|
557
|
+
- Core domain with clean boundaries
|
|
558
|
+
- Port interfaces for all external interactions
|
|
559
|
+
- Adapters for each technology/framework
|
|
560
|
+
- Highly testable application code
|
|
561
|
+
|
|
562
|
+
**Success Criteria:**
|
|
563
|
+
|
|
564
|
+
- Core has zero external dependencies
|
|
565
|
+
- All external interactions go through ports
|
|
566
|
+
- Adapters are easily swappable
|
|
567
|
+
- Tests use in-memory/mock adapters
|