Database Structure Guide

🎯 Purpose

How to design and structure Mongoose models in our codebase β€” consistent field naming, relationships, indexing, and patterns we follow across all projects.

πŸ“ File Location

All models live in src/DB/models/. One file per model.

src/DB/models/
β”œβ”€β”€ User.js
β”œβ”€β”€ Role.js
β”œβ”€β”€ Item.js
└── ...

Always import models from @/src/DB/models/ModelName in API routes. Never define schemas inside API route files.

πŸ“¦ Model Structure

src/DB/models/Item.js
import mongoose from 'mongoose';
 
const itemSchema = new mongoose.Schema(
  {
    // Required fields first
    name: {
      type: String,
      required: true,
      trim: true,
    },
 
    // Optional fields
    description: {
      type: String,
      default: '',
    },
 
    // References use ObjectId + ref
    createdBy: {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'User',
      required: true,
    },
 
    // Status fields use enum
    status: {
      type: String,
      enum: ['active', 'inactive', 'archived'],
      default: 'active',
    },
 
    // Booleans
    isDeleted: {
      type: Boolean,
      default: false,
    },
  },
  {
    timestamps: true, // Adds createdAt + updatedAt automatically
  }
);
 
// Add indexes for fields you query or sort by often
itemSchema.index({ createdBy: 1 });
itemSchema.index({ status: 1 });
itemSchema.index({ name: 'text' }); // Text index for search
 
// Prevent re-compiling model on hot reload
const Item = mongoose.models.Item || mongoose.model('Item', itemSchema);
 
export default Item;

πŸ”— Relationships

One-to-Many (e.g. User has many Items)

Use ObjectId ref on the β€œmany” side. Populate on demand in API routes.

src/DB/models/Item.js
createdBy: {
  type: mongoose.Schema.Types.ObjectId,
  ref: 'User',
  required: true,
},
pages/api/v1/items.js
import MongoDBConnection from '@/src/DB/connection';
import Item from '@/src/DB/models/Item';
 
export default async function handler(req, res) {
  await MongoDBConnection.connectIfNot();
  
  const items = await Item.find({ status: 'active' })
    .populate('createdBy', 'name email') // Only fetch needed fields
    .sort({ createdAt: -1 });
 
  return res.status(200).json({ items });
}

Many-to-Many

Store the relationship as an array of ObjectIds on one or both sides, depending on query needs.

// On the "owner" model
members: [
  {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
  },
],

πŸ” Indexing Rules

Add indexes when you:

  • Filter by a field in queries (find({ status }) β†’ index status)
  • Sort by a field (sort({ createdAt: -1 }) β†’ index createdAt, which timestamps: true handles)
  • Use text search β†’ add a text index on searchable fields
  • Reference another model β†’ index the ObjectId ref field

Don’t index everything β€” only fields that appear in find(), sort(), or search queries.

✏️ Naming Conventions

TypeConventionExample
Model filePascalCaseUser.js, BlogPost.js
Model namePascalCasemongoose.model('BlogPost', ...)
Schema fieldscamelCasefirstName, createdBy, isActive
Status enumslowercase-hyphen or lowercase'active', 'in-progress'
Boolean flagsis or has prefixisDeleted, hasPayment
TimestampsUse timestamps: trueAlways β€” gives createdAt + updatedAt

🚫 Soft Delete

Use isDeleted: Boolean instead of actually removing documents, unless the data is truly disposable. Filter it out in queries:

const items = await Item.find({ isDeleted: { $ne: true } });

βœ… Checklist for New Models

  • File in src/DB/models/
  • timestamps: true on the schema
  • mongoose.models.ModelName || mongoose.model(...) guard (prevents hot-reload errors)
  • Indexes on queried/sorted/searched fields
  • required: true on non-optional fields
  • trim: true on string fields that users type
  • enum on status/type fields
  • default: false on boolean flags
  • ref populated with exact model name string