MongoDB Best Practices
MongoDB patterns, anti-patterns, and common pitfalls to avoid in the GoGrab Space backend
MongoDB Best Practices
This guide documents MongoDB patterns used in the GoGrab Space backend and critical anti-patterns to avoid.
🔴 Critical: Upsert Operations
The createdAt Conflict Problem
Issue Discovered: December 1, 2025
Root Cause: MongoDB updateOne with upsert: true fails when the same field appears in both $set and $setOnInsert.
Error Message:
Updating the path 'createdAt' would create a conflict at 'createdAt'
❌ Anti-Pattern: Field Conflict
// ❌ WRONG - This will FAIL!
const module = {
_id: docId,
name: dto.name,
createdAt: now, // ← Field included here
updatedAt: now,
};
await collection.updateOne(
{ _id: docId },
{
$set: {
...module, // ← Spreads createdAt into $set
updatedAt: now
},
$setOnInsert: {
createdAt: now // ← CONFLICT! Same field
}
},
{ upsert: true }
);
Why it fails: MongoDB doesn't allow the same field path in both $set and $setOnInsert operators.
✅ Correct Pattern: Separate Fields
// ✅ CORRECT - Fields are properly separated
const updateDoc = {
_id: docId,
name: dto.name,
// NO createdAt here!
updatedAt: now,
};
await collection.updateOne(
{ _id: docId },
{
$set: updateDoc, // Only update-able fields
$setOnInsert: {
createdAt: now // Only set when document is created
}
},
{ upsert: true }
);
// Build full object for return value
const fullModule = {
...updateDoc,
createdAt: now, // Add for return value only
};
Implementation Template
Use this template for all upsert operations:
async upsertDocument(id: string, data: CreateDto): Promise<Document> {
const now = new Date();
// 1. Build update document (WITHOUT createdAt)
const updateDoc = {
_id: id,
...data,
updatedAt: now,
// NO createdAt here!
};
// 2. Perform upsert with separated operators
await this.collection.updateOne(
{ _id: id },
{
$set: updateDoc,
$setOnInsert: {
createdAt: now // Only on insert
}
},
{ upsert: true }
);
// 3. Return full document for API response
return {
...updateDoc,
createdAt: now,
} as Document;
}
📋 Upsert Checklist
Before implementing any upsert operation, verify:
-
createdAtis NOT in the object being spread into$set -
createdAtis ONLY in$setOnInsert -
updatedAtis in$set(updated on every operation) - All timestamp fields are explicitly managed
- Return value includes all fields (including
createdAt)
🔍 Debugging Upsert Issues
If you see the "conflict at 'fieldName'" error:
Check for field duplication:
console.log('$set fields:', Object.keys(setDoc)); console.log('$setOnInsert fields:', Object.keys(setOnInsertDoc)); // Look for overlapping fieldsVerify object spread:
- Does the spread object contain
createdAt? - Are you accidentally including timestamps in the base object?
- Does the spread object contain
Test with minimal data:
// Minimal test case await collection.updateOne( { _id: 'test' }, { $set: { name: 'test', updatedAt: new Date() }, $setOnInsert: { createdAt: new Date() } }, { upsert: true } );
📚 Related Files
apis/backend-api/src/app/spaces/space-modules.service.ts- Reference implementationapis/backend-api/src/app/spaces/spaces.service.ts- Universe upsert patterns
🏷️ Tags
mongodb upsert timestamps best-practices anti-patterns