Expiry Alerts
Food Tracker (React + Express)
Chapter 6 · Expiry Alerts
Every item added in Chapter 5 now has a real row, with a real (or null) expiry_date. This chapter turns that stored date into something actually useful: a list of what needs to be used soon.
The Alerts Route
status = 'active' excludes anything already marked used (Chapter 9's own territory); expiry_date IS NOT NULL excludes items with no expiry date at all — both conditions matter, since a used item retains its historical row but no longer has a live expiry date to alert on. date('now', '+3 days') is SQLite's own built-in date arithmetic, computing "three days from today" directly inside the query.
expiry_date is a TEXT column, and <= between two text values compares them lexicographically (character by character), not chronologically. This happens to give the correct chronological result here only because every date in this schema is consistently stored as YYYY-MM-DD — a format where lexicographic order and chronological order agree. Storing even one date in a different format (MM/DD/YYYY, for instance) would silently break every comparison in this route, with no error at all — just wrong results.
WHERE status = ..., ORDER BY expiry_date). SQLite has no such requirement: a query like this one runs correctly with no index declared at all, and at this app's own realistic scale (a single household's pantry, at most a few hundred rows), a full table scan on every request is genuinely fast enough that adding an index wouldn't even be noticeable. This isn't SQL being "better" in general — Firestore's index requirement exists for good reasons at Firestore's own intended scale — but at this specific app's own size, it's a real, fair point in this course's favor, named honestly rather than glossed over.
A Custom Hook for the Dashboard
refresh is exposed deliberately, not just called once internally — Chapter 9's own "mark used" action needs a way to trigger a fresh alerts fetch immediately afterward, since an item marked used should disappear from this list right away, not just on the next full page reload.
The Dashboard Component
'+3 days' is hardcoded directly in the SQL string above for clarity in this chapter, but a real app should pull that number from one shared constant (an environment variable, or a config file) rather than repeating the literal string anywhere the query might be duplicated later — the same "define it once" discipline this course already applied to the accent color and schema definitions.
Where This Course Is Headed
Item history and live search-as-you-type next — a debounced search built as a custom React hook, hitting a new Express search endpoint.
Hands-On Exercises
Explain why the alerts query's date comparison only works correctly because every date in this schema is stored in YYYY-MM-DD format, and what would happen if one date were stored in a different format.
📄 View solutionExplain why this course's alerts query needs no composite index while the equivalent Firestore query in Food Tracker (React + Firebase) does, and why this isn't simply "SQL is better than Firestore" in general.
📄 View solutionExplain why useExpiryAlerts exposes its own refresh function rather than only fetching once internally on mount.
📄 View solutionChapter 6 Quick Reference
- Route: GET /api/items/alerts — status='active' AND expiry_date IS NOT NULL AND expiry_date <= date('now', '+3 days')
- Real gotcha: SQLite text-date comparison only works because every date uses YYYY-MM-DD consistently
- Fair SQL advantage: no composite index needed at this app's realistic scale, unlike the Firebase sibling's equivalent query
- useExpiryAlerts: a custom hook exposing alerts, loading, and a callable refresh (needed by Chapter 9's own mark-used action)
- Next chapter: Item History & Live Search-as-You-Type