Understanding the Repository Pattern
The Repository pattern mediates between the domain and data mapping layers, acting like an in-memory domain object collection. It abstracts data access logic, providing a collection-like interface for accessing domain objects.
typescript
// Without Repository
class UserService {
async getUser(id: string) {
return await db.query('SELECT * FROM users WHERE id = ?', [id]);
}
}
// With Repository
class UserRepository {
findById(id: string): Promise<User> {
// Data access logic
}
}
class UserService {
constructor(private repo: UserRepository) {}
async getUser(id: string) {
return this.repo.findById(id);
}
}Benefits
Separation of Concerns
typescript
// Business logic doesn't know about database
class OrderService {
constructor(private orderRepo: OrderRepository) {}
async placeOrder(items: Item[]) {
const order = new Order(items);
await this.orderRepo.save(order);
}
}Testability
typescript
// Easy to mock for testing
const mockRepo = {
findById: jest.fn().mockResolvedValue(user)
};
const service = new UserService(mockRepo);Implementation
Generic Repository
typescript
interface Repository<T> {
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
create(entity: T): Promise<T>;
update(id: string, entity: T): Promise<T>;
delete(id: string): Promise<void>;
}
class SqlRepository<T> implements Repository<T> {
constructor(
private tableName: string,
private db: Database
) {}
async findById(id: string): Promise<T | null> {
const result = await this.db.query(
`SELECT * FROM ${this.tableName} WHERE id = ?`,
[id]
);
return result[0] || null;
}
async create(entity: T): Promise<T> {
await this.db.insert(this.tableName, entity);
return entity;
}
}Specific Repository
typescript
class UserRepository implements Repository<User> {
constructor(private db: Database) {}
async findByEmail(email: string): Promise<User | null> {
const result = await this.db.query(
'SELECT * FROM users WHERE email = ?',
[email]
);
return result[0] || null;
}
async findById(id: string): Promise<User | null> {
// Implementation
}
async create(user: User): Promise<User> {
const hashedPassword = await bcrypt.hash(user.password, 10);
await this.db.insert('users', {
...user,
password: hashedPassword
});
return user;
}
}Best Practices
- 1Keep repositories focused on data access
- 2Return domain objects, not database entities
- 3Use interfaces for flexibility
- 4Implement error handling
- 5Add transaction support
Conclusion
The Repository pattern separates data access from business logic, improving testability and maintainability. Use it when you have complex data access logic.
Build clean, maintainable applications with the Repository pattern!