Backend
Repositories
The repository pattern — data access with MongoDB and Redis caching.
Repositories are the only layer that touches MongoDB and Redis. Every collection has a dedicated repository built on a generic useRepo base.
Base Repository — useRepo
src/repositories/base.repo.ts provides generic CRUD operations with built-in Redis cache management:
// src/repositories/base.repo.ts (simplified)
import { useCache } from '@codisolutions23/node-utils'
import { useAtlas } from '@codisolutions23/node-utils'
import { paginate } from '@codisolutions23/node-utils'
export function useRepo<T>(collectionName: string) {
const { getDb } = useAtlas()
const { getCache, setCache, delCacheGroup } = useCache()
function col() {
return getDb().collection<T>(collectionName)
}
async function add(doc: T, session?: ClientSession) {
const result = await col().insertOne(doc as any, { session })
await delCacheGroup(collectionName) // invalidate on write
return { ...doc, _id: result.insertedId }
}
async function getAll(
pipeline: Document[],
page: number,
limit: number,
cacheOptions?: { key: string; ttl: number },
) {
if (cacheOptions) {
const cached = await getCache(cacheOptions.key)
if (cached) return cached
}
const [data, count] = await Promise.all([
col().aggregate([...pipeline, { $skip: (page - 1) * limit }, { $limit: limit }]).toArray(),
col().aggregate([...pipeline, { $count: 'total' }]).toArray(),
])
const result = paginate(data, count[0]?.total ?? 0, page, limit)
if (cacheOptions) await setCache(cacheOptions.key, result, cacheOptions.ttl)
return result
}
async function getById(id: string, pipeline?: Document[]) {
// ...
}
async function update(id: string, update: Partial<T>, session?: ClientSession) {
await col().updateOne(
{ _id: new ObjectId(id) } as any,
{ $set: { ...update, updatedAt: new Date() } },
{ session },
)
await delCacheGroup(collectionName)
return getById(id)
}
async function softDelete(id: string) {
return update(id, { deletedAt: new Date() } as any)
}
return { add, getAll, getById, update, softDelete, col }
}
Specific Repository
Each collection wraps the base repo with typed, domain-specific query methods:
// src/repositories/user.repo.ts
import { useRepo } from './base.repo'
import { buildCacheKey } from '@codisolutions23/node-utils'
import { CollectionName } from '../enums/collection.enum'
import type { IUser, TUserCreate } from '../models/user.model'
import { User } from '../models/user.model'
export function useUserRepo() {
const base = useRepo<IUser>(CollectionName.USERS)
function createUser(value: TUserCreate, session?: ClientSession) {
return base.add(new User(value), session)
}
function getUserByEmail(email: string, organizationId?: string) {
const pipeline = [
{ $match: { email, deletedAt: null, ...(organizationId ? { organizationId: new ObjectId(organizationId) } : {}) } },
]
return base.col().aggregate<IUser>(pipeline).next()
}
function getUsers({ organizationId, page = 1, limit = 10 }: { organizationId: string; page?: number; limit?: number }) {
const cacheKey = buildCacheKey(CollectionName.USERS, { organizationId, page, limit })
const pipeline = [
{ $match: { organizationId: new ObjectId(organizationId), deletedAt: null } },
{ $sort: { createdAt: -1 } },
]
return base.getAll(pipeline, page, limit, { key: cacheKey, ttl: 300 })
}
function updateUser(id: string, update: Partial<IUser>, session?: ClientSession) {
return base.update(id, update, session)
}
function deleteUser(id: string) {
return base.softDelete(id)
}
return { createUser, getUserByEmail, getUsers, updateUser, deleteUser }
}
Rules
- Never query MongoDB outside a repository. No
getDb()calls in controllers or services. - Always use
softDelete(setsdeletedAt) — never hard-delete unless absolutely required. - Cache reads — pass a
cacheKey(viabuildCacheKey) andttltogetAll/getById. - Invalidate on write — the base repo calls
delCacheGroup(collectionName)automatically onadd/update/softDelete. - Aggregate pipelines for complex queries — not
findOne+ in-memory filtering.