Marking Items Used
Food Tracker (React + Express + Prisma)
Chapter 8 · Marking Items Used
Every earlier chapter built toward this exact moment: an item is finally used up, and the row created back in Chapter 5 needs to reflect that — without ever disappearing from the history Chapter 7 searches. The sibling course does this with one atomic UPDATE ... WHERE id = ? AND status = 'active'. Replicating that single-query guard through Prisma turns out to need a specific, real feature — verified directly against Prisma's own documentation and issue history rather than assumed.
The Route
where: { id: Number(id), status: "active" } is doing the same job as the sibling's own WHERE id = ? AND status = 'active' — a single, atomic query that only matches a row currently active and with the requested id, run as one statement rather than a check followed by a separate write.
update() normally only accepts unique fields (like id) in its where clause — a plain field like status can't be added to it on its own. Combining a non-unique field alongside the actual unique identifier, exactly as done here, is a real, specific Prisma capability: introduced as a preview feature (extendedWhereUnique) in Prisma 4.5.0, and made stable — no preview flag needed — as of Prisma 5.0.0. Without it, this guard would need updateMany() instead, which can filter on any field but doesn't return the updated record directly.
| Sibling course | This course | |
|---|---|---|
| The guard itself | WHERE id = ? AND status = 'active' in one UPDATE | where: { id, status: "active" } in one update() — same atomic single-query shape |
| Signal when the guard fails | result.changes === 0 — a value the route checks with an if | A thrown PrismaClientKnownRequestError with code P2025 — an error the route catches with try/catch |
| The id itself | id passed straight through as a string — SQLite's own flexible typing handles the comparison | Number(id) — Prisma's typed Int field expects a real number, not a numeric string |
That last row matters in practice, not just in theory: forgetting Number(id) here is a genuinely easy mistake, since req.params.id is always a string regardless of what the underlying field's own type is.
GET — a browser's own link-prefetching, a crawler following every link on a page, or simply a user middle-clicking to open in a new tab could all trigger a GET request without the user ever intending to mark anything used. PATCH requires the request to come from an explicit action — a button's onClick firing a real fetch call — never something a browser might do on its own while merely loading or navigating a page.
The React Action
refresh from useExpiryAlerts specifically for this moment, rather than only fetching once internally. Calling refresh() right after the PATCH succeeds re-runs the alerts query, and since the item's status is now "used", it no longer matches that query's own status: "active" filter — it disappears from the dashboard immediately, with no manual list-filtering logic on the frontend at all. The backend query is the single source of truth for "what's currently expiring soon"; the frontend just asks it again.
PATCH to actually succeed, then re-fetch — accepting a small, honest delay in exchange for never showing the user a state the server hasn't actually confirmed.
Where This Course Is Headed
Recipe lookup with TheMealDB next — an Express route and a React results component, matching items nearing expiry against real recipes.
Hands-On Exercises
Explain what specific Prisma feature makes where: { id, status: "active" } possible on update(), and which Prisma version made it stable without a preview flag.
📄 View solutionUsing this chapter's own compare-table, explain why this route needs a try/catch rather than an if (result.changes === 0) check, and what P2025 actually means.
📄 View solutionExplain why marking an item used must never be implemented as a plain GET request, with a concrete example of how a GET-based version could be triggered unintentionally.
📄 View solutionChapter 8 Quick Reference
- Route: PATCH /api/items/:id/use — update() with where: { id, status: "active" }, clearing expiryDate to null
- The real feature: non-unique fields alongside a unique id in update()'s where clause — stable since Prisma 5.0.0
- Different failure signal: a thrown P2025 error here, a returned changes: 0 in the sibling — same atomic guard, different way of reporting "no match"
- Easy-to-forget detail: Number(id) — req.params.id is always a string; the sibling's SQLite comparison tolerates that, Prisma's typed Int field doesn't
- Never a GET: a state-changing action must require an explicit request, not something a browser could trigger while merely loading a page
- The payoff: refresh() from Chapter 6 re-runs the alerts query, which naturally excludes the now-used item
- Next chapter: Recipe Lookup with TheMealDB