Skip to content

F1 Technical HUD

The rapid-reference table for determining your database linkage strategy.

FactorEmbeddedReferenced
LocationInside Parent Doc (Subdocuments/Arrays)Independent Collection (Mapped via ObjectId)
Best ForMetadata that only makes sense inside its parent. Simple tags, specific project milestones.Generalized concepts used widely across the app. Categories, Authors, Taxonomy.
Speed⚡ Extremely Fast execution. Returns instantly with parent search.⏳ Moderate. Requires .populate() queries to append data.
ModificationsDifficult to normalize. Needs complex array modifications if changing data identically across all docs.Easy to normalize. Updates one item independently and it updates everywhere simultaneously.

Defining a Subdocument (Embedded Array)

// 1. Define the subdocument schema (turn off standalone _ids)
const commentSchema = new mongoose.Schema(
{
author: { type: String, required: true },
body: { type: String, required: true },
},
{ _id: false },
);
// 2. Embed it as an array in the parent schema
const postSchema = new mongoose.Schema({
title: String,
comments: [commentSchema], // Nests the subdocument
});

Defining an Object Reference (Relational)

const postSchema = new mongoose.Schema({
title: String,
// References the 'User' collection model via ObjectId
authorId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
});

Embedded arrays are just Javascript arrays until you run .save()!

// Add a new comment to a post
const post = await Post.findById(id);
post.comments.push({ author: "Solo", body: "Great post!" });
await post.save();

Overwriting an Array from a Form Submission If your form submits multiple text inputs with the same name (e.g., name="tags"), Express parses it as an array if there are multiple, or a string if there is only one. You must normalize this before saving:

// Helper function to safely parse tag inputs
const parseTags = (tagsInput) => {
if (!tagsInput) return [];
if (typeof tagsInput === "string") return [{ name: tagsInput }];
return tagsInput.map((tag) => ({ name: tag }));
};
// ... inside your route
post.tags = parseTags(req.body.tags);
await post.save();

Searching Inside an Embedded Array MongoDB automatically searches inside arrays of objects if you use dot notation on the property path.

// Find any post where AT LEAST ONE comment was authored by "Solo"
const posts = await Post.find({ "comments.author": "Solo" });

Populating Object References Use .populate() to replace the stored ObjectId string with the actual referenced document from the other collection.

// Find a post and populate its author details
const post = await Post.findById(id).populate("authorId");
// post.authorId is now a full nested object:
console.log(post.authorId.name); // Returns the User's name

Mongoose returns ObjectIds as complex objects, not strings. If you need to compare an ObjectId to a string (like a value conditionally selected from a dropdown), you must cast it to a string first!

{{/* EJS Template Condition comparing an ObjectId to a string variable */}} <%
if (post.authorId.toString() === currentUser.id) { %>
<p>You wrote this post!</p>
<% } %>

Before deleting a referenced document, ensure no other documents currently rely on it. This prevents orphaned data and application crashes when fetching lists!

// Secure deletion operation checking for relationships
async deleteCategoryById(id) {
// 1. Fetch the dependent model dynamically
const Project = require("mongoose").model("Project");
// 2. Count active connections
const refCount = await Project.countDocuments({ categoryId: id });
if (refCount > 0) return { success: false, error: "Active references exist." };
// 3. Delete safely
await Category.findByIdAndDelete(id);
return { success: true };
}

📘 Mongoose Relationships Mastery Guide (PDF)