Enforce Clean Architecture in Cursor IDE in 12 Minutes

Create custom Cursor rules that automatically catch architecture violations and enforce dependency boundaries in your codebase.

Problem: Manual Architecture Reviews Are Failing

Your team agreed on clean architecture principles, but PRs still merge with domain logic in controllers, database calls in services, and circular dependencies everywhere.

You'll learn:

  • How to write Cursor rules that block bad imports
  • Enforce layer boundaries automatically
  • Catch violations before code review

Time: 12 min | Level: Intermediate

Why This Happens

Cursor's AI doesn't know your architecture rules by default. It suggests code based on patterns it sees, not your team's agreed boundaries.

Common symptoms:

  • Controllers importing repositories directly
  • Domain entities with framework dependencies
  • Utility functions scattered across layers
  • "It worked in my branch" architecture drift

Solution

Step 1: Create Your Rules File

# In your project root
touch .cursorrules

Expected: New hidden file appears in project root (same level as .git)

If it fails:

  • File not recognized: Update Cursor to 0.41+ (Settings → Check for Updates)

Step 2: Define Layer Boundaries

# .cursorrules

## Architecture Rules

This project follows Clean Architecture with strict layer boundaries:

### Layer Structure
- **domain/** - Business logic, entities, value objects
- **application/** - Use cases, DTOs, interfaces
- **infrastructure/** - Database, external APIs, frameworks
- **presentation/** - Controllers, routes, middleware

### Dependency Rules

CRITICAL: Enforce these import restrictions:

1. **domain/** CANNOT import from:
   - application/
   - infrastructure/
   - presentation/
   - Any framework (express, fastify, typeorm)
   
2. **application/** CANNOT import from:
   - infrastructure/
   - presentation/
   
3. **infrastructure/** can import domain/ and application/

4. **presentation/** can import application/ only

### Code Generation Rules

When generating code:

- New entities → Always create in domain/entities/
- New use cases → Always create in application/use-cases/
- Database queries → Only in infrastructure/repositories/
- API routes → Only in presentation/routes/

If I ask you to add database logic to a domain entity, remind me this violates clean architecture and suggest the correct layer.

Why this works: Cursor reads .cursorrules on every AI interaction and uses it as context for suggestions.

Step 3: Add Import Pattern Enforcement

# .cursorrules (continued)

### Forbidden Patterns

NEVER suggest code containing:

```[typescript](/typescript-ollama-integration-type-safe-development/)
// ❌ BAD: Domain importing infrastructure
// in domain/entities/User.ts
import { UserRepository } from '../../infrastructure/repositories';

// ❌ BAD: Controller with business logic
// in presentation/controllers/UserController.ts
class UserController {
  async create(req, res) {
    // Direct business logic here
    if (user.age < 18) throw new Error('Too young');
  }
}

// ❌ BAD: Use case with database calls
// in application/use-cases/CreateUser.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

Required Patterns

ALWAYS suggest code following these patterns:

// ✅ GOOD: Domain entities are pure
// domain/entities/User.ts
export class User {
  constructor(
    private readonly id: string,
    private readonly email: Email, // Value object
    private readonly age: number
  ) {
    if (age < 18) throw new DomainError('Invalid age');
  }
}

// ✅ GOOD: Use cases depend on abstractions
// application/use-cases/CreateUser.ts
export class CreateUserUseCase {
  constructor(
    private readonly userRepo: IUserRepository // Interface
  ) {}
  
  async execute(dto: CreateUserDTO): Promise<User> {
    const user = new User(dto.id, dto.email, dto.age);
    return this.userRepo.save(user);
  }
}

// ✅ GOOD: Controllers are thin
// presentation/controllers/UserController.ts
export class UserController {
  constructor(private readonly createUser: CreateUserUseCase) {}
  
  async create(req: Request, res: Response) {
    const result = await this.createUser.execute(req.body);
    res.json(result);
  }
}

Review Checklist

Before suggesting code, verify:

  • Imports respect layer boundaries
  • Business logic is in domain/
  • Use cases orchestrate, not implement
  • Infrastructure concerns are isolated
  • No framework code in domain/

If my request would violate these rules, explain why and suggest the correct approach.

Step 4: Test Your Rules

Open Cursor and try this prompt:

Create a User entity that saves itself to the database

Expected: Cursor should refuse and suggest:

I can't add database logic to a domain entity. Instead, I'll create:
1. A User entity in domain/entities/
2. An IUserRepository interface in application/interfaces/
3. A repository implementation in infrastructure/repositories/

If it fails:

  • Cursor ignores rules: Reload window (Cmd/Ctrl + Shift + P → "Reload Window")
  • Still generates bad code: Make rules more explicit with "NEVER" and "ALWAYS"

Step 5: Add Project-Specific Conventions

# .cursorrules (continued)

## Project Conventions

### File Naming
- Entities: `User.ts` (PascalCase)
- Use cases: `CreateUser.usecase.ts`
- Repositories: `UserRepository.ts`
- Controllers: `UserController.ts`

Folder Structure

src/
├── domain/
│   ├── entities/
│   ├── value-objects/
│   └── errors/
├── application/
│   ├── use-cases/
│   ├── interfaces/
│   └── dtos/
├── infrastructure/
│   ├── repositories/
│   ├── database/
│   └── external-services/
└── presentation/
    ├── controllers/
    ├── routes/
    └── middleware/

Dependency Injection

Always use constructor injection:

// ✅ Dependencies via constructor
class CreateUserUseCase {
  constructor(
    private readonly userRepo: IUserRepository,
    private readonly emailService: IEmailService
  ) {}
}

// ❌ Never import concrete implementations
import { PostgresUserRepository } from '../../infrastructure';

Error Handling

  • Domain errors: Extend DomainError class
  • Application errors: Extend ApplicationError
  • Infrastructure errors: Extend InfrastructureError

NEVER throw generic Error or use error codes.

Verification

Test with these prompts in Cursor:

# Test 1: Should refuse
"Add a database query to the User entity"

# Test 2: Should suggest correct layers
"Create a feature to update user email"

# Test 3: Should enforce naming
"Create a new repository for products"

You should see: Cursor suggests code in correct layers with proper naming conventions.

What You Learned

  • .cursorrules acts as a persistent system prompt for Cursor
  • Explicit "NEVER" and "ALWAYS" rules work better than suggestions
  • Show examples of both bad and good patterns
  • Rules apply to all AI interactions in the project

Limitations:

  • Rules don't block manual typing (use ESLint for that)
  • Large files may truncate rules from context
  • Team needs to commit .cursorrules to repo

Advanced: Testing Rules

Create a test file to validate Cursor follows your rules:

// .cursor-test-prompts.md

Test these prompts - Cursor should refuse or correct:

1. "Add Prisma imports to domain/entities/User.ts"
   Expected: Refuse, suggest repository pattern

2. "Put validation logic in the controller"
   Expected: Refuse, suggest domain entity or value object

3. "Import UserRepository directly in CreateUserUseCase"
   Expected: Suggest IUserRepository interface instead

4. "Create a helper function that calls the database"
   Expected: Ask which layer (should be infrastructure)

Save this in your repo and share with your team.

Troubleshooting

Cursor still suggests violations:

  • Make rules more explicit ("NEVER import express in domain/")
  • Add examples of exact bad patterns to avoid
  • Put most important rules at the top

Rules too strict:

  • Add "Exceptions:" section for valid special cases
  • Use "Generally avoid" instead of "NEVER" for soft rules

Team not following:

  • Add .cursorrules to PR template checklist
  • Run weekly audit: grep -r "import.*infrastructure" domain/

Tested on Cursor 0.41+, TypeScript 5.5, Node.js 22.x