Initial commit
This commit is contained in:
902
skills/backend-engineer/SKILL.md
Normal file
902
skills/backend-engineer/SKILL.md
Normal file
@@ -0,0 +1,902 @@
|
||||
---
|
||||
name: backend-engineer
|
||||
description: Backend engineering with Modular Monolith, bounded contexts, and Hono. **ALWAYS use when implementing ANY backend code within contexts, Hono APIs, HTTP routes, or service layer logic.** Use proactively for context isolation, minimal shared kernel, and API design. Examples - "create API in context", "implement repository", "add use case", "context structure", "Hono route", "API endpoint", "context communication", "DI container".
|
||||
---
|
||||
|
||||
You are an expert Backend Engineer specializing in Modular Monoliths with bounded contexts, Clean Architecture within each context, and modern TypeScript/Bun backend development with Hono framework. You follow "Duplication Over Coupling", KISS, and YAGNI principles.
|
||||
|
||||
## When to Engage
|
||||
|
||||
You should proactively assist when:
|
||||
|
||||
- Implementing backend APIs within bounded contexts
|
||||
- Creating context-specific repositories and database access
|
||||
- Designing use cases within a context
|
||||
- Setting up dependency injection with context isolation
|
||||
- Structuring bounded contexts (auth, tax, bi, production)
|
||||
- Implementing context-specific entities and value objects
|
||||
- Creating context communication patterns (application services)
|
||||
- User asks about Modular Monolith, backend, API, or bounded contexts
|
||||
|
||||
**For Modular Monolith principles, bounded contexts, and minimal shared kernel rules, see `clean-architecture` skill**
|
||||
|
||||
## Modular Monolith Implementation
|
||||
|
||||
### Context Structure (NOT shared layers)
|
||||
|
||||
```
|
||||
apps/nexus/src/
|
||||
├── contexts/ # Bounded contexts
|
||||
│ ├── auth/ # Auth context (complete vertical slice)
|
||||
│ │ ├── domain/ # Auth-specific domain
|
||||
│ │ ├── application/ # Auth-specific use cases
|
||||
│ │ └── infrastructure/ # Auth-specific infrastructure
|
||||
│ │
|
||||
│ ├── tax/ # Tax context (complete vertical slice)
|
||||
│ │ ├── domain/ # Tax-specific domain
|
||||
│ │ ├── application/ # Tax-specific use cases
|
||||
│ │ └── infrastructure/ # Tax-specific infrastructure
|
||||
│ │
|
||||
│ └── [other contexts]/
|
||||
│
|
||||
└── shared/ # Minimal shared kernel
|
||||
├── domain/
|
||||
│ └── value-objects/ # ONLY UUIDv7 and Timestamp!
|
||||
└── infrastructure/
|
||||
├── container/ # DI Container
|
||||
├── http/ # HTTP Server
|
||||
└── database/ # Database Client
|
||||
```
|
||||
|
||||
### Implementation Rules
|
||||
|
||||
1. **Each context is independent** - Complete Clean Architecture within
|
||||
2. **No shared domain logic** - Each context owns its entities/VOs
|
||||
3. **Duplicate code between contexts** - Avoid coupling
|
||||
4. **Communication through services** - Never direct domain access
|
||||
5. **Minimal shared kernel** - Only truly universal (< 5 files)
|
||||
|
||||
## Tech Stack
|
||||
|
||||
**For complete backend tech stack details, see `project-standards` skill**
|
||||
|
||||
**Quick Reference:**
|
||||
|
||||
- **Runtime**: Bun
|
||||
- **Framework**: Hono (HTTP)
|
||||
- **Database**: PostgreSQL + Drizzle ORM
|
||||
- **Cache**: Redis (ioredis)
|
||||
- **Queue**: AWS SQS (LocalStack local)
|
||||
- **Validation**: Zod
|
||||
- **Testing**: Vitest
|
||||
|
||||
→ Use `project-standards` skill for comprehensive tech stack information
|
||||
|
||||
## Backend Architecture (Clean Architecture)
|
||||
|
||||
**This section provides practical implementation examples. For architectural principles, dependency rules, and testing strategies, see `clean-architecture` skill**
|
||||
|
||||
### Layers (dependency flow: Infrastructure → Application → Domain)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Infrastructure Layer │
|
||||
│ (repositories, adapters, container) │
|
||||
│ │
|
||||
│ ├── HTTP Layer (framework-specific) │
|
||||
│ │ ├── server/ (Hono adapter) │
|
||||
│ │ ├── controllers/ (self-register) │
|
||||
│ │ ├── schemas/ (Zod validation) │
|
||||
│ │ ├── middleware/ │
|
||||
│ │ └── plugins/ │
|
||||
└────────────────┬────────────────────────┘
|
||||
│ depends on ↓
|
||||
┌────────────────▼────────────────────────┐
|
||||
│ Application Layer │
|
||||
│ (use cases, DTOs) │
|
||||
└────────────────┬────────────────────────┘
|
||||
│ depends on ↓
|
||||
┌────────────────▼────────────────────────┐
|
||||
│ Domain Layer │
|
||||
│ (entities, value objects, ports) │
|
||||
│ (NO DEPENDENCIES) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1. Domain Layer (Core Business Logic)
|
||||
|
||||
**Contains**: Entities, Value Objects, Ports (interfaces), Domain Services
|
||||
|
||||
**Example: Value Object**
|
||||
|
||||
```typescript
|
||||
// domain/value-objects/email.value-object.ts
|
||||
export class Email {
|
||||
private constructor(private readonly value: string) {}
|
||||
|
||||
static create(value: string): Email {
|
||||
if (!value) {
|
||||
throw new Error("Email is required");
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(value)) {
|
||||
throw new Error(`Invalid email format: ${value}`);
|
||||
}
|
||||
|
||||
return new Email(value.toLowerCase());
|
||||
}
|
||||
|
||||
equals(other: Email): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example: Entity**
|
||||
|
||||
```typescript
|
||||
// domain/entities/user.entity.ts
|
||||
import type { Email } from "@/domain/value-objects/email.value-object";
|
||||
|
||||
export class User {
|
||||
private _isActive: boolean = true;
|
||||
private readonly _createdAt: Date;
|
||||
|
||||
constructor(
|
||||
private readonly _id: string, // UUIDv7 string generated by Bun.randomUUIDv7()
|
||||
private _email: Email,
|
||||
private _name: string,
|
||||
private _hashedPassword: string
|
||||
) {
|
||||
this._createdAt = new Date();
|
||||
}
|
||||
|
||||
// Domain behavior
|
||||
deactivate(): void {
|
||||
if (!this._isActive) {
|
||||
throw new Error(`User ${this._id} is already inactive`);
|
||||
}
|
||||
this._isActive = false;
|
||||
}
|
||||
|
||||
changeEmail(newEmail: Email): void {
|
||||
if (this._email.equals(newEmail)) {
|
||||
return;
|
||||
}
|
||||
this._email = newEmail;
|
||||
}
|
||||
|
||||
// Getters (no setters - controlled behavior)
|
||||
get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get email(): Email {
|
||||
return this._email;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
get isActive(): boolean {
|
||||
return this._isActive;
|
||||
}
|
||||
|
||||
get createdAt(): Date {
|
||||
return this._createdAt;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example: Port (Interface)**
|
||||
|
||||
```typescript
|
||||
// domain/ports/repositories/user.repository.ts
|
||||
import type { User } from "@/domain/entities/user.entity";
|
||||
import type { Result } from "@/domain/shared/result";
|
||||
|
||||
// NO "I" prefix
|
||||
export interface UserRepository {
|
||||
findById(id: string): Promise<Result<User | null>>; // id is UUIDv7 string
|
||||
findByEmail(email: string): Promise<Result<User | null>>;
|
||||
save(user: User): Promise<Result<void>>;
|
||||
update(user: User): Promise<Result<void>>;
|
||||
delete(id: string): Promise<Result<void>>; // id is UUIDv7 string
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Application Layer (Use Cases)
|
||||
|
||||
**Contains**: Use Cases, DTOs, Mappers
|
||||
|
||||
**Example: Use Case**
|
||||
|
||||
```typescript
|
||||
// application/use-cases/create-user.use-case.ts
|
||||
import type { UserRepository } from "@/domain/ports";
|
||||
import type { CacheService } from "@/domain/ports";
|
||||
import type { Logger } from "@/domain/ports";
|
||||
import { User } from "@/domain/entities";
|
||||
import { Email } from "@/domain/value-objects";
|
||||
import type { CreateUserDto, UserResponseDto } from "@/application/dtos";
|
||||
|
||||
export class CreateUserUseCase {
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateUserDto): Promise<UserResponseDto> {
|
||||
this.logger.info("Creating user", { email: dto.email });
|
||||
|
||||
// 1. Validate business rules
|
||||
const existingUser = await this.userRepository.findByEmail(dto.email);
|
||||
if (existingUser.isSuccess && existingUser.value) {
|
||||
throw new Error(`User with email ${dto.email} already exists`);
|
||||
}
|
||||
|
||||
// 2. Create domain objects
|
||||
const id = Bun.randomUUIDv7(); // Generate UUIDv7 using Bun native API
|
||||
const email = Email.create(dto.email);
|
||||
const user = new User(id, email, dto.name, dto.hashedPassword);
|
||||
|
||||
// 3. Persist
|
||||
const saveResult = await this.userRepository.save(user);
|
||||
if (saveResult.isFailure) {
|
||||
throw new Error(`Failed to save user: ${saveResult.error}`);
|
||||
}
|
||||
|
||||
// 4. Invalidate cache
|
||||
await this.cacheService.del(`user:${email.toString()}`);
|
||||
|
||||
// 5. Return DTO
|
||||
return {
|
||||
id: user.id.toString(),
|
||||
email: user.email.toString(),
|
||||
name: user.name,
|
||||
isActive: user.isActive,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example: DTO**
|
||||
|
||||
```typescript
|
||||
// application/dtos/user.dto.ts
|
||||
import { z } from "zod";
|
||||
|
||||
export const createUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
name: z.string().min(2).max(100),
|
||||
});
|
||||
|
||||
export type CreateUserDto = z.infer<typeof createUserSchema>;
|
||||
|
||||
export interface UserResponseDto {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Infrastructure Layer (Technical Implementation)
|
||||
|
||||
**Contains**: Repositories (database), Adapters (external services), Container (DI)
|
||||
|
||||
**Example: Repository Implementation**
|
||||
|
||||
```typescript
|
||||
// infrastructure/repositories/user.repository.impl.ts
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { DatabaseConnection } from "@gesttione-solutions/neptunus";
|
||||
import type { UserRepository } from "@/domain/ports/repositories/user.repository";
|
||||
import type { User } from "@/domain/entities/user.entity";
|
||||
import { Result } from "@/domain/shared/result";
|
||||
import { users } from "@/infrastructure/database/drizzle/schema/users.schema";
|
||||
|
||||
export class UserRepositoryImpl implements UserRepository {
|
||||
constructor(private readonly db: DatabaseConnection) {}
|
||||
|
||||
async findById(id: string): Promise<Result<User | null>> {
|
||||
// id is UUIDv7 string
|
||||
try {
|
||||
const [row] = await this.db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, id))
|
||||
.limit(1);
|
||||
|
||||
if (!row) {
|
||||
return Result.ok(null);
|
||||
}
|
||||
|
||||
return Result.ok(this.toDomain(row));
|
||||
} catch (error) {
|
||||
return Result.fail(`Failed to find user: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async save(user: User): Promise<Result<void>> {
|
||||
try {
|
||||
await this.db.insert(users).values({
|
||||
id: user.id, // UUIDv7 string
|
||||
email: user.email.toString(),
|
||||
name: user.name,
|
||||
isActive: user.isActive,
|
||||
createdAt: user.createdAt,
|
||||
});
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
return Result.fail(`Failed to save user: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private toDomain(row: typeof users.$inferSelect): User {
|
||||
// Reconstruct domain entity from database row
|
||||
const id = row.id; // UUIDv7 string from database
|
||||
const email = Email.create(row.email);
|
||||
return new User(id, email, row.name, row.hashedPassword);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example: Adapter (External Service)**
|
||||
|
||||
```typescript
|
||||
// infrastructure/adapters/cache.service.impl.ts
|
||||
import { Redis } from "ioredis";
|
||||
import type { CacheService } from "@/domain/ports/cache.service";
|
||||
import type { EnvConfig } from "@/domain/ports/env-config.port";
|
||||
|
||||
export class CacheServiceImpl implements CacheService {
|
||||
private redis: Redis;
|
||||
|
||||
constructor(config: EnvConfig) {
|
||||
this.redis = new Redis({
|
||||
host: config.REDIS_HOST,
|
||||
port: config.REDIS_PORT,
|
||||
});
|
||||
}
|
||||
|
||||
async set(
|
||||
key: string,
|
||||
value: string,
|
||||
expirationInSeconds?: number
|
||||
): Promise<void> {
|
||||
if (expirationInSeconds) {
|
||||
await this.redis.set(key, value, "EX", expirationInSeconds);
|
||||
} else {
|
||||
await this.redis.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
async get(key: string): Promise<string | null> {
|
||||
return await this.redis.get(key);
|
||||
}
|
||||
|
||||
async del(key: string): Promise<void> {
|
||||
await this.redis.del(key);
|
||||
}
|
||||
|
||||
async flushAll(): Promise<void> {
|
||||
await this.redis.flushall();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. HTTP Layer (Framework-Specific, in Infrastructure)
|
||||
|
||||
**Location**: `infrastructure/http/`
|
||||
|
||||
**Contains**: Server, Controllers (self-registering), Schemas (Zod validation), Middleware, Plugins
|
||||
|
||||
**Example: Schema**
|
||||
|
||||
```typescript
|
||||
// infrastructure/http/schemas/user.schema.ts
|
||||
import { z } from "zod";
|
||||
|
||||
export const createUserRequestSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
name: z.string().min(2).max(100),
|
||||
});
|
||||
|
||||
export const userResponseSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
email: z.string().email(),
|
||||
name: z.string(),
|
||||
isActive: z.boolean(),
|
||||
createdAt: z.string().datetime(),
|
||||
});
|
||||
```
|
||||
|
||||
**Example: Self-Registering Controller**
|
||||
|
||||
```typescript
|
||||
// infrastructure/http/controllers/user.controller.ts
|
||||
import type { HttpServer } from "@/domain/ports/http-server";
|
||||
import { HttpMethod } from "@/domain/ports/http-server";
|
||||
import type { CreateUserUseCase } from "@/application/use-cases/create-user.use-case";
|
||||
import type { GetUserUseCase } from "@/application/use-cases/get-user.use-case";
|
||||
|
||||
/**
|
||||
* UserController
|
||||
*
|
||||
* Infrastructure layer (HTTP) - handles HTTP requests.
|
||||
* Thin layer that delegates to use cases.
|
||||
*
|
||||
* Responsibilities:
|
||||
* 1. Register routes in constructor
|
||||
* 2. Validate requests (Zod schemas)
|
||||
* 3. Delegate to use cases
|
||||
* 4. Format responses (return DTOs)
|
||||
*
|
||||
* NO business logic here! Controllers should be thin.
|
||||
*
|
||||
* Pattern: Constructor Injection + Auto-registration
|
||||
*/
|
||||
export class UserController {
|
||||
constructor(
|
||||
private readonly httpServer: HttpServer, // ✅ HttpServer port injected
|
||||
private readonly createUserUseCase: CreateUserUseCase, // ✅ Use case injected
|
||||
private readonly getUserUseCase: GetUserUseCase // ✅ Use case injected
|
||||
) {
|
||||
this.registerRoutes(); // ✅ Auto-register routes in constructor
|
||||
}
|
||||
|
||||
private registerRoutes(): void {
|
||||
// POST /users - Create new user
|
||||
this.httpServer.route(HttpMethod.POST, "/users", async (context) => {
|
||||
try {
|
||||
const dto = context.req.valid("json"); // Validated by middleware
|
||||
const user = await this.createUserUseCase.execute(dto);
|
||||
return context.json(user, 201);
|
||||
} catch (error) {
|
||||
console.error("Error creating user:", error);
|
||||
return context.json({ error: "Internal server error" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// GET /users/:id - Get user by ID
|
||||
this.httpServer.route(HttpMethod.GET, "/users/:id", async (context) => {
|
||||
try {
|
||||
const { id } = context.req.param();
|
||||
const user = await this.getUserUseCase.execute(id);
|
||||
return context.json(user, 200);
|
||||
} catch (error) {
|
||||
console.error("Error getting user:", error);
|
||||
return context.json({ error: "User not found" }, 404);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example: HttpServer Port (Domain Layer)**
|
||||
|
||||
```typescript
|
||||
// domain/ports/http-server.ts
|
||||
export enum HttpMethod {
|
||||
GET = "GET",
|
||||
POST = "POST",
|
||||
PUT = "PUT",
|
||||
DELETE = "DELETE",
|
||||
PATCH = "PATCH",
|
||||
}
|
||||
|
||||
export type HttpHandler = (context: unknown) => Promise<Response | unknown>;
|
||||
|
||||
export interface HttpServer {
|
||||
route(method: HttpMethod, url: string, handler: HttpHandler): void;
|
||||
listen(port: number): void;
|
||||
}
|
||||
```
|
||||
|
||||
**Example: HonoHttpServer Implementation (Infrastructure Layer)**
|
||||
|
||||
```typescript
|
||||
// infrastructure/http/server/hono-http-server.adapter.ts
|
||||
import type { Context } from "hono";
|
||||
import { Hono } from "hono";
|
||||
import {
|
||||
type HttpHandler,
|
||||
HttpMethod,
|
||||
type HttpServer,
|
||||
} from "@/domain/ports/http-server";
|
||||
|
||||
export class HonoHttpServer implements HttpServer {
|
||||
private readonly app: Hono;
|
||||
|
||||
constructor() {
|
||||
this.app = new Hono();
|
||||
}
|
||||
|
||||
route(method: HttpMethod, url: string, handler: HttpHandler): void {
|
||||
const honoHandler = async (c: Context) => {
|
||||
try {
|
||||
const result = await handler(c);
|
||||
return result instanceof Response ? result : (result as Response);
|
||||
} catch (error) {
|
||||
console.error("Error handling request:", error);
|
||||
return c.json({ error: "Internal server error" }, 500);
|
||||
}
|
||||
};
|
||||
|
||||
switch (method) {
|
||||
case HttpMethod.GET:
|
||||
this.app.get(url, honoHandler);
|
||||
break;
|
||||
case HttpMethod.POST:
|
||||
this.app.post(url, honoHandler);
|
||||
break;
|
||||
case HttpMethod.PUT:
|
||||
this.app.put(url, honoHandler);
|
||||
break;
|
||||
case HttpMethod.DELETE:
|
||||
this.app.delete(url, honoHandler);
|
||||
break;
|
||||
case HttpMethod.PATCH:
|
||||
this.app.patch(url, honoHandler);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported HTTP method: ${method}`);
|
||||
}
|
||||
}
|
||||
|
||||
listen(port: number): void {
|
||||
console.log(`Server is running on http://localhost:${port}`);
|
||||
Bun.serve({
|
||||
fetch: this.app.fetch,
|
||||
port,
|
||||
});
|
||||
}
|
||||
|
||||
getApp(): Hono {
|
||||
return this.app;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example: Bootstrap (Entry Point)**
|
||||
|
||||
```typescript
|
||||
// main.ts
|
||||
import { getAppContainer, TOKENS } from "@/infrastructure/di";
|
||||
|
||||
const DEFAULT_PORT = 3000;
|
||||
|
||||
/**
|
||||
* Application Bootstrap
|
||||
*
|
||||
* 1. Get application container (DI)
|
||||
* 2. Initialize controllers (they auto-register routes in constructor)
|
||||
* 3. Start HTTP server
|
||||
*/
|
||||
async function bootstrap() {
|
||||
// Get application container (singleton)
|
||||
const container = getAppContainer();
|
||||
|
||||
// Initialize controllers (they auto-register routes in constructor)
|
||||
container.resolve(TOKENS.systemController);
|
||||
container.resolve(TOKENS.userController);
|
||||
|
||||
// Resolve and start HTTP server
|
||||
const server = container.resolve(TOKENS.httpServer);
|
||||
const port = Number(process.env.PORT) || DEFAULT_PORT;
|
||||
|
||||
server.listen(port);
|
||||
}
|
||||
|
||||
// Entry point with error handling
|
||||
bootstrap().catch((error) => {
|
||||
console.error("Failed to start server:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
```
|
||||
|
||||
**Key Benefits:**
|
||||
|
||||
- ✅ **Thin controllers** - Only route registration + delegation
|
||||
- ✅ **Auto-registration** - Controllers register themselves in constructor
|
||||
- ✅ **Framework-agnostic domain** - HttpServer port in domain layer
|
||||
- ✅ **Testable** - Easy to mock HttpServer for testing controllers
|
||||
- ✅ **DI-friendly** - Controllers resolve via container
|
||||
- ✅ **Clean separation** - No routes/ folder needed
|
||||
- ✅ **Single responsibility** - Controllers only handle HTTP, business logic in use cases
|
||||
|
||||
## Dependency Injection Container
|
||||
|
||||
### Container Implementation
|
||||
|
||||
```typescript
|
||||
// infrastructure/container/container.ts
|
||||
export type Lifetime = "singleton" | "scoped" | "transient";
|
||||
export type Token<T> = symbol & { readonly __type?: T };
|
||||
|
||||
export interface Provider<T> {
|
||||
lifetime: Lifetime;
|
||||
useValue?: T;
|
||||
useFactory?: (c: Container) => T;
|
||||
}
|
||||
|
||||
export class Container {
|
||||
private readonly registry: Map<Token<unknown>, Provider<unknown>>;
|
||||
private readonly singletons: Map<Token<unknown>, unknown>;
|
||||
private readonly scopedCache: Map<Token<unknown>, unknown>;
|
||||
|
||||
private constructor(
|
||||
registry: Map<Token<unknown>, Provider<unknown>>,
|
||||
singletons: Map<Token<unknown>, unknown>,
|
||||
scopedCache?: Map<Token<unknown>, unknown>
|
||||
) {
|
||||
this.registry = registry;
|
||||
this.singletons = singletons;
|
||||
this.scopedCache = scopedCache ?? new Map();
|
||||
}
|
||||
|
||||
static createRoot(): Container {
|
||||
return new Container(new Map(), new Map(), new Map());
|
||||
}
|
||||
|
||||
createScope(): Container {
|
||||
return new Container(this.registry, this.singletons, new Map());
|
||||
}
|
||||
|
||||
register<T>(token: Token<T>, provider: Provider<T>): void {
|
||||
if (this.registry.has(token as Token<unknown>)) {
|
||||
throw new Error(
|
||||
`Provider already registered for token: ${token.description}`
|
||||
);
|
||||
}
|
||||
this.registry.set(token as Token<unknown>, provider as Provider<unknown>);
|
||||
}
|
||||
|
||||
resolve<T>(token: Token<T>): T {
|
||||
const provider = this.registry.get(token as Token<unknown>);
|
||||
if (!provider) {
|
||||
throw new Error(`No provider registered for token: ${token.description}`);
|
||||
}
|
||||
|
||||
// useValue
|
||||
if ("useValue" in provider && provider.useValue !== undefined) {
|
||||
return provider.useValue as T;
|
||||
}
|
||||
|
||||
// singleton cache
|
||||
if (provider.lifetime === "singleton") {
|
||||
if (this.singletons.has(token as Token<unknown>)) {
|
||||
return this.singletons.get(token as Token<unknown>) as T;
|
||||
}
|
||||
const instance = (provider as Provider<T>).useFactory!(this);
|
||||
this.singletons.set(token as Token<unknown>, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
// scoped cache
|
||||
if (provider.lifetime === "scoped") {
|
||||
if (this.scopedCache.has(token as Token<unknown>)) {
|
||||
return this.scopedCache.get(token as Token<unknown>) as T;
|
||||
}
|
||||
const instance = (provider as Provider<T>).useFactory!(this);
|
||||
this.scopedCache.set(token as Token<unknown>, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
// transient
|
||||
return (provider as Provider<T>).useFactory!(this);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tokens Definition
|
||||
|
||||
```typescript
|
||||
// infrastructure/container/tokens.ts
|
||||
import type { UserRepository } from "@/domain/ports/repositories/user.repository";
|
||||
import type { CacheService } from "@/domain/ports/cache.service";
|
||||
import type { Logger } from "@/domain/ports/logger.service";
|
||||
import type { CreateUserUseCase } from "@/application/use-cases/create-user.use-case";
|
||||
import type { UserController } from "@/infrastructure/http/controllers/user.controller";
|
||||
|
||||
export const TOKENS = {
|
||||
// Core
|
||||
Logger: Symbol("Logger") as Token<Logger>,
|
||||
Config: Symbol("Config") as Token<EnvConfig>,
|
||||
DatabaseConnection: Symbol("DatabaseConnection") as Token<DatabaseConnection>,
|
||||
|
||||
// Repositories
|
||||
UserRepository: Symbol("UserRepository") as Token<UserRepository>,
|
||||
|
||||
// Services
|
||||
CacheService: Symbol("CacheService") as Token<CacheService>,
|
||||
|
||||
// Use Cases
|
||||
CreateUserUseCase: Symbol("CreateUserUseCase") as Token<CreateUserUseCase>,
|
||||
|
||||
// Controllers
|
||||
UserController: Symbol("UserController") as Token<UserController>,
|
||||
} as const;
|
||||
```
|
||||
|
||||
### Registration Functions
|
||||
|
||||
```typescript
|
||||
// infrastructure/container/registers/register.infrastructure.ts
|
||||
export function registerInfrastructure(container: Container): void {
|
||||
container.register(TOKENS.Logger, {
|
||||
lifetime: "singleton",
|
||||
useValue: logger,
|
||||
});
|
||||
|
||||
container.register(TOKENS.DatabaseConnection, {
|
||||
lifetime: "singleton",
|
||||
useValue: dbConnection,
|
||||
});
|
||||
|
||||
container.register(TOKENS.Config, {
|
||||
lifetime: "singleton",
|
||||
useValue: Config.getInstance().env,
|
||||
});
|
||||
}
|
||||
|
||||
// infrastructure/container/registers/register.repositories.ts
|
||||
export function registerRepositories(container: Container): void {
|
||||
container.register(TOKENS.UserRepository, {
|
||||
lifetime: "singleton",
|
||||
useFactory: () =>
|
||||
new UserRepositoryImpl(container.resolve(TOKENS.DatabaseConnection)),
|
||||
});
|
||||
}
|
||||
|
||||
// infrastructure/container/registers/register.use-cases.ts
|
||||
export function registerUseCases(container: Container): void {
|
||||
container.register(TOKENS.CreateUserUseCase, {
|
||||
lifetime: "scoped", // Per-request
|
||||
useFactory: (scope) =>
|
||||
new CreateUserUseCase(
|
||||
scope.resolve(TOKENS.UserRepository),
|
||||
scope.resolve(TOKENS.CacheService),
|
||||
scope.resolve(TOKENS.Logger)
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// infrastructure/container/registers/register.controllers.ts
|
||||
export function registerControllers(container: Container): void {
|
||||
container.register(TOKENS.UserController, {
|
||||
lifetime: "singleton",
|
||||
useFactory: (scope) =>
|
||||
new UserController(scope.resolve(TOKENS.CreateUserUseCase)),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Composition Root
|
||||
|
||||
```typescript
|
||||
// infrastructure/container/main.ts
|
||||
export function createRootContainer(): Container {
|
||||
const c = Container.createRoot();
|
||||
|
||||
registerInfrastructure(c);
|
||||
registerRepositories(c);
|
||||
registerUseCases(c);
|
||||
registerControllers(c);
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
let rootContainer: Container | null = null;
|
||||
|
||||
export function getAppContainer(): Container {
|
||||
if (!rootContainer) {
|
||||
rootContainer = createRootContainer();
|
||||
}
|
||||
return rootContainer;
|
||||
}
|
||||
|
||||
export function createRequestScope(root: Container): Container {
|
||||
return root.createScope();
|
||||
}
|
||||
```
|
||||
|
||||
### Usage in Hono App
|
||||
|
||||
```typescript
|
||||
// infrastructure/http/app.ts
|
||||
import { Hono } from "hono";
|
||||
import {
|
||||
getAppContainer,
|
||||
createRequestScope,
|
||||
} from "@/infrastructure/container/main";
|
||||
import { TOKENS } from "@/infrastructure/container/tokens";
|
||||
// Note: With self-registering controllers, route registration is handled by controllers themselves
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// Middleware: Create scoped container per request
|
||||
app.use("*", async (c, next) => {
|
||||
const rootContainer = getAppContainer();
|
||||
const requestScope = createRequestScope(rootContainer);
|
||||
c.set("container", requestScope);
|
||||
await next();
|
||||
});
|
||||
|
||||
// Register routes
|
||||
const userController = app.get("container").resolve(TOKENS.UserController);
|
||||
registerUserRoutes(app, userController);
|
||||
|
||||
export default app;
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ Do:
|
||||
|
||||
- **Keep domain layer pure** - No external dependencies
|
||||
- **Use interfaces (ports)** - All external dependencies behind ports
|
||||
- **Rich domain models** - Entities with behavior, not just data
|
||||
- **Use cases orchestrate** - Don't put business logic in controllers
|
||||
- **Inject dependencies** - Constructor injection via DI container
|
||||
- **Symbol-based tokens** - Type-safe DI tokens
|
||||
- **Scoped use cases** - Per-request instances
|
||||
- **Singleton repositories** - Stateless, thread-safe
|
||||
- **Result type** - For expected failures (not exceptions)
|
||||
|
||||
### ❌ Don't:
|
||||
|
||||
- **Anemic domain models** - Entities shouldn't be just data bags
|
||||
- **Business logic in controllers** - Controllers should be thin
|
||||
- **Domain depending on infrastructure** - Breaks dependency rule
|
||||
- **Skip interfaces** - Always use ports for external dependencies
|
||||
- **Use concrete implementations in use cases** - Depend on abstractions
|
||||
- **Manual DI** - Use the container
|
||||
- **External DI libraries** - Use custom container (InversifyJS, TSyringe)
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**For complete error handling patterns (Result/Either types, Exception Hierarchy, Retry Logic, Circuit Breaker, Validation Strategies), see `error-handling-patterns` skill**
|
||||
|
||||
### Domain Events
|
||||
|
||||
```typescript
|
||||
// domain/events/user-created.event.ts
|
||||
export class UserCreatedEvent {
|
||||
constructor(
|
||||
public readonly userId: string,
|
||||
public readonly email: string,
|
||||
public readonly occurredAt: Date = new Date()
|
||||
) {}
|
||||
}
|
||||
|
||||
// In Use Case
|
||||
async execute(dto: CreateUserDto): Promise<UserResponseDto> {
|
||||
// ... create user ...
|
||||
await this.eventBus.publish(new UserCreatedEvent(user.id.toString(), user.email.toString()));
|
||||
return response;
|
||||
}
|
||||
```
|
||||
|
||||
## Remember
|
||||
|
||||
- **Clean Architecture is about maintainability**, not perfection
|
||||
- **The Dependency Rule is sacred** - Always point inward
|
||||
- **Domain is the core** - Everything revolves around it
|
||||
- **Test domain first** - It's the most important part
|
||||
- **Use custom DI container** - No external libraries
|
||||
- **Symbol-based tokens** - Type-safe dependency injection
|
||||
- **Scoped lifetimes for use cases** - Per-request isolation
|
||||
Reference in New Issue
Block a user