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 (sets deletedAt) — never hard-delete unless absolutely required.
  • Cache reads — pass a cacheKey (via buildCacheKey) and ttl to getAll.
  • Invalidate on write — the base repo calls delCacheGroup(collectionName) on add/update/softDelete.
  • Aggregate pipelines for complex queries — not findOne + in-memory filtering.
  • Filter deletedAt: null in every query pipeline to exclude soft-deleted documents.