Building the Add-Item Flow

Food Tracker (React + Express)

Chapter 5 · Building the Add-Item Flow

Chapter 4's scanner hands off a barcode lookup result — or nothing at all, if the scan misses or the product isn't in Open Food Facts. Either way, this chapter is where that result actually becomes a row in the items table Chapter 2 defined.

The Form Component

Pre-filled where Chapter 3's lookup provided data, editable everywhere, and fully usable even with nothing pre-filled at all — a genuine manual-entry fallback, not an afterthought:

import { useState } from "react"; function AddItemForm({ initialData = {}, onSaved }) { const [name, setName] = useState(initialData.name || ""); const [category, setCategory] = useState(initialData.category || ""); const [expiryDate, setExpiryDate] = useState(""); const [barcode] = useState(initialData.barcode || null); const [error, setError] = useState(null); const handleSubmit = async (e) => { e.preventDefault(); setError(null); if (!name.trim()) { setError("Item name is required."); return; } const response = await fetch("/api/items", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, barcode, category, expiry_date: expiryDate || null }), }); if (!response.ok) { const data = await response.json(); setError(data.error || "Something went wrong."); return; } onSaved(await response.json()); }; return ( <form onSubmit={handleSubmit}> <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Item name" /> <input value={category} onChange={(e) => setCategory(e.target.value)} placeholder="Category" /> <input type="date" value={expiryDate} onChange={(e) => setExpiryDate(e.target.value)} /> {error && <p className="form-error">{error}</p>} <button type="submit">Add Item</button> </form> ); }

expiryDate || null matters here specifically: an empty string is not the same value as a missing date, and Chapter 2's schema expects NULL, not an empty string, whenever no expiry date applies.

The Client-Side Check, and Why It's Not Enough on Its Own

if (!name.trim()) above catches an empty name instantly, before any network request — real, useful UX, giving immediate feedback with no round-trip delay. But it's running entirely inside code the browser executes, which means it's also code a user (or a malicious script, or a stray curl command) can simply never run at all. Nothing about the client-side check stops a POST request built by hand from reaching the server with no name field whatsoever.

The Real Gate: Server-Side Validation

Chapter 3's POST route, updated to actually check what it receives before touching the database:

// routes/items.js router.post("/", (req, res) => { const { name, barcode, category, expiry_date } = req.body; if (!name || typeof name !== "string" || !name.trim()) { return res.status(400).json({ error: "name is required" }); } if (expiry_date && isNaN(Date.parse(expiry_date))) { return res.status(400).json({ error: "expiry_date is not a valid date" }); } const result = db.prepare( "INSERT INTO items (name, barcode, category, expiry_date) VALUES (?, ?, ?, ?)" ).run(name.trim(), barcode || null, category || null, expiry_date || null); res.status(201).json({ id: result.lastInsertRowid, name, barcode, category, expiry_date }); });
Why this app already has a real gate, by construction
Food Tracker (React + Firebase) had to introduce Security Rules specifically because its React client writes directly to Firestore — with no server code sitting in between by default, nothing would validate a write at all unless Security Rules were deliberately added later to fill that gap. This course never had that gap in the first place: every single write already passes through Chapter 2's own Express route, because that's simply how this architecture works. Server-side validation here isn't a bolted-on addition; it's the natural, unavoidable consequence of choosing "the client always talks to my own server" back in Chapter 1.
Never trust req.body
Anything arriving in req.body came from outside this process, regardless of which client sent it or how carefully that client's own form was built. A missing field, a wrong type, or a deliberately malformed request are all real possibilities the server must check for itself — the client-side check earlier in this chapter exists purely to make the honest, well-behaved case pleasant; it does no security work whatsoever.
The lookup-miss case is not an edge case
A meaningful share of real barcodes won't resolve to a name at all (Chapter 3's own honest note about Open Food Facts' inconsistent data) — initialData being {} the whole way through, with the user typing every field by hand, needs to be a genuinely first-class path through this form, not something only handled if there happens to be time for it.

Where This Course Is Headed

Expiry alerts next — an Express endpoint querying for items nearing their expiry date, paired with a React dashboard component.

Hands-On Exercises

Exercise 1

Explain why the client-side name check in AddItemForm provides no real security, even though it correctly prevents an empty name from being submitted through the form's own UI.

📄 View solution
Exercise 2

Explain the finding-box's own claim: why did Food Tracker (React + Firebase) need to add Security Rules specifically to get real write validation, while this course's Express route already provides it "by construction"?

📄 View solution
Exercise 3

Explain why expiryDate || null matters in the form's submit handler — what would go wrong if an empty string were sent to the server instead of null when no date is entered?

📄 View solution

Chapter 5 Quick Reference

  • AddItemForm: pre-filled from Chapter 4's scan result, fully usable with nothing pre-filled (the manual-entry fallback)
  • Client-side validation: real UX value (instant feedback), zero security value (trivially bypassable)
  • Server-side validation: the actual gate — checks name presence/type and expiry_date validity before touching the database
  • This course's own architectural advantage: every write already passes through Express by construction — no separate Security Rules layer needed, unlike the Firebase sibling
  • Gotcha: an empty string and a missing value are not the same thing — always normalize to null before hitting the database
  • Next chapter: Expiry Alerts