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&lt;T&gt; and Option&lt;T&gt; 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&lt;T&gt; for single items
  • Support transactions via trx parameter

āœ… 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 TransactionManager via getInjection
  • Controller manages transaction lifecycle
  • UseCases receive optional trx?: Transaction parameter
  • 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 + UseCase suffix (CreateUserUseCase, UpdateUserEmailUseCase)
  • Repositories: I prefix for interface, implementation describes technology
    • Interface: IUserRepository
    • Implementation: DrizzleUserRepository, InMemoryUserRepository
  • DTOs: PascalCase + Dto suffix (CreateUserDto, UserResponseDto)
  • Events: PascalCase + Event suffix, past tense (UserCreatedEvent, OrderPlacedEvent)
  • Mappers: PascalCase + Mapper suffix (UserMapper, OrderMapper)

Quick Reference Checklist

Before submitting code, verify:

  • [ ] Does Domain layer have ZERO imports from outer layers?
  • [ ] Am I using Result&lt;T&gt; 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&lt;T&gt; 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&lt;T&gt; pattern for error handling
- Replace null with Option&lt;T&gt;
- 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&lt;T&gt; usage in domain/application
4. Verify Option&lt;T&gt; 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&lt;T&gt; 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:

  1. CLAUDE.md - Complete architecture documentation
  2. .cursorrules - Coding rules and patterns
  3. packages/ddd-kit/src/entity.ts - Entity base class
  4. packages/ddd-kit/src/value-object.ts - Value Object base class
  5. packages/ddd-kit/src/result.ts - Result pattern implementation
  6. packages/ddd-kit/src/option.ts - Option pattern implementation
  7. apps/nextjs/common/di/types.ts - DI container type definitions
  8. 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.