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:

🔍 Debugging Upsert Issues

If you see the "conflict at 'fieldName'" error:

  1. Check for field duplication:

    console.log('$set fields:', Object.keys(setDoc));
    console.log('$setOnInsert fields:', Object.keys(setOnInsertDoc));
    // Look for overlapping fields
    
  2. Verify object spread:

    • Does the spread object contain createdAt?
    • Are you accidentally including timestamps in the base object?
  3. 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

🏷️ Tags

mongodb upsert timestamps best-practices anti-patterns