Backend

Models

Interface and class model patterns for MongoDB documents.

Models define the shape of MongoDB documents. There is no Mongoose — just TypeScript interfaces and classes.

Pattern: Interface + Class

Each model file exports:

  1. An interface (I prefix) — the full document shape with all optional fields.
  2. A type alias for create inputs (T prefix + Create suffix).
  3. A class implementing the interface — the constructor sets defaults, timestamps, and any derived fields.
// src/models/user.model.ts
import { ObjectId } from 'mongodb'
import bcrypt from 'bcrypt'
import { UserType, UserStatus } from '../enums/user.enum'
import type { TUserType, TUserStatus } from '../types/common.types'

export interface IUser {
  _id?:            ObjectId
  type?:           TUserType
  organizationId?: ObjectId | string
  firstName:       string
  lastName:        string
  email:           string
  phone:           string
  password?:       string
  refreshToken?:   string | null
  status?:         TUserStatus
  createdAt?:      Date
  updatedAt?:      Date
  deletedAt?:      Date | null
}

export type TUserCreate = Pick<
  IUser,
  'type' | 'organizationId' | 'firstName' | 'lastName' | 'email' | 'phone'
> & { password: string }

export class User implements IUser {
  _id?:            ObjectId
  type:            TUserType
  organizationId?: ObjectId
  firstName:       string
  lastName:        string
  email:           string
  phone:           string
  password?:       string
  refreshToken:    string | null
  status:          TUserStatus
  createdAt:       Date
  updatedAt:       Date
  deletedAt:       Date | null

  constructor(data: TUserCreate) {
    this.type           = data.type        ?? UserType.CUSTOMER
    this.organizationId = data.organizationId ? new ObjectId(data.organizationId) : undefined
    this.firstName      = data.firstName
    this.lastName       = data.lastName
    this.email          = data.email.toLowerCase().trim()
    this.phone          = data.phone
    this.password       = data.password    // service layer hashes before constructing
    this.refreshToken   = null
    this.status         = UserStatus.ACTIVE
    this.createdAt      = new Date()
    this.updatedAt      = new Date()
    this.deletedAt      = null
  }
}

Rules

  • Interfaces own the query/read shape — all fields optional where MongoDB might not include them.
  • Classes own the write shape — the constructor enforces defaults so the DB never gets partial documents.
  • Never skip deletedAt: null in the constructor — it enables the soft-delete filter { deletedAt: null } on all queries.
  • ObjectId conversion happens in the constructor (new ObjectId(id)), not in the service.
  • No methods on model classes other than the constructor. Behaviour lives in services.