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.

Step 1 — Scanning a New Item

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.

Step 2 — Adding It

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.

Step 3 — A Few Days Later, an Alert

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.

Step 4 — Searching for Something Bought Before

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.

Step 5 — Marking the Milk Used

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.

Step 6 — Everything Updates, Without Being Told To Individually

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.

Step 7 — A Recipe Suggestion

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.

Step 8 — All of This, Actually Deployed

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

StepChapter(s) applied
1 — ScanningChapter 4 (camera scanning, unchanged), Chapter 3 (lookup route + BarcodeCache upsert)
2 — Adding the itemChapter 5 (AddItemForm, no field translation), Chapter 2 (schema.prisma, create())
3 — Expiry alertChapter 6 (typed DateTime query, display formatting)
4 — SearchChapter 7 (contains filter, the COLLATE NOCASE fix)
5 — Marking usedChapter 8 (update() with extended where, P2025 handling)
6 — Automatic updatesChapter 10 (ItemsContext, notifyChange, version — unchanged)
7 — Recipe suggestionChapter 9 (Promise.all, RecipeCache upsert, plain-String meals)
8 — Real deploymentChapter 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:

Where Prisma genuinely won
  • 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 (missing status, 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 DateTime field makes date-format consistency structural, not a matter of every developer remembering to format dates the same way.
Where Prisma genuinely cost something
  • 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 DateTime value 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 — contains defaults to case-sensitive on SQLite, unlike the sibling's automatically case-insensitive LIKE — 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 thrown P2025, not a returned count).
  • Chapter 11 — two real extra deployment steps, prisma generate and prisma migrate deploy, that the sibling's deployment never needed.
Where nothing changed at all
Chapter 1's own architecture, Chapter 4's camera scanner, Chapter 9's 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.
What this whole course was really about
Someone who already knows SQL well, deciding whether an ORM is worth learning, doesn't need to be told Prisma is better or worse — they need the specific, honest list above. Real wins (Chapter 3's race-condition fix, Chapter 6's structural type safety), real costs (Chapter 7's actual regression, Chapter 11's extra deploy steps), and a solid stretch of the app where the choice simply didn't matter. That specific, itemized answer — not a verdict — is this course's own real closing point.
Honest scope note
This capstone deliberately stops short of several things, matching the sibling course's own honest limits: the weekly meal planner named as future work back in Chapter 1 was never built; there's no offline/PWA support; no multi-user ownership model exists anywhere in this course — every item belongs to whoever can reach the server; Chapter 9's own honest limit on matching generic branded product names against TheMealDB's fixed vocabulary was never solved, only named; and no automated test suite or CI pipeline was covered anywhere in this course.

Hands-On Exercises

Exercise 1

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 solution
Exercise 2

Pick 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 solution
Exercise 3

Using 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 solution

Chapter 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