Capstone: A Complete, Working Food Tracker (Prisma Edition)
Food Tracker (React + Express + Prisma)
Chapter 12 · Capstone: A Complete, Working Food Tracker (Prisma Edition)
Marcus keeps his own household pantry tracked with the app this course just spent eleven chapters building — the same app, the same React-plus-Express architecture as its sibling, with Prisma running the data layer throughout. What follows is the same ordinary session the sibling course's own capstone walked through, this time through this course's own real code.
Marcus scans a carton of milk. Chapter 4's useBarcodeScanner hook decodes the barcode entirely client-side — genuinely unchanged, character for character, from both the Firebase and raw-SQL siblings — then handleDetected calls fetch("/api/lookup/..."). Chapter 3's route checks BarcodeCache first via findUnique() and falls back to a live Open Food Facts call, writing the result with upsert() — the fix for a real race condition the sibling's own plain INSERT doesn't have.
Chapter 5's AddItemForm pre-fills the name and category from that lookup. Marcus sets the expiry date and submits — the request body is { name, barcode, category, expiryDate }, the same field name end to end with no snake_case translation needed. Chapter 2's prisma.item.create() is the real validation gate, and it responds with the full created row — status and addedAt included, a real correctness detail the sibling's own hand-assembled response actually misses.
Chapter 6's alerts route surfaces the milk once it's within three days of its expiry date — expiryDate: { not: null, lte: threeDaysFromNow }, comparing a real typed DateTime rather than leaning on every date being formatted consistently by hand. The dashboard shows item.expiryDate.slice(0, 10) — the one small display-formatting cost of that same type safety.
Wanting to check if he's bought a particular brand of yogurt before, Marcus types "yog" into the search box. Chapter 7's useDebouncedSearch waits for him to stop typing, then hits prisma.item.findMany({ where: { name: { contains: q } } }) — matching case-insensitively only because Chapter 7's own migration added COLLATE NOCASE to the name column by hand. Without that fix, searching "yog" would never have matched an item actually named "Yogurt" — a real regression the sibling course never had to fix at all.
The milk gets finished. Chapter 8's PATCH /api/items/:id/use — never a plain link — calls prisma.item.update({ where: { id, status: "active" }, data: {...} }), the same atomic single-query guard as the sibling's own WHERE id = ? AND status = 'active', wrapped in try/catch to turn a thrown P2025 into a clean 404 if Marcus somehow clicks twice.
Marking the milk used calls Chapter 10's notifyChange() — one call, with no knowledge of who's listening, genuinely unchanged from the sibling course. The alerts dashboard disappears the milk from its own list; the recipe suggestions (Chapter 9) stop counting it as an expiring ingredient — both react on their own, entirely independently of whether the route they each call runs a raw SQL query or a Prisma one underneath.
With chicken and spinach both nearing expiry, Chapter 9's Promise.all fan-out queries TheMealDB for both concurrently — a real Node/JavaScript fact, identical in this course and its raw-SQL sibling, owing nothing to Prisma either way. RecipeCache reuses Chapter 3's own upsert() pattern for the same race-safety reason, storing each meal list as a plain JSON-encoded string rather than depending on Prisma's own inconsistent Json-type support for SQLite.
Marcus's session happens against a real deployment: Express serving the built React app via Chapter 11's own catch-all SPA route, pm2 keeping the process alive, nginx terminating HTTPS in front of it — all identical to the sibling. What isn't identical: npx prisma generate ran on the deploy server to rebuild the generated client, and npx prisma migrate deploy applied every migration this course's own eleven chapters accumulated, including Chapter 7's own hand-edited COLLATE NOCASE fix — with the underlying SQLite file's own persistence tracked through DATABASE_URL rather than a hardcoded path.
Chapter Attribution
| Step | Chapter(s) applied |
|---|---|
| 1 — Scanning | Chapter 4 (camera scanning, unchanged), Chapter 3 (lookup route + BarcodeCache upsert) |
| 2 — Adding the item | Chapter 5 (AddItemForm, no field translation), Chapter 2 (schema.prisma, create()) |
| 3 — Expiry alert | Chapter 6 (typed DateTime query, display formatting) |
| 4 — Search | Chapter 7 (contains filter, the COLLATE NOCASE fix) |
| 5 — Marking used | Chapter 8 (update() with extended where, P2025 handling) |
| 6 — Automatic updates | Chapter 10 (ItemsContext, notifyChange, version — unchanged) |
| 7 — Recipe suggestion | Chapter 9 (Promise.all, RecipeCache upsert, plain-String meals) |
| 8 — Real deployment | Chapter 11 (prisma generate, migrate deploy, DATABASE_URL) |
The Full Scorecard: SQL vs. Prisma, Chapter by Chapter
Chapter 1 opened this course with a specific promise: not "Prisma is better," but an honest account of what it actually costs and buys, chapter by chapter, against a sibling course doing the identical job in raw SQL. Eleven chapters later, here's the complete tally:
- Chapter 3 & 9 —
upsert()closes a real race condition the sibling's own check-then-insert pattern has, for both the barcode and recipe caches. - Chapter 5 —
create()'s full-row response structurally can't omit a field the way the sibling's hand-assembled response actually does (missingstatus,added_at). - Chapter 5 — matching field names end to end (no snake_case translation) is a small but real, permanent reduction in places a bug could hide.
- Chapter 6 — a typed
DateTimefield makes date-format consistency structural, not a matter of every developer remembering to format dates the same way.
- Chapter 2 — every schema change needs an explicit migration step; the sibling's raw SQL takes effect the moment the query runs.
- Chapter 6 — a
DateTimevalue serializes to a full ISO timestamp in JSON, needing.slice(0, 10)the sibling never needed. - Chapter 7 — the one real regression in this whole course —
containsdefaults to case-sensitive on SQLite, unlike the sibling's automatically case-insensitiveLIKE— fixed only by a hand-edited migration. - Chapter 8 — replicating the sibling's own atomic guard needed a specific, real Prisma feature (extended
where, stable since 5.0.0) and a different failure-handling shape (a thrownP2025, not a returned count). - Chapter 11 — two real extra deployment steps,
prisma generateandprisma migrate deploy, that the sibling's deployment never needed.
Promise.all concurrency, and Chapter 10's ItemsContext — four genuinely large pieces of this app that Prisma never touches, because they either predate the database layer entirely or live in a part of the stack (the browser, Node's own async model) Prisma has no reason to reach into.
Hands-On Exercises
Trace Step 6 in detail: explain exactly what markUsed calls, and how that one call results in both the alerts dashboard and the recipe suggestions updating, without either being called directly.
📄 View solutionPick any two steps from Marcus's session and explain how each one depends on at least two earlier chapters working together, not just one chapter in isolation.
📄 View solutionUsing this chapter's own scorecard, name one place Prisma genuinely improved on the sibling course and one place it genuinely cost something real — explaining, for each, what specifically made it a win or a cost rather than just a difference.
📄 View solutionChapter 12 Quick Reference — Course Complete
- 8 steps, 11 prior chapters — one continuous, realistic session with the finished, deployed app
- 4 real Prisma wins: upsert() race-safety (Ch.3, Ch.9), full-row create() responses (Ch.5), no field-name translation (Ch.5), structural DateTime consistency (Ch.6)
- 5 real Prisma costs: the migration step (Ch.2), display formatting (Ch.6), the case-sensitivity regression (Ch.7), extended-where research and different error handling (Ch.8), two extra deploy steps (Ch.11)
- 4 chapters unaffected either way: architecture (Ch.1), the camera scanner (Ch.4), Promise.all concurrency (Ch.9), Context-based state (Ch.10)
- This course's own closing point: a specific, itemized answer to "what does Prisma actually cost and buy," not a verdict
- Food Tracker (React + Express + Prisma) is now complete — 12/12 chapters