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
DomainErrorclass - 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
.cursorrulesacts 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
.cursorrulesto 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
.cursorrulesto PR template checklist - Run weekly audit:
grep -r "import.*infrastructure" domain/
Tested on Cursor 0.41+, TypeScript 5.5, Node.js 22.x