AI-Assisted Development with Claude Code
Optimized for AI coding assistants: Claude Code, Cursor, and other AI tools
Why CleanStack is AI-Friendly
CleanStack is specifically designed to work seamlessly with AI coding assistants. Every architectural decision is documented explicitly, making it easy for AI to understand and maintain the codebase.
Key AI-Friendly Features
- CLAUDE.md - Complete codebase context in a single file
- .cursorrules - Architectural rules and coding patterns
- Explicit patterns - Result<T>, Option<T>, and other functional patterns
- Helper scripts - Generate boilerplate code instantly
- Rich documentation - DDD primitives are well-documented with examples
- Type safety - End-to-end TypeScript with strict mode
- Clear boundaries - Layer separation makes intentions obvious
Quick Start with AI Assistants
Step 1: Load Context
Start every session by asking your AI assistant to load the project context:
"Read CLAUDE.md and understand the architecture, focusing on:
1. The dependency rule (all dependencies point inward)
2. Result<T> and Option<T> patterns
3. Layer boundaries (Domain, Application, Adapters, Infrastructure)
4. Immutability requirements"
Step 2: Understand Patterns
Ask your AI to familiarize itself with the patterns:
"Read .cursorrules and @packages/ddd-kit to understand:
- How to create Value Objects
- How to create Entities and Aggregates
- How to write Use Cases
- How to implement Repositories"
Step 3: Start Coding
Now you can give high-level instructions:
"Create a complete user registration feature following Clean Architecture:
- Email value object with validation
- User entity
- CreateUser use case
- HTTP route handler
- Repository interface and implementation"
AI Development Guidelines
This section provides explicit patterns and rules for AI assistants working in this codebase. Following these guidelines ensures code quality and architectural consistency.
CRITICAL: Mandatory Architectural Rules
1. Respect the Dependency Rule
NEVER violate the dependency direction.
ā Allowed Dependencies:
Infrastructure ā Application (via ports/interfaces)
Adapters ā Application
Adapters ā Domain
Application ā Domain
Domain ā NOTHING (except @packages/ddd-kit)
ā FORBIDDEN Dependencies:
// WRONG: Domain importing from Application
import { SomeUseCase } from '@/application/...' // in domain layer
// WRONG: Application importing Infrastructure
import { DrizzleUserRepository } from '@/adapters/out/...' // in application
// WRONG: Domain importing external libraries
import { db } from '@packages/drizzle' // in domain layer
import { NextRequest } from 'next/server' // in domain layer
ā Correct Patterns:
// Domain: ONLY imports from ddd-kit
import { Entity, Result, UUID } from '@packages/ddd-kit'
// Application: imports Domain + defines ports
import { User } from '@/domain/user/User'
import type { IUserRepository } from '@/application/ports/IUserRepository'
// Infrastructure: imports Application ports, implements them
import type { IUserRepository } from '@/application/ports/IUserRepository'
export class DrizzleUserRepository implements IUserRepository { }
2. Error Handling Pattern (MANDATORY)
NEVER throw exceptions in Domain or Application layers. Use Result<T, E> pattern.
ā WRONG:
async execute(input: CreateUserInput): Promise<User> {
if (!input.email) {
throw new Error('Email is required') // ā NEVER throw
}
const user = await this.repo.create(...)
return user
}
ā CORRECT:
async execute(input: CreateUserInput): Promise<Result<User>> {
const emailOrError = Email.create(input.email)
if (emailOrError.isFailure) {
return Result.fail(emailOrError.error) // ā
Return Result
}
const userOrError = await this.repo.create(...)
if (userOrError.isFailure) {
return Result.fail(userOrError.error)
}
return Result.ok(userOrError.value)
}
Repository Pattern:
async findById(id: UUID): Promise<Result<Option<User>>> {
try {
const row = await db.select().where(eq(users.id, id.value))
if (!row) return Result.ok(None()) // ā
None, not null
const userOrError = UserMapper.toDomain(row)
if (userOrError.isFailure) return Result.fail(userOrError.error)
return Result.ok(Some(userOrError.value))
} catch (error) {
return Result.fail(new DatabaseOperationError(...)) // ā
Never throw
}
}
3. Immutability Pattern (MANDATORY)
All Domain objects MUST be immutable.
ā Value Objects:
export class Email extends ValueObject<{ value: string }> {
private constructor(props: { value: string }) {
super(props)
Object.freeze(this) // ā
REQUIRED
}
static create(email: string): Result<Email> {
if (!emailRegex.test(email)) {
return Result.fail('Invalid email format')
}
return Result.ok(new Email({ value: email }))
}
// ā NEVER add setters
// setValue(v: string) { this.props.value = v } // WRONG
}
ā Entities:
export class User extends Entity<UserProps> {
private constructor(props: UserProps, id?: UUID) {
super(props, id)
}
// ā
Return new instance for updates
updateEmail(email: Email): Result<User> {
const newProps = { ...this.props, email }
return Result.ok(new User(newProps, this.id))
}
// ā NEVER mutate
// setEmail(email: Email) { this.props.email = email } // WRONG
}
4. Use Cases Pattern (MANDATORY)
Every Use Case MUST:
- Implement
UseCase<Input, Output>interface - Return
Result<Output> - Be registered in DI container
- Have a single responsibility
ā Complete Use Case Example:
import { UseCase } from '@packages/ddd-kit'
import type { IUserRepository } from '@/application/ports/IUserRepository'
interface CreateUserInput {
email: string
name: string
}
export class CreateUserUseCase implements UseCase<CreateUserInput, User> {
constructor(
private readonly userRepo: IUserRepository
) {}
async execute(input: CreateUserInput): Promise<Result<User>> {
// 1. Validate input & create Value Objects
const emailOrError = Email.create(input.email)
if (emailOrError.isFailure) return Result.fail(emailOrError.error)
// 2. Check business rules
const existsOrError = await this.userRepo.findByEmail(input.email)
if (existsOrError.isFailure) return Result.fail(existsOrError.error)
if (existsOrError.value.isSome()) {
return Result.fail(new ConflictException('Email already exists'))
}
// 3. Create Domain Entity
const userOrError = User.create({
email: emailOrError.value,
name: input.name,
})
if (userOrError.isFailure) return Result.fail(userOrError.error)
// 4. Persist via Repository
const savedOrError = await this.userRepo.create(userOrError.value)
if (savedOrError.isFailure) return Result.fail(savedOrError.error)
// 5. Return Result
return Result.ok(savedOrError.value)
}
}
5. Repository Pattern (MANDATORY)
Repositories MUST:
- Be interfaces (ports) in Application layer
- Be implemented in Infrastructure/Adapters layer
- Extend
BaseRepository<T>from ddd-kit - Use
Option<T>for single items - Support transactions via
trxparameter
ā Repository Interface (Application layer):
// File: application/ports/IUserRepository.ts
import type { BaseRepository } from '@packages/ddd-kit'
import type { User } from '@/domain/user/User'
import type { Email } from '@/domain/user/Email'
export interface IUserRepository extends BaseRepository<User> {
findByEmail(email: string): Promise<Result<Option<User>>>
}
ā Repository Implementation (Infrastructure layer):
// File: adapters/out/persistence/DrizzleUserRepository.ts
import type { IUserRepository } from '@/application/ports/IUserRepository'
import type { Transaction } from 'drizzle-orm/node-postgres'
import { db } from '@packages/drizzle'
import { eq } from 'drizzle-orm'
import { users } from '@packages/drizzle/schema'
export class DrizzleUserRepository implements IUserRepository {
async findById(
id: UUID,
trx?: Transaction // ā
Always support transactions
): Promise<Result<Option<User>>> {
const database = trx ?? db
try {
const row = await database.query.users.findFirst({
where: eq(users.id, id.value)
})
if (!row) return Result.ok(None()) // ā
Use Option, not null
const userOrError = UserMapper.toDomain(row)
if (userOrError.isFailure) return Result.fail(userOrError.error)
return Result.ok(Some(userOrError.value))
} catch (error) {
return Result.fail(new DatabaseOperationError('Failed to find user'))
}
}
async create(user: User, trx?: Transaction): Promise<Result<User>> {
const database = trx ?? db
try {
const data = UserMapper.toPersistence(user)
const [row] = await database.insert(users).values(data).returning()
const userOrError = UserMapper.toDomain(row)
if (userOrError.isFailure) return Result.fail(userOrError.error)
return Result.ok(userOrError.value)
} catch (error) {
return Result.fail(new DatabaseOperationError('Failed to create user'))
}
}
// Implement other BaseRepository methods...
}
6. Transaction Pattern (MANDATORY for multi-step operations)
Transaction management happens at the Controller/Route Handler level, not in UseCases. This allows composing multiple UseCases in a single transaction.
Key Points:
- Controller retrieves
TransactionManagerviagetInjection - Controller manages transaction lifecycle
- UseCases receive optional
trx?: Transactionparameter - This enables chaining multiple UseCases in one transaction
ā CORRECT - Transaction managed in Controller:
// Route Handler / Controller
import { getInjection } from '@/common/di/container'
import type { Transaction } from 'drizzle-orm/node-postgres'
export async function POST(request: Request) {
const txManager = getInjection('ITransactionManagerService')
const createUserUseCase = getInjection('CreateUserUseCase')
const sendWelcomeEmailUseCase = getInjection('SendWelcomeEmailUseCase')
const body = await request.json()
// Controller manages the transaction
const result = await txManager.execute(async (trx) => {
// Execute multiple UseCases in same transaction
const userResult = await createUserUseCase.execute(body, trx)
if (userResult.isFailure) return Result.fail(userResult.error)
const emailResult = await sendWelcomeEmailUseCase.execute(
{ userId: userResult.value.id },
trx
)
if (emailResult.isFailure) return Result.fail(emailResult.error)
return Result.ok(userResult.value)
})
// Transaction auto-commits on success, auto-rolls back on failure
if (result.isFailure) {
return Response.json({ error: result.error }, { status: 400 })
}
return Response.json(result.value)
}
ā CORRECT - UseCase accepts optional transaction:
// Use Case
import type { Transaction } from 'drizzle-orm/node-postgres'
export class CreateUserUseCase implements UseCase<CreateUserInput, User> {
constructor(
private readonly userRepo: IUserRepository
) {}
// Transaction is passed as optional parameter
async execute(
input: CreateUserInput,
trx?: Transaction
): Promise<Result<User>> {
const emailOrError = Email.create(input.email)
if (emailOrError.isFailure) return Result.fail(emailOrError.error)
const userOrError = User.create({
email: emailOrError.value,
name: input.name,
})
if (userOrError.isFailure) return Result.fail(userOrError.error)
// Pass transaction to repository
const savedOrError = await this.userRepo.create(userOrError.value, trx)
if (savedOrError.isFailure) return Result.fail(savedOrError.error)
return Result.ok(savedOrError.value)
}
}
Why this pattern?
- ā Compose multiple UseCases in one transaction
- ā Controller has control over transaction boundaries
- ā UseCases remain focused on business logic
- ā Easy to test UseCases with or without transactions
- ā Flexible: can chain multiple UseCases in same transaction
7. Domain Events Pattern
ā Complete Domain Events Flow:
// 1. Define event in Domain layer
export class UserCreatedEvent {
constructor(
public readonly userId: UUID,
public readonly email: string,
public readonly occurredAt: Date = new Date()
) {}
}
// 2. Add event to Aggregate
export class User extends Aggregate<UserProps> {
static create(props: CreateUserProps): Result<User> {
const user = new User(props)
user.addEvent(new UserCreatedEvent(user.id, user.email.value)) // ā
return Result.ok(user)
}
}
// 3. Subscribe to events (in application layer)
DomainEvents.subscribe(
UserCreatedEvent,
async (event: UserCreatedEvent) => {
// Handle side effects
await emailService.sendWelcomeEmail(event.email)
}
)
// 4. Dispatch after persistence (in use case)
const userOrError = await this.userRepo.create(user)
if (userOrError.isSuccess) {
user.markEventsForDispatch()
DomainEvents.dispatch(user.id) // Fire events asynchronously
}
8. Dependency Injection Pattern (MANDATORY)
All dependencies MUST be injected via the DI container.
ā WRONG:
const repo = new DrizzleUserRepository()
const useCase = new CreateUserUseCase(repo)
ā CORRECT:
Step 1: Register in DI module (common/di/modules/user.module.ts):
import { ApplicationContainer } from '@evyweb/ioctopus'
import { DrizzleUserRepository } from '@/adapters/out/persistence/DrizzleUserRepository'
import { CreateUserUseCase } from '@/application/use-cases/CreateUserUseCase'
export const userModule = (container: ApplicationContainer) => {
container.bind('IUserRepository').toClass(DrizzleUserRepository)
container.bind('CreateUserUseCase').toClass(CreateUserUseCase)
}
Step 2: Retrieve from container:
import { getInjection } from '@/common/di/container'
// In route handlers or controllers
const useCase = getInjection('CreateUserUseCase') // ā
Type-safe
const result = await useCase.execute(input)
Code Organization Rules
File Structure Convention
apps/nextjs/src/
āāā domain/ # Core business logic
ā āāā user/
ā āāā User.ts # Entity/Aggregate
ā āāā Email.ts # Value Object
ā āāā UserName.ts # Value Object
ā āāā events/
ā āāā UserCreatedEvent.ts
āāā application/ # Use cases & ports
ā āāā use-cases/
ā ā āāā user/
ā ā āāā CreateUserUseCase.ts
ā āāā ports/ # Interfaces only
ā āāā IUserRepository.ts
āāā adapters/
ā āāā in/ # Input adapters
ā ā āāā api/
ā ā āāā users/
ā ā āāā route.ts # Next.js route handler
ā āāā out/ # Output adapters
ā āāā persistence/
ā āāā DrizzleUserRepository.ts
ā āāā mappers/
ā āāā UserMapper.ts # Domain ā DB mapping
āāā shared/ # Shared utilities
āāā errors/
Naming Conventions
- Entities: PascalCase, singular (
User,Order,Product) - Value Objects: PascalCase, descriptive (
Email,Money,Address,UserName) - Use Cases: PascalCase +
UseCasesuffix (CreateUserUseCase,UpdateUserEmailUseCase) - Repositories:
Iprefix for interface, implementation describes technology- Interface:
IUserRepository - Implementation:
DrizzleUserRepository,InMemoryUserRepository
- Interface:
- DTOs: PascalCase +
Dtosuffix (CreateUserDto,UserResponseDto) - Events: PascalCase +
Eventsuffix, past tense (UserCreatedEvent,OrderPlacedEvent) - Mappers: PascalCase +
Mappersuffix (UserMapper,OrderMapper)
Quick Reference Checklist
Before submitting code, verify:
- [ ] Does Domain layer have ZERO imports from outer layers?
- [ ] Am I using
Result<T>instead of throwing exceptions? - [ ] Are Value Objects and Entities immutable?
- [ ] Did I define repository interfaces in Application layer?
- [ ] Are repository implementations in Infrastructure/Adapters layer?
- [ ] Am I using
Option<T>instead of null/undefined? - [ ] Is my Use Case registered in the DI container?
- [ ] Do multi-step operations use
TransactionService? - [ ] Are Domain Events dispatched AFTER persistence?
- [ ] Did I write tests for Domain logic?
- [ ] Is input validated with Zod in the adapter layer?
- [ ] Are all dependencies injected via DI container?
Common AI Prompts
Creating Features
Prompt:
"Create a complete user registration feature following Clean Architecture:
1. Email value object with validation (regex pattern)
2. UserName value object with validation (min/max length)
3. User entity with business rules
4. CreateUser use case that:
- Validates email and name
- Checks if email already exists
- Creates user entity
- Persists via repository
5. IUserRepository interface in application/ports
6. DrizzleUserRepository implementation in adapters/out
7. UserMapper for domain ā database conversion
8. HTTP POST route handler in adapters/in/api/users/route.ts
9. Register everything in DI container
10. Write unit tests for Email and UserName value objects"
Adding Repository Methods
Prompt:
"Add a findByEmail method to IUserRepository:
- Define interface in application/ports/IUserRepository.ts
- Implement in DrizzleUserRepository
- Return Result<Option<User>>
- Support transactions via optional trx parameter
- Handle errors with DatabaseOperationError"
Refactoring to DDD
Prompt:
"Refactor this code to follow DDD patterns:
- Extract value objects from primitives
- Create entities with business logic
- Use Result<T> pattern for error handling
- Replace null with Option<T>
- Ensure immutability
- Follow dependency rule"
Validating Architecture
Prompt:
"Check if my code follows Clean Architecture principles:
1. Verify dependency directions
2. Check for domain purity (no framework imports)
3. Ensure Result<T> usage in domain/application
4. Verify Option<T> usage in repositories
5. Check immutability of domain objects
6. Validate DI container usage"
Writing Tests
Prompt:
"Write unit tests for the Email value object:
- Test valid email creation
- Test invalid email formats
- Test immutability
- Use Vitest
- Follow existing test patterns in packages/ddd-kit/__TESTS__/"
Best Practices for AI Development
1. Always Load Context First
Start every session with:
"Read CLAUDE.md to understand the architecture"
2. Reference Patterns Explicitly
When asking for code:
"Follow the Result<T> pattern as defined in @packages/ddd-kit"
"Use the same structure as in src/domain/examples/User.ts"
3. Request Validation
After generating code:
"Validate this code against Clean Architecture rules in CLAUDE.md"
"Check if I'm violating any dependency rules"
4. Ask for Complete Features
Instead of:
"Create a user entity" // Too vague
Ask for:
"Create a complete user registration feature with:
- Domain layer (entities, value objects)
- Application layer (use case, repository interface)
- Infrastructure layer (repository implementation)
- Adapter layer (HTTP route handler)
- DI registration
- Tests"
5. Iterate and Refine
"The Email value object looks good, but:
1. Add more validation rules (no spaces, max length)
2. Add a method to get the domain part
3. Ensure it's frozen with Object.freeze()
4. Add unit tests"
Helper Scripts
CleanStack includes scripts to generate boilerplate:
Create Use Case
./scripts/create-use-case.sh CreateUser
Generates:
- Use case file in
application/use-cases/ - Test file
- DI registration template
Create Value Object
./scripts/create-value-object.sh Email
Generates:
- Value object file in
domain/ - Test file
- Validation template
Create Entity
./scripts/create-entity.sh User
Generates:
- Entity file in
domain/ - Props interface
- Factory methods
- Test template
Key Files for AI Reference
Always keep these files in context:
- CLAUDE.md - Complete architecture documentation
- .cursorrules - Coding rules and patterns
- packages/ddd-kit/src/entity.ts - Entity base class
- packages/ddd-kit/src/value-object.ts - Value Object base class
- packages/ddd-kit/src/result.ts - Result pattern implementation
- packages/ddd-kit/src/option.ts - Option pattern implementation
- apps/nextjs/common/di/types.ts - DI container type definitions
- apps/nextjs/src/domain/examples/ - Example implementations
Common Mistakes to Avoid
ā Domain Importing Infrastructure
// WRONG - in domain layer
import { db } from '@packages/drizzle'
class User extends Entity<UserProps> {
async save() {
await db.insert(users).values(this.toObject())
}
}
ā Throwing Exceptions in Domain
// WRONG
class Email extends ValueObject<EmailProps> {
static create(email: string): Email {
if (!isValid(email)) {
throw new Error('Invalid email') // ā
}
return new Email({ value: email })
}
}
// CORRECT
class Email extends ValueObject<EmailProps> {
static create(email: string): Result<Email> {
if (!isValid(email)) {
return Result.fail('Invalid email') // ā
}
return Result.ok(new Email({ value: email }))
}
}
ā Returning Null Instead of Option
// WRONG
async findById(id: UUID): Promise<User | null> {
const user = await db.query.users.findFirst(...)
return user ?? null // ā
}
// CORRECT
async findById(id: UUID): Promise<Result<Option<User>>> {
try {
const user = await db.query.users.findFirst(...)
if (!user) return Result.ok(None()) // ā
return Result.ok(Some(UserMapper.toDomain(user)))
} catch (error) {
return Result.fail(new DatabaseOperationError(...))
}
}
ā Direct Instantiation Instead of DI
// WRONG
const repo = new DrizzleUserRepository()
const useCase = new CreateUserUseCase(repo)
// CORRECT
const useCase = getInjection('CreateUserUseCase')
Ready to build with AI assistance! Follow these guidelines and your AI assistant will generate clean, maintainable code that respects all architectural boundaries.