Files
trackgo-be/.agents/skills/nestjs-best-practices/rules/arch-use-repository-pattern.md
T
shancheas 0b0bdd9c4b Add new skills for backend patterns, coding standards, continuous learning, and NestJS best practices
- Introduced backend patterns skill with guidelines on API design, database optimization, and server-side best practices.
- Added coding standards skill outlining universal coding principles for TypeScript, NestJS, and Node.js development.
- Implemented continuous learning skill to automatically extract reusable patterns from Cursor sessions.
- Created NestJS best practices skill detailing architecture patterns, dependency injection, error handling, and security measures.
- Included various rules and templates for NestJS best practices to ensure production-ready applications.
2026-08-20 18:30:39 +07:00

98 lines
2.8 KiB
Markdown

---
title: Use Repository Pattern for Data Access
impact: HIGH
impactDescription: Decouples business logic from database
tags: architecture, repository, data-access
---
## Use Repository Pattern for Data Access
Create custom repositories to encapsulate complex queries and database logic. This keeps services focused on business logic, makes testing easier with mock repositories, and allows changing database implementations without affecting business code.
**Incorrect (complex queries in services):**
```typescript
// Complex queries in services
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User) private repo: Repository<User>,
) {}
async findActiveWithOrders(minOrders: number): Promise<User[]> {
// Complex query logic mixed with business logic
return this.repo
.createQueryBuilder('user')
.leftJoinAndSelect('user.orders', 'order')
.where('user.isActive = :active', { active: true })
.andWhere('user.deletedAt IS NULL')
.groupBy('user.id')
.having('COUNT(order.id) >= :min', { min: minOrders })
.orderBy('user.createdAt', 'DESC')
.getMany();
}
// Service becomes bloated with query logic
}
```
**Correct (custom repository with encapsulated queries):**
```typescript
// Custom repository with encapsulated queries
@Injectable()
export class UsersRepository {
constructor(
@InjectRepository(User) private repo: Repository<User>,
) {}
async findById(id: string): Promise<User | null> {
return this.repo.findOne({ where: { id } });
}
async findByEmail(email: string): Promise<User | null> {
return this.repo.findOne({ where: { email } });
}
async findActiveWithMinOrders(minOrders: number): Promise<User[]> {
return this.repo
.createQueryBuilder('user')
.leftJoinAndSelect('user.orders', 'order')
.where('user.isActive = :active', { active: true })
.andWhere('user.deletedAt IS NULL')
.groupBy('user.id')
.having('COUNT(order.id) >= :min', { min: minOrders })
.orderBy('user.createdAt', 'DESC')
.getMany();
}
async save(user: User): Promise<User> {
return this.repo.save(user);
}
}
// Clean service with business logic only
@Injectable()
export class UsersService {
constructor(private usersRepo: UsersRepository) {}
async getActiveUsersWithOrders(): Promise<User[]> {
return this.usersRepo.findActiveWithMinOrders(1);
}
async create(dto: CreateUserDto): Promise<User> {
const existing = await this.usersRepo.findByEmail(dto.email);
if (existing) {
throw new ConflictException('Email already registered');
}
const user = new User();
user.email = dto.email;
user.name = dto.name;
return this.usersRepo.save(user);
}
}
```
Reference: [Repository Pattern](https://martinfowler.com/eaaCatalog/repository.html)