Node / Express Guide
Repositories
The repository pattern — data access with MongoDB native driver 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
import { useCache, useAtlas, paginate } from '@codisolutions23/node-utils'
import type { Document, ClientSession } from 'mongodb'
import { ObjectId } from 'mongodb'
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)
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[]) {
const base = [{ $match: { _id: new ObjectId(id), deletedAt: null } }]
return col().aggregate<T>([...(pipeline ?? base)]).next()
}
async function update(id: string, patch: Partial<T>, session?: ClientSession) {
await col().updateOne(
{ _id: new ObjectId(id) } as any,
{ $set: { ...patch, 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/resource.repo.ts
import { useRepo } from './base.repo'
import { buildCacheKey } from '@codisolutions23/node-utils'
import { CollectionName } from '../enums/collection.enum'
import type { IResource, TResourceCreate } from '../models/resource.model'
import { Resource } from '../models/resource.model'
import { ObjectId } from 'mongodb'
import type { ClientSession } from 'mongodb'
export function useResourceRepo() {
const base = useRepo<IResource>(CollectionName.RESOURCES)
function createResource(value: TResourceCreate, session?: ClientSession) {
return base.add(new Resource(value), session)
}
function getByName(name: string, organizationId: string) {
return base.col().findOne({ name, organizationId: new ObjectId(organizationId), deletedAt: null })
}
function getResources({ organizationId, page = 1, limit = 10 }: {
organizationId: string
page?: number
limit?: number
}) {
const cacheKey = buildCacheKey(CollectionName.RESOURCES, { 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 updateResource(id: string, patch: Partial<IResource>, session?: ClientSession) {
return base.update(id, patch, session)
}
function deleteResource(id: string) {
return base.softDelete(id)
}
return { createResource, getByName, getById: base.getById, getResources, updateResource, deleteResource }
}
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. - Invalidate on write — the base repo calls
delCacheGroup(collectionName)onadd/update/softDelete. - Aggregate pipelines for complex queries — not
findOne+ in-memory filtering. - Filter
deletedAt: nullin every query pipeline to exclude soft-deleted documents.