Building the Add-Item Flow with Direct Firestore Writes

Food Tracker (React + Firebase)

Chapter 5 · Building the Add-Item Flow with Direct Firestore Writes

Chapter 4 got a barcode; Chapter 3 turned it into a product name and category. This chapter finally saves the item — and along the way, hits a genuine fork this course's three siblings never face at all: should this write go straight from the browser to the database, or through a Cloud Function?

Two Ways to Write to Firestore

In FastAPI, Django, and Express, there's only ever one path: the client sends data to your server, and your server writes to the database. There's no other option, because there's no other code that's allowed to touch the database at all. In this course, the client can write to Firestore directly, with no server code involved — or it can call a Cloud Function that performs the write on the client's behalf. Both are real, valid options, and this app will end up using both, in different chapters, for different reasons.

Direct Client Writes: The Common Path

import { collection, addDoc, serverTimestamp } from "firebase/firestore"; import { db } from "./firebase"; async function addItem({ name, barcode, category, expiryDate }) { await addDoc(collection(db, "items"), { name, barcode, category, status: "active", expiryDate, // a Firestore Timestamp, built from the form's date input addedAt: serverTimestamp(), }); }

This is the Firestore-idiomatic default, not a shortcut taken to save effort: it skips a network round-trip through a Function, it's less code to maintain, and — most importantly — Chapter 6's Security Rules are what actually make this safe, not the absence of a server. A BaaS app doesn't need a server function standing between the client and the database just to "protect" an ordinary write.

When a Cloud Function Write Makes More Sense Instead

Writing through a Function is the better choice when:

  • The write needs elevated privilege the client itself should never be granted (bypassing Security Rules deliberately, from trusted server code).
  • A value must be computed or verified server-side, because it can't be trusted coming from the client at all.
  • The write needs to happen atomically alongside another side effect — sending a notification, writing an audit log entry — guaranteed to succeed or fail together.

None of that applies to adding a Pantry Item. There's no privileged operation, nothing here needs server-side verification, and there's no coupled side effect at write time. The direct-client-write path is the right choice for this specific flow — a conclusion worth stating plainly rather than leaving as an exercise for later.

The Add-Item Component

A scanned-and-looked-up item pre-fills name and category; a lookup failure (Chapter 3's not-found error) instead surfaces a plain manual-entry field for the name. Either path converges on the same form: confirm or edit the details, pick an expiry date, and save.

function AddItemForm({ scanned }) { const [name, setName] = useState(scanned?.name ?? ""); const [category, setCategory] = useState(scanned?.category ?? ""); const [expiryDate, setExpiryDate] = useState(null); const handleSubmit = async (e) => { e.preventDefault(); if (!name || !expiryDate) return; // basic client-side check, see below await addItem({ name, barcode: scanned?.barcode, category, expiryDate }); }; // ...form markup }

A live, type-ahead search across previously-added item names — so re-adding something bought before doesn't mean retyping it — is deliberately not built here. That's Chapter 7's own feature, once the item history exists to search against.

Where "the backend" actually is, for this feature
In every sibling course, adding an item means writing server-side code — a route handler, a view function — that owns the create-item logic. Here, for the common case, there is no backend code for it at all. The write happens directly from the browser to the database, and the only real gatekeeper is a set of declarative rules covered in the very next chapter. This is the most concrete, hands-on demonstration yet of what "no server you write and deploy yourself" (Chapter 1) actually means in practice.
Use serverTimestamp(), not new Date()
addedAt: serverTimestamp() asks Firestore's own server to fill in the timestamp at the moment it processes the write, rather than trusting whatever the client's local clock happens to say. A client's clock can be wrong, in a different timezone than expected, or — worst case — deliberately manipulated; a server-assigned timestamp removes that whole category of problem for data as important as "when was this actually added."
Client-side validation is a UX nicety here, not a security boundary
The if (!name || !expiryDate) return; check above only stops an honest user from submitting an incomplete form by accident — it does nothing to stop a modified client, a browser console, or a direct API call from writing whatever it wants straight to Firestore. Exactly like Chapter 2's schema-on-read and Chapter 3's "a Cloud Function isn't automatically secure," nothing in this chapter is actually enforced yet. Chapter 6's Security Rules are the one place that is.
This code will fail with a permission error until Chapter 6
Chapter 1 set up Firestore in production mode, which denies every read and write by default until Security Rules explicitly allow them. Running this chapter's own addDoc call right now, before Chapter 6 is written, will fail with a permission-denied error — expected behavior, not a sign this chapter's code is broken.

Where This Course Is Headed

Security Rules as the real gatekeeper for everything built in this chapter, expiry alerts, item history and search, marking items used, recipe lookup, Firebase Authentication, deployment, and a capstone.

Hands-On Exercises

Exercise 1

Explain the two ways to write to Firestore this chapter presents, and why it concludes the direct-client-write path is the right choice specifically for the add-item flow, rather than routing it through a Cloud Function.

📄 View solution
Exercise 2

Explain why the client-side check in AddItemForm isn't a security boundary, and name what this chapter says actually is.

📄 View solution
Exercise 3

Explain why running this chapter's own addDoc code before Chapter 6 exists will fail with a permission error, and why that's expected rather than a bug in this chapter's own code.

📄 View solution

Chapter 5 Quick Reference

  • Two write paths — direct client write, or through a Cloud Function — a fork that only exists in a BaaS architecture
  • This app's add-item flow — direct client write; no elevated privilege, server-side verification, or coupled side effect needed
  • serverTimestamp() — always for addedAt, never a client-generated new Date()
  • Client-side validation ≠ security — it's a UX nicety; Chapter 6's Security Rules are the real enforcement
  • Expect a permission error right now — production-mode Firestore denies everything until Chapter 6's rules allow it
  • Next chapter: Firestore Security Rules