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 })β indexstatus) - Sort by a field (
sort({ createdAt: -1 })β indexcreatedAt, whichtimestamps: truehandles) - Use text search β add a
textindex on searchable fields - Reference another model β index the
ObjectIdref field
Donβt index everything β only fields that appear in find(), sort(), or search queries.
βοΈ Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Model file | PascalCase | User.js, BlogPost.js |
| Model name | PascalCase | mongoose.model('BlogPost', ...) |
| Schema fields | camelCase | firstName, createdBy, isActive |
| Status enums | lowercase-hyphen or lowercase | 'active', 'in-progress' |
| Boolean flags | is or has prefix | isDeleted, hasPayment |
| Timestamps | Use timestamps: true | Always β 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: trueon the schema -
mongoose.models.ModelName || mongoose.model(...)guard (prevents hot-reload errors) - Indexes on queried/sorted/searched fields
-
required: trueon non-optional fields -
trim: trueon string fields that users type -
enumon status/type fields -
default: falseon boolean flags -
refpopulated with exact model name string