Building the Add-Item Flow
Food Tracker (React + Express + Prisma)
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 via Chapter 2's Item model. The form itself is identical in spirit to the sibling course's own; what genuinely changes is what happens once a submission reaches the server.
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:
{ name, barcode, category, expiryDate: expiryDate || null } — the exact same field name, expiryDate, all the way from the React state variable to the JSON key to the Prisma field it lands in. The sibling course's own version of this same line has to write expiry_date: expiryDate || null — translating its camelCase React state into the snake_case key its raw-SQL backend expects. That translation step is small, but it's a real extra thing to get right on every field, every time, and this course simply doesn't have it, purely as a side effect of Prisma's own camelCase convention matching JavaScript's own.
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:
Two differences from the sibling's own version of this route worth naming directly. First, the explicit Date.parse check still runs here — Prisma's own type system does add a real backstop underneath it (passing a genuinely malformed value into a DateTime field raises a real validation error from Prisma Client itself), but that error has no useful HTTP status or message attached to it by default, so the explicit check stays because it produces a clean 400 response the raw Prisma error wouldn't. Second, the response is just res.status(201).json(item) — the whole created row, returned automatically by create(), rather than a response object assembled field by field.
POST handler responds with { id: result.lastInsertRowid, name, barcode, category, expiry_date } — built by hand from the variables already in scope. Look at what's missing: status and added_at are both real columns on every item, and neither one makes it into that response, simply because nobody added them to the object literal. This course's res.status(201).json(item) can't have that specific bug — the whole row Prisma just created is what gets sent back, automatically, whether or not every field was explicitly remembered.
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.
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 — a Prisma query using gte/lte against the real DateTime field this course has had since Chapter 2, paired with a React dashboard component.
Hands-On Exercises
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 solutionUsing this chapter's own finding-box, explain exactly what's missing from the sibling course's manually-assembled POST response, and why this course's res.status(201).json(item) can't have the same bug.
📄 View solutionExplain why the explicit isNaN(Date.parse(expiryDate)) check still belongs in this route even though Prisma's own type system would reject a genuinely malformed date value on its own.
📄 View solutionChapter 5 Quick Reference
- AddItemForm: pre-filled from Chapter 4's scan result, fully usable with nothing pre-filled (the manual-entry fallback)
- No field-name translation:
expiryDateis the same name in React state, the JSON body, and the Prisma field — unlike the sibling's camelCase-to-snake_case step - Client-side validation: real UX value (instant feedback), zero security value (trivially bypassable)
- Server-side validation: the actual gate — an explicit check still needed for a clean 400, even with Prisma's own type system underneath it
- Real correctness win:
create()'s full-row response can't accidentally omit a field the way the sibling's hand-assembled response does (it's missing status and added_at) - This course's own architectural advantage, shared with the sibling: every write already passes through Express by construction — no separate Security Rules layer needed, unlike the Firebase sibling
- Next chapter: Expiry Alerts