Why You Should Use Repository Classes for Data Management
When you’re building software, keeping your code simple and easy to work with is super important. One way to do that is by using repository classes. These classes help you manage data and keep your code neat. Think of them as a middleman between your app and the data source, like a database or an API.
Let’s talk about why using repository classes is a great idea. We’ll use a ProductRepository example to show how they can help when managing product data in an online store. By the end, you’ll see how they make coding easier and more efficient.
What Happens Without a Repository?
Imagine you’re working on an online store. To get product info, you might write code like this:
ts// Directly fetching data in a service
async () => {
const product = await database.query(`SELECT * FROM products WHERE id = $1`, [productId])
const productList = await database.query(`SELECT * FROM products WHERE category_id = $1`, [categoryId])
}
This works fine for small projects, but it has problems:
- Repeated Code: You’ll find yourself writing the same database queries in different places.
- Hard to Change: If you switch to a new database, you’ll need to update your code everywhere.
- Difficult to Test: Testing this code means dealing with actual database queries.
- Messy Code: Your business logic and database code get all tangled up.
How Repository Classes Solve These Problems
Repository classes organize your data-related code in one place. They give you an easy way to interact with your data without worrying about the details.
Example: Creating a ProductRepository
Here’s how you can move the data logic into a repository:
tsclass ProductRepository {
private readonly database: Database
public constructor(database: Database) {
this.database = database
}
public async getProduct(productId: number): Product {
return await this.database.query(`SELECT * FROM products WHERE id = $1`, [productId])
}
public async getProductList(categoryId: number): Paginated<Product> {
return await this.database.query(`SELECT * FROM products WHERE category_id = $1`, [categoryId])
}
public async searchProducts(keyword: string): Paginated<Product> {
return await this.database.query(
`SELECT * FROM products WHERE name ILIKE $1`,
[`%${keyword}%`]
)
}
}
Using the Repository Class
tsconst database = new DatabaseClient() // Your database connection
const productRepository = new ProductRepository(database)
// Get a single product
const product = await productRepository.getProduct(1)
// Get products in a category
const products = await productRepository.getProductList(10)
// Search for products by name
const searchResults = await productRepository.searchProducts("laptop")
Why Use Repository Classes?
1. Keep Things Organized
By putting all product-related queries in ProductRepository
, you don’t have to worry about messy database details in other parts of your app. Plus, using techniques like dependency injection (where you pass the database client into the repository), your code becomes flexible and easier to test.
2. Easier Testing
You can mock the database when testing the repository:
tsconst mockDatabase = {
query: jest.fn()
}
const productRepository = new ProductRepository(mockDatabase)
mockDatabase.query.mockResolvedValue([{ id: 1, name: "Mock Product" }])
const products = await productRepository.getProductList(10)
expect(products).toEqual([{ id: 1, name: "Mock Product" }])
expect(mockDatabase.query).toHaveBeenCalledWith(
`SELECT * FROM products WHERE category_id = $1`,
[10]
)
3. Reuse Code
Once you create the repository, you can use it in different parts of your app, like services, controllers, or background jobs. This prevents duplicate code.
4. Easier Updates
If you change your database, you only need to update the repository. Everything else stays the same.
5. Focus on Business Logic
With the data-handling code in the repository, you can focus on solving business problems in your services and controllers.
Extra Features with Repositories
Repository classes can handle more than just databases. For example:
- APIs: Manage calls to other services.
- Files: Organize file uploads and downloads.
- Caching: Speed up frequently accessed data.
Example: Adding Caching
Here’s how you can extend ProductRepository
to include caching:
tsclass ProductRepository {
private readonly database: Database
private cache: Record<Property key, unknown>
public constructor(database: Database, cache: Record<Property key, unknown>) {
this.database = database
this.cache = cache
}
public async getProduct(productId: number) {
const cacheKey = `product:${productId}`
const cachedProduct = await this.cache.get(cacheKey)
if (cachedProduct) return JSON.parse(cachedProduct)
const product = await this.database.query(`SELECT * FROM products WHERE id = $1`, [productId])
await this.cache.set(cacheKey, JSON.stringify(product))
return product
}
}
Wrap-Up
Using repository classes makes your code cleaner, easier to test, and simpler to maintain. They help you keep your data logic separate, reducing duplicate code and making your app more flexible. Whether you’re building a small app or a big system, repositories are a smart choice.
Try using repository classes in your next project—you’ll thank yourself later!