How Firestore Structures Data and How to Perform CRUD Operations With It — Opportunihub
Course Remote

How Firestore Structures Data and How to Perform CRUD Operations With It

Caleb Mintoumba · Remote

At a glance

Type
Course
Organisation
Caleb Mintoumba
Location
Remote
Work mode
Remote
Deadline
Rolling / not stated
Posted
1 Sep 2026

About this course

<p>Most apps eventually need to store and manipulate data. And if you're building with Firebase, that data lives in Firestore, Google's flexible, scalable NoSQL document database.</p> <p>But before you can confidently create, read, update, or delete data, you need to understand how Firestore actually organizes information. It doesn't look like a SQL database, and treating it like one is the fastest way to end up with a messy, hard-to-query data structure.</p> <p>In this tutorial, you'll learn how Firestore's NoSQL data model works, then build a small task management app to practice every CRUD operation with the Firebase Web SDK (v9+, modular). By the end, you'll be able to add tasks, query them, update nested fields and arrays, and delete data safely without leaving orphaned subcollections behind.</p> <h3 id="heading-table-of-contents">Table of Contents</h3> <ul> <li><p><a href="#heading-prerequisites">Prerequisites</a></p> </li> <li><p><a href="#heading-how-firestore-structures-data">How Firestore Structures Data</a></p> </li> <li><p><a href="#heading-step-1-set-up-your-firebase-project">Step 1 – Set Up Your Firebase Project</a></p> </li> <li><p><a href="#heading-step-2-initialize-the-sdk">Step 2 – Initialize the SDK</a></p> </li> <li><p><a href="#heading-step-3-create-adding-tasks">Step 3 – Create: Adding Tasks</a></p> </li> <li><p><a href="#heading-step-4-read-querying-tasks">Step 4 – Read: Querying Tasks</a></p> </li> <li><p><a href="#heading-step-5-update-modifying-tasks">Step 5 – Update: Modifying Tasks</a></p> </li> <li><p><a href="#heading-step-6-delete-removing-tasks">Step 6 – Delete: Removing Tasks</a></p> </li> <li><p><a href="#heading-debugging-common-issues">Debugging Common Issues</a></p> </li> <li><p><a href="#heading-conclusion">Conclusion</a></p> </li> </ul> <h3 id="heading-prerequisites">Prerequisites</h3> <p>Before you start, make sure you have the following:</p> <ul> <li><p><strong>Node.js v18 or later</strong> (<code>node --version</code>)</p> </li> <li><p>A <strong>Google account</strong> to create a Firebase project (the free Spark plan is enough for this tutorial)</p> </li> <li><p>Basic familiarity with JavaScript, including <code>async</code>/<code>await</code> and ES modules</p> </li> <li><p>A code editor and a terminal</p> </li> </ul> <p>You don't need prior experience with Firebase or NoSQL databases, as this guide builds that understanding from the ground up.</p> <h2 id="heading-how-firestore-structures-data">How Firestore Structures Data</h2> <p>If you're coming from a relational (SQL) background, the first thing to unlearn is the idea of tables with a fixed schema and foreign key joins. Firestore is a <strong>document-oriented NoSQL database</strong>, and it organizes data around two core concepts: <strong>collections</strong> and <strong>documents</strong>.</p> <ul> <li><p>A collection is a named bucket that holds documents. Think <code>tasks</code>, <code>users</code>, or <code>orders</code>.</p> </li> <li><p>A document is a single record inside a collection, identified by a unique ID. It stores data as key-value pairs, similar to a JSON object.</p> </li> </ul> <p>Here's the catch that trips up a lot of newcomers: <strong>documents don't need to share the same fields</strong>. One <code>task</code> document can have a <code>dueDate</code> field while another doesn't. Firestore doesn't enforce a schema at the database level, that responsibility shifts to your application code.</p> <h4 id="heading-nesting-and-subcollections">Nesting and subcollections</h4> <p>Documents can hold two kinds of nested data:</p> <ul> <li><p><strong>Maps</strong>, which are objects nested directly inside a document (for example, a <code>metadata</code> field containing <code>{ priority, dueDate }</code>)</p> </li> <li><p><strong>Subcollections</strong>, which are entire collections nested under a specific document (for example, every task can have its own <code>comments</code> subcollection)</p> </li> </ul> <p>This gives you a structure that looks like a tree:</p> <pre><code class="language-plaintext">tasks (collection) └── taskId (document) ├── title: "Article title" ├── completed: false ├── tags: ["writing", "firebase"] ├── metadata: { priority: "high", dueDate: &lt;timestamp&gt; } └── comments (subcollection) └── commentId (document) ├── text: "CRUD Article" └── createdAt: &lt;timestamp&gt; </code></pre> <img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/8c16db01-6335-4d00-92c0-8bbf392bd2e9.jpg" alt="A tree diagram illustrating Firestore's data hierarchy: a &quot;tasks&quot; collection contains a &quot;taskId&quot; document, which holds fields such as title, completed, tags, and a nested metadata map, alongside a &quot;comments&quot; subcollection containing individual comment documents with their own text and createdAt fields" style="display:block;margin:0 auto" width="600" height="400" loading="lazy"> <h4 id="heading-supported-data-types">Supported data types</h4> <p>Firestore documents can store several native types. The ones you'll use most often are:</p> <table> <thead> <tr> <th>Type</th> <th>Example</th> </tr> </thead> <tbody><tr> <td><code>string</code></td> <td><code>"Write CRUD article"</code></td> </tr> <tr> <td><code>number</code></td> <td><code>42</code></td> </tr> <tr> <td><code>boolean</code></td> <td><code>true</code></td> </tr> <tr> <td><code>array</code></td> <td><code>["writing", "firebase"]</code></td> </tr> <tr> <td><code>map</code></td> <td><code>{ priority: "high" }</code></td> </tr> <tr> <td><code>timestamp</code></td> <td><code>Timestamp.now()</code></td> </tr> <tr> <td><code>reference</code></td> <td>a pointer to another document</td> </tr> <tr> <td><code>geopoint</code></td> <td>a latitude/longitude pair</td> </tr> </tbody></table> <h4 id="heading-why-this-matters-before-writing-crud-code">Why this matters before writing CRUD code</h4> <p>Every CRUD operation you'll write later depends on this structure:</p> <ul> <li><p><strong>Create</strong> means adding a document to a collection, with an auto-generated or custom ID.</p> </li> <li><p><strong>Read</strong> means fetching either a single document by ID or a set of documents matching a query.</p> </li> <li><p><strong>Update</strong> means modifying fields on an existing document, including nested maps and arrays.</p> </li> <li><p><strong>Delete</strong> means removing a document, and Firestore will <em>not</em> automatically clean up its subcollections (a common gotcha you'll see in Step 6).</p> </li> </ul> <p>With the mental model in place, let's set up a project and start writing code.</p> <h3 id="heading-step-1-set-up-your-firebase-project">Step 1 – Set Up Your Firebase Project</h3> <p>Head to the <a href="https://console.firebase.google.com/">Firebase console</a> and create a new project.</p> <ol> <li><p>Click <strong>Add project</strong>, give it a name (for example: <code>crud-tasks-demo</code>), and follow the setup wizard (Google Analytics is optional for this tutorial).</p> </li> <li><p>Once the project is created, open the left sidebar and click <strong>Databases and Storage</strong> and then <strong>Firestore</strong>.</p> </li> <li><p>Click <strong>Create database</strong>. Choose a location close to you, and for this tutorial, start in <strong>test mode</strong> so you can read and write without configuring security rules yet.</p> </li> </ol> <p><strong>Note:</strong> Test mode leaves your database open to anyone for 30 days. Never ship an app to production without proper <a href="https://firebase.google.com/docs/firestore/security/get-started">Firestore security rules</a>, we'll touch on this in the Debugging section.</p> <p>You should now see an empty Firestore database, ready to receive your first collection.</p> <h3 id="heading-step-2-initialize-the-sdk">Step 2 – Initialize the SDK</h3> <p>Create a new project folder and install the Firebase Web SDK:</p> <pre><code class="language-shell">mkdir firestore-crud-demo &amp;&amp; cd firestore-crud-demo npm init -y npm install firebase </code></pre> <p>Grab your project's config object from <strong>Project settings - General - Your apps - Web app</strong> in the Firebase console (register a new web app if you haven't yet).</p> <p>Create a <code>firebase-config.js</code> file:</p> <pre><code class="language-javascript">// firebase-config.js import { initializeApp } from "firebase/app"; import { getFirestore } from "firebase/firestore"; const firebaseConfig = { apiKey: "YOUR_API_KEY", authDomain: "YOUR_PROJECT_ID.firebaseapp.com", projectId: "YOUR_PROJECT_ID", storageBucket: "YOUR_PROJECT_ID.appspot.com", messagingSenderId: "YOUR_SENDER_ID", appId: "YOUR_APP_ID", }; const app = initializeApp(firebaseConfig); export const db = getFirestore(app); </code></pre> <p>Every CRUD example from here on imports <code>db</code> from this file. Keep your actual config values out of version control (use environment variables in a real project).</p> <h3 id="heading-step-3-create-adding-tasks">Step 3 – Create: Adding Tasks</h3> <p>Firestore gives you two ways to create a document: let Firestore generate the ID, or set your own.</p> <h4 id="heading-auto-generated-id-with-adddoc">Auto-generated ID with <code>addDoc()</code></h4> <pre><code class="language-javascript">// create-task.js import { collection, addDoc, Timestamp } from "firebase/firestore"; import { db } from "./firebase-config.js"; async function createTask() { try { const docRef = await addDoc(collection(db, "tasks"), { title: "Write CRUD article", completed: false, tags: ["writing", "firebase"], metadata: { priority: "high", dueDate: Timestamp.fromDate(new Date("2026-09-15")), }, createdAt: Timestamp.now(), }); console.log("Task created with ID:", docRef.id); } catch (error) { console.error("Error creating task:", error); } } createTask(); </code></pre> <h4 id="heading-custom-id-with-setdoc"><strong>Custom ID with</strong> <code>setDoc()</code></h4> <p>Use this when you want to control the document ID yourself, for example, matching it to an ID from another system.</p> <pre><code class="language-javascript">import { doc, setDoc } from "firebase/firestore"; import { db } from "./firebase-config.js"; async function createTaskWithId(taskId) { await setDoc(doc(db, "tasks", taskId), { title: "Review pull request", completed: false, tags: ["code-review"], }); } createTaskWithId("task-001"); </code></pre> <h4 id="heading-adding-a-document-to-a-subcollection">Adding a document to a subcollection</h4> <p>To add a comment under a specific task, you reference the parent document first:</p> <pre><code class="language-javascript">import { collection, addDoc, Timestamp } from "firebase/firestore"; import { db } from "./firebase-config.js"; async function addComment(taskId, text) { await addDoc(collection(db, "tasks", taskId, "comments"), { text, createdAt: Timestamp.now(), }); } addComment("task-001", "First draft done"); </code></pre> <h3 id="heading-step-4-read-querying-tasks">Step 4 – Read: Querying Tasks</h3> <h4 id="heading-fetching-a-single-document">Fetching a single document</h4> <pre><code class="language-javascript">import { doc, getDoc } from "firebase/firestore"; import { db } from "./firebase-config.js"; async function getTask(taskId) { const snapshot = await getDoc(doc(db, "tasks", taskId)); if (snapshot.exists()) { console.log(snapshot.id, snapshot.data()); } else { console.log("No such task."); } } getTask("task-001"); </code></pre> <h4 id="heading-fetching-an-entire-collection">Fetching an entire collection</h4> <pre><code class="language-javascript">import { collection, getDocs } from "firebase/firestore"; import { db } from "./firebase-config.js"; async function getAllTasks() { const snapshot = await getDocs(collection(db, "tasks")); snapshot.forEach((doc) =&gt; { console.log(doc.id, doc.data()); }); } getAllTasks(); </code></pre> <h4 id="heading-filtering-with-queries">Filtering with queries</h4> <pre><code class="language-javascript">import { collection, query, where, orderBy, limit, getDocs } from "firebase/firestore"; import { db } from "./firebase-config.js"; async function getUrgentPendingTasks() { const q = query( collection(db, "tasks"), where("completed", "==", false), orderBy("metadata.priority"), limit(10) ); const snapshot = await getDocs(q); snapshot.forEach((doc) =&gt; console.log(doc.id, doc.data())); } getUrgentPendingTasks(); </code></pre> <p><strong>Heads up:</strong> combining <code>where()</code> on one field with <code>orderBy()</code> on another often requires a <strong>composite index</strong>. Firestore will throw an error in your console with a direct link to create it. More on this in Debugging.</p> <h4 id="heading-real-time-updates-with-onsnapshot">Real-time updates with <code>onSnapshot()</code></h4> <p>Instead of fetching once, you can subscribe to live changes. This is useful for a task list that updates instantly across devices:</p> <pre><code class="language-javascript">import { collection, onSnapshot } from "firebase/firestore"; import { db } from "./firebase-config.js"; const unsubscribe = onSnapshot(collection(db, "tasks"), (snapshot) =&gt; { snapshot.docChanges().forEach((change) =&gt; { console.log(change.type, change.doc.id, change.doc.data()); }); }); // Call unsubscribe() when you no longer need updates (e.g., component unmount) </code></pre> <h3 id="heading-step-5-update-modifying-tasks">Step 5 – Update: Modifying Tasks</h3> <h4 id="heading-partial-update-with-updatedoc">Partial update with <code>updateDoc()</code></h4> <p>Unlike <code>setDoc()</code>, <code>updateDoc()</code> only touches the fields you specify. Everything else on the document stays untouched.</p> <pre><code class="language-javascript">import { doc, updateDoc } from "firebase/firestore"; import { db } from "./firebase-config.js"; async function completeTask(taskId) { await updateDoc(doc(db, "tasks", taskId), { completed: true, }); } completeTask("task-001"); </code></pre> <h4 id="heading-updating-a-nested-field-with-dot-notation">Updating a nested field with dot notation</h4> <p>You don't need to rewrite the whole <code>metadata</code> map to change one property inside it:</p> <pre><code class="language-javascript">await updateDoc(doc(db, "tasks", "task-001"), { "metadata.priority": "low", }); </code></pre> <h4 id="heading-updating-arrays-safely">Updating arrays safely</h4> <p>Directly overwriting an array field is risky in concurrent scenarios. Use <code>arrayUnion()</code> and <code>arrayRemove()</code> instead:</p> <pre><code class="language-javascript">import { doc, updateDoc, arrayUnion, arrayRemove } from "firebase/firestore"; import { db } from "./firebase-config.js"; async function addTag(taskId, tag) { await updateDoc(doc(db, "tasks", taskId), { tags: arrayUnion(tag), }); } async function removeTag(taskId, tag) { await updateDoc(doc(db, "tasks", taskId), { tags: arrayRemove(tag), }); } </code></pre> <p><code>arrayUnion()</code> won't add a duplicate value, and <code>arrayRemove()</code> removes every matching instance. Both operate atomically on the server.</p> <h3 id="heading-step-6-delete-removing-tasks">Step 6 – Delete: Removing Tasks</h3> <h4 id="heading-deleting-a-document">Deleting a document</h4> <pre><code class="language-javascript">import { doc, deleteDoc } from "firebase/firestore"; import { db } from "./firebase-config.js"; async function deleteTask(taskId) { await deleteDoc(doc(db, "tasks", taskId)); } deleteTask("task-001"); </code></pre> <h4 id="heading-the-subcollection-trap">The subcollection trap</h4> <p>Here's the gotcha mentioned earlier: deleting <code>tasks/task-001</code> does <strong>not</strong> delete its <code>comments</code> subcollection. Those comment documents become orphaned, they still exist in your database. They're just unreachable through the UI unless you know the path.</p> <p>To clean up properly, delete the subcollection's documents first, then the parent:</p> <pre><code class="language-javascript">import { collection, getDocs, doc, deleteDoc, writeBatch } from "firebase/firestore"; import { db } from "./firebase-config.js"; async function deleteTaskWithComments(taskId) { const commentsRef = collection(db, "tasks", taskId, "comments"); const commentsSnapshot = await getDocs(commentsRef); const batch = writeBatch(db); commentsSnapshot.forEach((commentDoc) =&gt; { batch.delete(commentDoc.ref); }); batch.delete(doc(db, "tasks", taskId)); await batch.commit(); } deleteTaskWithComments("task-001"); </code></pre> <p><code>writeBatch()</code> groups multiple deletes into one atomic operation. Either all of them succeed, or none do.</p> <h4 id="heading-deleting-a-single-field">Deleting a single field</h4> <p>If you only want to remove one field without deleting the whole document, use <code>deleteField()</code>:</p> <pre><code class="language-javascript">import { doc, updateDoc, deleteField } from "firebase/firestore"; import { db } from "./firebase-config.js"; await updateDoc(doc(db, "tasks", "task-001"), { metadata: deleteField(), }); </code></pre> <h3 id="heading-debugging-common-issues">Debugging Common Issues</h3> <h4 id="heading-firebaseerror-missing-or-insufficient-permissions"><code>FirebaseError: Missing or insufficient permissions</code></h4> <p>Your security rules are blocking the request. If you're still in test mode, check whether your 30-day window expired (rules revert to deny-all after that). For a real app, review your rules in <strong>Firestore</strong> and then <strong>Rules</strong> and make sure they match the paths you're reading/writing, including subcollections, which need their own rule blocks.</p> <h4 id="heading-function-adddoc-called-with-invalid-data-unsupported-field-value-undefined"><code>Function addDoc() called with invalid data. Unsupported field value: undefined</code></h4> <p>Firestore rejects <code>undefined</code> values outright, unlike <code>null</code>, which is allowed. This usually happens when a form field is empty and you pass it straight into your write call. Filter out <code>undefined</code> fields before writing, or default them to <code>null</code>.</p> <h4 id="heading-the-query-requires-an-index"><code>The query requires an index</code></h4> <p>This shows up when you combine <code>where()</code> and <code>orderBy()</code> on different fields, as in the Step 4 example. Firestore can't serve that query with automatic indexes. The error message includes a direct link that pre-fills the composite index for you in the console, click it, wait a minute or two for the index to build, and rerun your query.</p> <h4 id="heading-reads-adding-up-fast-quota-warnings">Reads adding up fast / quota warnings</h4> <p>Every document returned by <code>getDocs()</code> counts as a read, even inside a loop calling <code>getDoc()</code> repeatedly. Avoid fetching a whole collection just to filter it client-side, push filtering into your query with <code>where()</code> instead, and use <code>limit()</code> on anything that could grow unbounded.</p> <h4 id="heading-orphaned-subcollections-after-delete">Orphaned subcollections after delete</h4> <p>If you notice documents you thought you deleted still consuming storage or showing up in exports, check for subcollections under the deleted document's path. As shown in Step 6, <code>deleteDoc()</code> never cascades, cleanup is always your responsibility.</p> <img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/6f5d1798-3798-4e05-9ceb-073d8857e15c.jpg" alt="A circular flow diagram showing the four CRUD operations as a continuous cycle, Create, Read, Update, and Delete, each labeled with its corresponding Firestore JavaScript functions (addDoc/setDoc, getDoc/getDocs/onSnapshot, updateDoc/arrayUnion, deleteDoc/writeBatch), illustrating how these operations connect in a typical data lifecycle." style="display:block;margin:0 auto" width="600" height="400" loading="lazy"> <h2 id="heading-conclusion">Conclusion</h2> <p>You now have a working mental model of Firestore's structure and hands-on experience with every CRUD operation using the Web SDK v9+. Here's a quick recap:</p> <table> <thead> <tr> <th>Operation</th> <th>Key functions</th> </tr> </thead> <tbody><tr> <td>Create</td> <td><code>addDoc()</code>, <code>setDoc()</code></td> </tr> <tr> <td>Read</td> <td><code>getDoc()</code>, <code>getDocs()</code>, <code>query()</code>, <code>onSnapshot()</code></td> </tr> <tr> <td>Update</td> <td><code>updateDoc()</code>, <code>arrayUnion()</code>, <code>arrayRemove()</code></td> </tr> <tr> <td>Delete</td> <td><code>deleteDoc()</code>, <code>deleteField()</code>, <code>writeBatch()</code></td> </tr> </tbody></table> <p>From here, there are a few natural next steps once you're comfortable with the basics:</p> <ul> <li><p><strong>Transactions</strong>, for reads and writes that must succeed or fail together (for example, transferring a task between two users)</p> </li> <li><p><strong>Batch writes</strong>, which you already saw in Step 6. They're useful anytime you need to touch multiple documents atomically</p> </li> <li><p><strong>Composite indexes</strong>, for more advanced filtering and sorting combinations</p> </li> <li><p><strong>Pagination</strong> with <code>startAfter()</code>, for loading large collections in chunks instead of all at once</p> </li> </ul> <p>If you haven't already, it's worth revisiting how to model your data <em>before</em> you write queries against it. Decisions made at the modeling stage (like whether to nest data or use a subcollection) directly shape which of these CRUD patterns will feel natural versus awkward later on.</p>

How to apply

  1. 1 Read the full details above and confirm you meet the eligibility criteria.
  2. 2 Prepare your documents — an updated CV, and any cover letter, proposal or certificates required.
  3. 3 Click Apply on official site to complete your application on Caleb Mintoumba’s official page.
  4. 4 Submit as early as possible — many close once filled.
Apply on official site

Sourced from freecodecamp. Always verify details on the official website. Opportunihub never charges you to apply.

Frequently asked questions

How do I apply for How Firestore Structures Data and How to Perform CRUD Operations With It?

Review the full details and eligibility on this page, prepare your documents, then use the “Apply on official site” button to complete your application on Caleb Mintoumba’s official page.

Is this opportunity remote or location-based?

This opportunity is remote-friendly and open to applicants who can work from anywhere.

Is How Firestore Structures Data and How to Perform CRUD Operations With It free to apply for?

Opportunihub lists this Course for free. Legitimate Courses do not ask for payment to apply — never pay a fee to submit an application.