Food Tracker (FastAPI)
A Complete 12-Chapter Async-Native Course
Table of Contents
- Project Overview & FastAPI Project Setup
- Data Modeling with Pydantic & SQLAlchemy
- Barcode Lookup: Integrating Open Food Facts
- Camera-Based Barcode Scanning (Frontend)
- Building the Add-Item Flow
- Expiry Alerts
- Item History & Live Search-as-You-Type
- Marking Items Used
- Recipe Lookup with TheMealDB
- Async Patterns & Background Tasks
- Deployment
- Capstone: A Complete, Working Food Tracker
Project Overview & FastAPI Project Setup
Food Tracker (FastAPI)
Chapter 1 · Project Overview & FastAPI Project Setup
This is the fourth and final course in a deliberate quartet — Food Tracker (Django), Food Tracker (React + Express), and Food Tracker (React + Firebase) are its siblings, each building the exact same app in a genuinely different architecture. This course closes the set with the one variant that isn't paired with React at all: a plain, framework-free vanilla-JS frontend, keeping every chapter's attention on FastAPI and its backend itself.
What the App Actually Does
Before any architecture talk, the shared spec every course in the quartet builds toward:
- Scan a barcode with a phone or webcam camera, look it up against Open Food Facts (free, open, no API key) to fetch the product's name and details automatically.
- Record a use-by date for the item, and see it flagged once it's expiring soon.
- Keep a full history of every item ever added — some still active with a real expiry date, some already marked used with no expiry date at all — searchable in real time as you type, so re-adding something you've bought before is fast.
- Look up recipes via TheMealDB (also free, no key) that use ingredients close to expiring.
A weekly meal planner is explicitly out of scope for all four courses — named future work, not something any of them will build.
Why Vanilla JS, Not a Fourth React Course
Both React + Express and React + Firebase already exist in this quartet, each demonstrating React against a genuinely different backend. Pairing FastAPI with React a third time would mostly repeat frontend lessons this quartet has already taught twice over, diluting the actual point of this course: FastAPI's own backend design. A deliberately plain frontend — real HTML, real CSS, real fetch calls, no build step, no framework — keeps every chapter's focus exactly where it belongs.
Why FastAPI
Three concrete, genuine reasons, not just general popularity:
- Async-native from the ground up. Route handlers are written as
async defby default, with real, non-blocking I/O for API calls and database access — a different default than Django's own synchronous-first model. - Pydantic built in. Request and response shapes are declared as real Python classes with real type hints, validated automatically on every request — no separate serializer library to reach for.
- Automatic interactive API documentation. A fully working, browsable API explorer at
/docs, generated entirely from the route definitions and Pydantic models already written for validation — nothing extra to configure.
drf-spectacular to get interactive API docs at all. Food Tracker (React + Express) has nothing built in for this — a tool like Swagger UI would need to be wired up manually, describing every route by hand. Food Tracker (React + Firebase)'s own Cloud Functions have no request/response schema tooling whatsoever. FastAPI's own /docs page is genuinely automatic, generated directly from the same Pydantic models Chapter 2 writes for validation — one more course-specific "batteries included" moment, the same shape as Django's own free admin panel, just for a completely different battery.
Reusing an Already-Established Project Structure
This site's own Website Rebuild with FastAPI course already worked out a real, proven FastAPI project layout — this course reuses that convention rather than inventing a new one:
Installing and Running
A minimal main.py, confirming the setup works end to end before any real feature exists:
StaticFiles(..., html=True) serves index.html automatically for the root path and any unmatched path — this course's own equivalent of the SPA-fallback route Food Tracker (React + Express) had to hand-write, given for free by a single constructor argument here, since there's no client-side router involved at all in a plain multi-page vanilla-JS app.
SQLAlchemy (Chapter 2), a real database driver, and every other piece are choices this course makes and assembles deliberately, not defaults FastAPI provides out of the box. FastAPI's own "batteries included" story is narrower and more specific: async request handling, validation, and documentation — genuinely excellent at exactly those three things, and honestly minimal everywhere else.
uvicorn main:app --reload is running, http://localhost:8000/docs already shows a working, interactive explorer for the /api/health route above — a genuinely useful way to test every route this course builds, directly in the browser, without needing the vanilla-JS frontend at all.
Where This Course Is Headed
Data modeling with Pydantic and SQLAlchemy next — two genuinely separate layers, one for request/response validation and one for the database — then barcode lookup, camera scanning, the add-item flow, expiry alerts, item history and search, marking items used, recipe lookup, async patterns and background tasks, deployment, and a capstone tying every chapter into one complete, working app.
Hands-On Exercises
Explain why this course deliberately uses a plain vanilla-JS frontend instead of pairing FastAPI with React a third time in this quartet.
📄 View solutionExplain why FastAPI's automatic /docs page is described as a genuine feature none of the other three sibling courses get for free, naming what each sibling would need instead.
📄 View solutionExplain why StaticFiles(directory="static", html=True) is described as this course's own equivalent of Food Tracker (React + Express)'s own hand-written SPA-fallback route, and why main.py above still registers the /api/health route before mounting it — despite this course having no client-side router to worry about.
📄 View solutionChapter 1 Quick Reference
- The shared app — barcode scan (Open Food Facts) → expiry tracking → alerts → searchable history → recipe lookup (TheMealDB); no meal planner
- Frontend: deliberately plain vanilla JS — no framework, no build step, keeping focus on FastAPI itself
- Why FastAPI: async-native, Pydantic validation built in, automatic interactive docs at /docs
- Real advantage: /docs is genuinely automatic — none of the three sibling courses get this for free
- Honest limit: FastAPI itself ships no ORM, no admin, no database layer — assembled deliberately, not provided by default
- Project structure: reused from Website Rebuild with FastAPI's own established convention
- Next chapter: Data Modeling with Pydantic & SQLAlchemy
Data Modeling with Pydantic & SQLAlchemy
Food Tracker (FastAPI)
Chapter 2 · Data Modeling with Pydantic & SQLAlchemy
Every course in this quartet stores the same shape of data. This course expresses it as two genuinely separate models — one for what actually lives in the database, one for what a request is allowed to send.
The SQLAlchemy Model: Storage
The same design decisions every course in this quartet already made: expiry_date nullable, cleared rather than the row deleted once an item is used; added_at stamped by the database itself via server_default=func.now(), never trusted from client input.
The Pydantic Schemas: Request & Response Shape
ItemCreate deliberately has no id, no status, and no added_at field at all — not because the client sends them and they're ignored, but because they genuinely don't exist as fields the schema will accept. A request body containing a status field is simply rejected as an unexpected field, before any route handler code runs.
if (!name || typeof name !== "string" || !name.trim()) — inside the route handler's own body, and its own finding-box named that server-side check as the app's real gate. Here, the equivalent gate is the ItemCreate class itself: declaring item: ItemCreate as a route parameter means FastAPI validates the incoming JSON against that schema automatically, rejecting anything that doesn't match with a structured 422 response — the route handler's own code never even runs for a malformed request. Django's ModelForm sits somewhere between these two: one class handling both storage and form validation together, rather than SQLAlchemy and Pydantic's clean two-layer split.
Database Setup
get_db is written as a generator specifically so FastAPI's own dependency injection can use it — every route that needs a database session declares db: Session = Depends(get_db) and gets one automatically opened and closed around that single request, without writing that setup/teardown logic in every route by hand.
Item instance (the SQLAlchemy model) directly from a route declared to return ItemResponse only works because from_attributes = True tells Pydantic how to read attributes off a non-dict object. Without it, FastAPI would fail trying to serialize the SQLAlchemy object directly — a genuinely common early mistake, conflating "a Python object representing a database row" with "a Python object representing a validated API response," which look similar but are built and behave very differently.
expiryDate || null before sending a request, specifically to avoid an empty string reaching the database where a true NULL was expected. Optional[date] = None expresses the same "not required, genuinely absent" idea directly in the schema's own type — there's no separate normalization step to remember, because the type itself only ever admits a real date or nothing at all.
Where This Course Is Headed
Barcode lookup next — a FastAPI endpoint proxying Open Food Facts, reusing this chapter's own Pydantic/SQLAlchemy split for the lookup cache.
Hands-On Exercises
Explain why ItemCreate has no id, status, or added_at fields, and what happens to a request body that includes a status field anyway.
📄 View solutionExplain the difference between this chapter's validation approach and Food Tracker (React + Express)'s own hand-written validation checks, specifically in terms of whether route handler code ever runs for a malformed request.
📄 View solutionExplain what from_attributes = True actually does, and what would go wrong if a route tried to return a SQLAlchemy Item instance from a route declared to return ItemResponse without it.
📄 View solutionChapter 2 Quick Reference
- Two separate layers: Item (SQLAlchemy, storage) and ItemCreate/ItemResponse (Pydantic, request/response validation)
- ItemCreate: only fields a client may set — no id, status, or added_at, by design, not by convention
- Real advantage: a malformed request is rejected by Pydantic before route handler code ever runs, unlike the Express sibling's own hand-written checks
- from_attributes = True: lets a Pydantic schema read directly from a SQLAlchemy object — required, not automatic
- get_db(): a generator dependency FastAPI uses to open/close a database session per request
- Optional[date] = None: the type itself expresses "genuinely absent," no separate normalization step needed
- Next chapter: Barcode Lookup: Integrating Open Food Facts
Barcode Lookup: Integrating Open Food Facts
Food Tracker (FastAPI)
Chapter 3 · Barcode Lookup: Integrating Open Food Facts
Every course in this quartet integrates Open Food Facts through a server the client trusts, not a direct browser call. Here, that means a genuinely async FastAPI route — the first place this course's own "async-native" framing from Chapter 1 becomes concrete rather than theoretical.
Why Proxy Through the Server At All
Open Food Facts needs no API key — there's no secret to hide, so this isn't a security question. The real reasons, the same ones every sibling course already reasoned through:
- Caching. The same barcode gets scanned repeatedly — caching the result avoids re-querying for a product already looked up.
- Consistency. Every client gets identical lookup behavior, defined in one place.
- Future-proofing. If Open Food Facts' own API shape ever changes, only the server needs to change.
A Cache Model, Same Pattern as Chapter 2
barcode as the primary key here, exactly as reasoned in every sibling course: one product lookup per barcode, distinct from Item, where the same barcode can legitimately appear across many separate purchases.
A Genuinely Async Lookup Route
requests library — a genuinely reasonable choice there, since Django's own request-handling model is synchronous by default. This route uses httpx.AsyncClient specifically because it's await-able: while this one request is waiting on Open Food Facts to respond, FastAPI's event loop is free to keep handling other incoming requests on the same worker, rather than that worker sitting blocked and idle for the whole duration of the external call. This is the same non-blocking behavior Food Tracker (React + Express)'s own fetch-based route got for free from Node's single-threaded event loop — Chapter 1's own async-native claim, now doing real, measurable work rather than just describing FastAPI's own design philosophy.
product.get("product_name") and the defensive (... or [None])[0] above already expect that — data.get("status") == 1 only confirms a product record exists, not that it's complete. Chapter 5's own add-item flow has to handle a None name or category gracefully, the same honest limit every sibling course names in its own equivalent chapter.
raise HTTPException(status_code=404, detail="...") immediately stops the route and returns a proper JSON error response shaped like {"detail": "Product not found"} — no manually building and returning an error object the way Food Tracker (React + Express)'s own routes did with res.status(404).json({ error: ... }). FastAPI recognizes the exception type and handles the response formatting on its own.
Where This Course Is Headed
The camera-scanning frontend next — plain JavaScript, no framework, wiring a decoded barcode to this chapter's own lookup endpoint.
Hands-On Exercises
Explain what "the event loop is free to keep handling other requests" concretely means during the await client.get(...) call, and why a synchronous requests call in Django's own equivalent view doesn't offer the same benefit.
📄 View solutionExplain why barcode is the primary key in BarcodeCache but the same field is not a primary key in the Item table from Chapter 2.
📄 View solutionExplain the difference between the 404 raised for data.get("status") != 1 and the 502 raised for httpx.RequestError — what real-world situation does each one actually represent?
📄 View solutionChapter 3 Quick Reference
- Why proxy: caching, consistency, future-proofing — not secrecy, no API key involved
- BarcodeCache: keyed by barcode, same pattern as every sibling course's own cache table
- httpx.AsyncClient: a genuinely non-blocking call — the event loop stays free to serve other requests while this one waits
- Real contrast: Food Tracker (Django)'s own synchronous requests call blocks its worker for the same duration this route doesn't
- HTTPException: FastAPI's own structured way to return an error response, no manual res.status().json() equivalent needed
- Real gotcha: Open Food Facts data is crowdsourced and often incomplete — null fields are expected, not an error
- Next chapter: Camera-Based Barcode Scanning (Frontend)
Camera-Based Barcode Scanning (Frontend)
Food Tracker (FastAPI)
Chapter 4 · Camera-Based Barcode Scanning (Frontend)
This is the one chapter in this entire course with no FastAPI involvement at all — the same is true of every sibling course's own Chapter 4. What's genuinely different here is the frontend it's written against: plain JavaScript, no React, no hooks, no component lifecycle to lean on.
Requesting Camera Access
facingMode: "environment" requests the rear camera — the same call every sibling course's own scanning chapter starts with, since decoding a barcode client-side has nothing to do with which framework, or lack of one, sits around it.
Decoding, Written as Plain Functions
The same BarcodeDetector-with-ZXing-fallback logic every sibling course uses, here with no useEffect, no useRef — just a module-level variable holding the active stream, and two exported functions:
Wiring It Into the App's Views
This app has no client-side router — Chapter 1's static-file setup serves one index.html, and separate "views" are plain <div> sections shown and hidden with a CSS class, toggled by JavaScript:
useEffect's own return function runs whenever the component unmounts, with no way to forget it once it's written. Vanilla JS has no equivalent lifecycle concept at all: there's no "unmount" event when a plain <div> gets hidden by a CSS class. stopScanner() has to be called explicitly, by name, at every single place a user could leave the scan view — a successful scan, a cancel button, even a back-button handler if one existed. Miss even one path and the camera silently keeps running, with nothing in the language or the DOM to catch the mistake. This is the honest, direct cost of Chapter 1's own "deliberately minimal, no framework" choice: fewer moving parts, but every lifecycle guarantee React provides for free here becomes the developer's own manual responsibility instead.
stopScanner() in two places — once inside onDetected, once in the cancel button's handler — specifically because there are two distinct ways to leave the scan view. Adding a third way to leave later (a global nav menu, say) without also adding a third stopScanner() call would leave the camera running in the background, exactly the same underlying bug the React-based siblings' own cleanup functions exist to prevent automatically.
Where This Course Is Headed
The add-item flow next — combining this chapter's scan result with a manual-entry fallback, a POST endpoint, and Pydantic validation errors surfaced directly to the user.
Hands-On Exercises
Explain why this course's own cleanup discipline is described as a real cost of choosing no framework, contrasting it directly with how the three React-based sibling courses handle the same cleanup problem.
📄 View solutionExplain why showScanView's code calls stopScanner() in two separate places, and what would happen if a third way to leave the scan view were added without a matching third call.
📄 View solutionExplain what the if (!currentStream) return; check inside the scan function actually guards against, and why it's necessary given that requestAnimationFrame keeps calling scan() repeatedly.
📄 View solutionChapter 4 Quick Reference
- Same decoding logic as every sibling course: facingMode: "environment", BarcodeDetector with a ZXing fallback
- Written as plain functions: startScanner/stopScanner, a module-level variable instead of a React ref
- Real cost of no framework: cleanup is manual and explicit at every navigation path — no automatic useEffect equivalent
- Every exit path needs its own stopScanner() call — a successful scan, a cancel button, and any future navigation path added later
- The if (!currentStream) return guard: stops a stale scan loop from continuing after stopScanner() already ran
- Next chapter: Building the Add-Item Flow
Building the Add-Item Flow
Food Tracker (FastAPI)
Chapter 5 · Building the Add-Item Flow
Chapter 4's scan flow hands off a lookup result — or nothing at all, if the scan misses or the product isn't found. This chapter is where that result, or a manually typed one, actually becomes a row in the database.
A Trivially Short Route
if (!name || typeof name !== "string" || ...) — because that route's own job was both validating and saving. This route's job is only saving. By the time create_item's own body starts executing, item is already a fully validated ItemCreate instance — Chapter 2's own type declaration on the route parameter did that work before this function was ever called. item.model_dump() unpacks the already-clean fields directly into a new Item row. There's no defensive checking left to write, because there's nothing left to defend against by this point.
What Happens When Validation Fails
Submitting a request missing name never reaches the function above at all — FastAPI intercepts it and returns a structured 422 response on its own:
Compare this to Food Tracker (React + Express)'s own error shape — a single, free-form string: { "error": "name is required" }. FastAPI's detail array pinpoints exactly which field failed and why, in a consistent, machine-readable structure — genuinely useful for mapping an error directly onto the specific form input that caused it, rather than displaying one generic message regardless of which field was actually wrong.
Mapping Errors to Form Fields
err.loc is itself an array — ["body", "name"] — describing exactly where in the request the problem was found; taking its last element gives the specific field name, letting showFieldErrors place each message right next to the input that actually caused it.
required attribute on the name input gives instant, free client-side feedback for the obvious case — genuine UX, costing nothing to add. It is not, and doesn't need to be, a hand-written copy of the server's own validation logic the way Food Tracker (React + Express)'s own client-side check was: that course's if (!name.trim()) check duplicated real logic on both ends, while this course's client-side hint and Pydantic's own server-side schema never need to be kept in sync with each other at all, since neither one is a copy of the other's actual rules.
response_model=schemas.ItemResponse means FastAPI filters the outgoing response through that schema too — even if the Item SQLAlchemy object somehow carried an extra internal attribute never meant to be exposed, only the fields actually declared on ItemResponse would ever be serialized into the response. A genuine safety property, not just documentation sugar.
Where This Course Is Headed
Expiry alerts next — a date-filtered query for items expiring soon, and a dashboard endpoint.
Hands-On Exercises
Explain why create_item's own function body contains no validation checks at all, tracing exactly what already happened to the request before that function was ever called.
📄 View solutionExplain the concrete advantage of FastAPI's structured detail array over Food Tracker (React + Express)'s own single error string, specifically in terms of what showFieldErrors is able to do with it.
📄 View solutionExplain why this chapter says there's "no duplicated validation logic on either side," contrasting that directly with Food Tracker (React + Express)'s own client-side name check.
📄 View solutionChapter 5 Quick Reference
- The route: POST / — six lines, no validation code, because Chapter 2's ItemCreate already did that work
- 422 responses: a structured detail array naming exactly which field failed and why, unlike the Express sibling's single error string
- showFieldErrors: maps each detail entry's loc directly onto the form field that caused it
- No duplicated logic: HTML5 required is a free UX hint, not a hand-copied version of the server's own rules
- response_model: also filters the outgoing response — a genuine safety property, not just documentation sugar
- Next chapter: Expiry Alerts
Expiry Alerts
Food Tracker (FastAPI)
Chapter 6 · Expiry Alerts
Every item added in Chapter 5 now has a real row, with a real (or None) expiry_date. This chapter turns that stored date into a list of what needs to be used soon.
The Alerts Query
status == "active" excludes anything already marked used (Chapter 8's own territory); expiry_date.isnot(None) excludes items with no expiry date at all — the same two conditions every sibling course's own alerts query applies.
YYYY-MM-DD text — SQLite has no dedicated date type, so that course's own expiry_date <= date('now', '+3 days') was comparing strings lexicographically, and would have silently broken if even one date were ever stored in a different format. Chapter 2's decision to model expiry_date as a real SQLAlchemy Date column, not a raw string, removes that entire class of bug here: models.Item.expiry_date <= threshold compares real date values, correctly, regardless of how any particular database driver happens to store them internally. This is a genuine, direct payoff of choosing an ORM with real typed columns back in Chapter 2, not something this route had to work around on its own.
httpx.AsyncClient call, get_alerts is a plain def, not async def — this uses the classic, synchronous SQLAlchemy session pattern (Session, not AsyncSession). A local SQLite query is fast enough that this rarely matters in practice at this app's own realistic scale, but it's worth naming honestly rather than implying every route in this course is async by default: Chapter 3's route benefits from async because it waits on a genuinely slow external network call; this one queries a local file, where the same non-blocking benefit doesn't apply nearly as much, and a fully async SQLAlchemy setup (AsyncSession, an async database driver) was a deliberate scope decision this course doesn't take on.
The Dashboard View
date.today() + timedelta(days=3) runs once in application code before the query executes, rather than being computed inside the SQL statement the way Food Tracker (React + Express)'s own date('now', '+3 days') was. Both approaches are valid; this one keeps the "how many days is soon" logic visible directly in Python, right next to the route that uses it, rather than buried inside a SQL string.
Where This Course Is Headed
Item history and live search next — a ?q= prefix-search endpoint and debounced frontend fetching.
Hands-On Exercises
Explain the specific bug class this course's typed Date column avoids that Food Tracker (React + Express) had to name explicitly, tracing it back to a decision made in Chapter 2.
📄 View solutionExplain why get_alerts is a plain def rather than async def, contrasting it with Chapter 3's own async lookup route.
📄 View solutionExplain the difference between computing the expiry threshold in Python (date.today() + timedelta(days=3)) versus computing it inside the SQL query itself, as Food Tracker (React + Express) did.
📄 View solutionChapter 6 Quick Reference
- Route: GET /alerts — status='active', expiry_date not null, expiry_date <= today+3 days, ordered by expiry_date
- Real advantage: a typed Date column avoids the TEXT-comparison gotcha Food Tracker (React + Express) had to warn about explicitly
- Honest note: this route is a plain def, not async def — a local SQLite query doesn't benefit from async the way Chapter 3's external API call did
- Threshold computed in Python: date.today() + timedelta(days=3), kept visible next to the route, not buried in a SQL string
- Next chapter: Item History & Live Search-as-You-Type
Item History & Live Search-as-You-Type
Food Tracker (FastAPI)
Chapter 7 · Item History & Live Search-as-You-Type
Every item ever added stays in the items table forever — Chapter 2's own design, active or used. This chapter surfaces that whole history, searchable in real time as the user types.
The Search Route
Deliberately no status filter, exactly like every sibling course's own search route — this searches the entire history, active and used items both, since re-adding something bought before is exactly the case this endpoint exists for.
.ilike() is SQLAlchemy's own built-in, explicitly-named case-insensitive LIKE — no separate lowercase field to maintain, the same real advantage Food Tracker (Django)'s icontains lookup and Food Tracker (React + Express)'s own raw SQLite LIKE both already claimed for themselves. All three of this quartet's SQL-backed courses get native, case-insensitive substring search essentially for free, each expressed in that framework's own idiom — ilike() here, icontains in Django, a plain LIKE in Express. Only Food Tracker (React + Firebase) needed a workaround at all, maintaining its own nameLower shadow field specifically because Firestore has no equivalent native capability. This isn't really a FastAPI-specific advantage — it's a relational-database advantage the three SQL courses in this quartet all happen to share, each one naming it in its own equivalent chapter.
A Debounce, Written as Plain Functions
The same underlying idea as Food Tracker (React + Express)'s own useDebouncedSearch hook, here with no hook at all — just setTimeout/clearTimeout and a module-level variable:
clearTimeout(searchTimeoutId) at the start of onSearchInput cancels whatever timer the previous keystroke scheduled, before scheduling a new one — the same underlying discipline as Chapter 4's stopScanner() calls, applied here to a timer instead of a camera stream.
clearTimeout here would mean an extra, unnecessary search request firing occasionally — a wasted network call, nothing more. Chapter 4's own stopScanner() omission would leave a camera physically running in the background indefinitely. Both are the same underlying "clean up what the last run scheduled" pattern, but the actual cost of getting it wrong is meaningfully different — worth knowing which mistakes in a framework-free app are merely wasteful versus genuinely harmful.
Where This Course Is Headed
Marking items used next — a PATCH endpoint transitioning status and clearing the expiry date without deleting the row.
Hands-On Exercises
Explain why this chapter says the native case-insensitive substring search advantage isn't really FastAPI-specific, naming which three courses in the quartet share it and which one doesn't.
📄 View solutionExplain why clearTimeout(searchTimeoutId) is described as the same underlying pattern as Chapter 4's stopScanner() calls, and why the two are still described as having meaningfully different stakes if forgotten.
📄 View solutionExplain why the search route deliberately has no status filter, unlike Chapter 6's own alerts route.
📄 View solutionChapter 7 Quick Reference
- Route: GET /search?q=... — ilike('%'+q+'%'), no status filter, limited to 50 results
- Quartet-wide finding: Django's icontains, Express's LIKE, and this course's ilike() all share the same native advantage — only the Firebase sibling needed a nameLower workaround
- Debounce: plain setTimeout/clearTimeout, the same underlying pattern as Chapter 4's stopScanner(), but lower stakes if forgotten
- Honest limit: LIMIT 50 is a scope decision, not real pagination
- Next chapter: Marking Items Used
Marking Items Used
Food Tracker (FastAPI)
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 Route
expiry_date = None pays off Chapter 2's own nullable design — the row stays in the table forever, but its live expiry date genuinely goes away, exactly what Chapter 6's own expiry_date.isnot(None) filter already expects. The combined item_id == item_id, status == "active" filter means marking an already-used item "used" again matches no row at all — a safe no-op, not a re-stamped timestamp.
item_id: int isn't just documentation — declaring the path parameter's type means FastAPI validates and coerces it automatically, the same mechanism Chapter 5's own request-body validation used. A request to /api/items/abc/use is rejected with a 422 before mark_used's own body ever runs, since "abc" can't be parsed as an int — one more place this course's validation happens structurally, through a type annotation, rather than through a hand-written check.
GET — a browser's own link-prefetching or a crawler following every link on a page could trigger it without the user ever intending to. PATCH requires an explicit fetch call from real JavaScript, never something a browser might do on its own while simply loading a page.
Wiring It Into the Frontend
ItemsContext specifically because several separate React components each needed to react to the same mutation, with no direct relationship between them. This app has no component tree at all — markUsed can simply call loadAlerts() and loadRecipeSuggestions() directly, by name, because there are only ever a handful of views and no framework-imposed boundary between them. This isn't a missing feature; it's the direct, honest payoff of Chapter 1's own "deliberately minimal frontend" choice — a coordination problem only really needs a coordination mechanism once the app is complex enough to have one, and this one deliberately isn't.
Where This Course Is Headed
Recipe lookup with TheMealDB next — a second external API integration, reusing Chapter 3's own lessons.
Hands-On Exercises
Explain what the combined item_id == item_id, status == "active" filter actually prevents, and what would go wrong without the status == "active" part if a user managed to click "Mark Used" twice.
📄 View solutionExplain what happens to a request to /api/items/abc/use, and why this counts as the same kind of validation Chapter 5 covered for request bodies, just applied to a path parameter instead.
📄 View solutionExplain why this course never needed a coordination mechanism like Food Tracker (React + Express)'s own ItemsContext, tracing the reason back to a decision made in Chapter 1.
📄 View solutionChapter 8 Quick Reference
- Route: PATCH /{item_id}/use — sets status='used', used_at, clears expiry_date to None
- Guard: filtering on status == "active" too makes a duplicate click a safe no-op
- Path parameter validation: item_id: int is validated automatically, the same mechanism as Chapter 5's own body validation
- Never a GET: a state-changing action must require an explicit fetch call, not something a browser could trigger on its own
- No Context needed: markUsed calls loadAlerts()/loadRecipeSuggestions() directly — a direct payoff of this course's own deliberately minimal frontend
- Next chapter: Recipe Lookup with TheMealDB
Recipe Lookup with TheMealDB
Food Tracker (FastAPI)
Chapter 9 · Recipe Lookup with TheMealDB
Chapter 6's alerts query already knows what's expiring soon. This chapter takes that same list and asks a second free API, TheMealDB, what could actually be cooked with it — reusing every lesson Chapter 3 already taught about proxying an external API, plus one genuinely new gotcha this specific combination introduces.
A Recipe Cache, Same Pattern as Chapter 3
Real Concurrency via asyncio.gather()
Multiple ingredients need looking up at once — the same fan-out shape every sibling course faces. FastAPI's own async model makes genuine concurrency the natural way to write this, not a special optimization bolted on afterward:
Depends(get_db) — perfectly fine when only one query runs at a time. asyncio.gather() runs several lookup_ingredient calls concurrently, and SQLAlchemy's classic Session is explicitly not designed to be shared across concurrently-running tasks — interleaved queries and commits against one shared session can leave its internal state genuinely inconsistent. The fix above is deliberate: lookup_ingredient opens and closes its own dedicated SessionLocal() rather than reusing the route's own db parameter, giving each concurrent task a session entirely its own. This is a real, specific consequence of combining FastAPI's async concurrency with SQLAlchemy's synchronous session model — not a gotcha any of this quartet's other three courses have to think about in quite the same way.
asyncio.gather() here fires every ingredient's lookup concurrently instead, the exact same category of advantage Food Tracker (React + Express)'s own Promise.all already claimed for itself in that course's own Chapter 9 — two genuinely different languages, Python and JavaScript, each with a real async model, both reaching the same concurrent result for the same underlying reason: an async-first runtime turns out to matter for more than just how a single request feels to write.
ingredient means a cached "chicken" lookup gets reused the next time chicken appears in the alerts list, regardless of what else happened to be expiring alongside it that day — the same granular-caching principle as every sibling course's own recipe cache.
Where This Course Is Headed
Async patterns and background tasks next — a deeper look at async def, BackgroundTasks, and connection-pooling considerations, building directly on this chapter's own concurrency work.
Hands-On Exercises
Explain why sharing the route's own Depends(get_db) session across every concurrent lookup_ingredient call would be unsafe, and what lookup_ingredient does instead to avoid the problem.
📄 View solutionExplain why this course and Food Tracker (React + Express) both achieve real concurrent fan-out for the same underlying reason, despite being written in two completely different languages.
📄 View solutionExplain why recipe_cache is keyed by ingredient rather than by the full combination of expiring items on any given day.
📄 View solutionChapter 9 Quick Reference
- Route: GET /suggest — asyncio.gather() fans out to TheMealDB per expiring ingredient, merges and sorts by match count
- New table: recipe_cache, keyed by ingredient, storing the JSON meal list as text
- Real gotcha: a shared synchronous SQLAlchemy Session is unsafe across concurrent asyncio tasks — each lookup_ingredient call opens and closes its own dedicated session
- Real advantage: asyncio.gather() matches Food Tracker (React + Express)'s own Promise.all — two of four courses achieve genuine concurrency here, vs. Food Tracker (Django)'s own honestly-named sequential cost
- Next chapter: Async Patterns & Background Tasks
Async Patterns & Background Tasks
Food Tracker (FastAPI)
Chapter 10 · Async Patterns & Background Tasks
This course has already used async two different ways — a single external call in Chapter 3, a concurrent fan-out in Chapter 9. This chapter steps back to make the decision rule explicit, then adds one genuinely new tool: doing real work after a response has already been sent.
When async def Actually Helps
| Route | What it waits on | async def helps? |
|---|---|---|
| Chapter 3 — lookup_barcode | A network call to Open Food Facts | Yes — genuine idle waiting time, real benefit |
| Chapter 6 — get_alerts | A local SQLite query | Barely — the query finishes almost immediately |
| Chapter 9 — suggest_recipes | Several network calls, run concurrently | Yes — the whole point of asyncio.gather() |
The rule, stated plainly: async def earns its keep when a route genuinely waits on something slow and external — a network call, most often. A route that only touches a fast local resource gains little from being async, and Chapter 6 was written as a plain def specifically for that reason.
Doing Work After the Response Is Sent
Every mutation so far has made the client wait for absolutely everything to finish before responding. BackgroundTasks lets a route respond immediately and still do a little extra work afterward — here, logging every "marked used" event to its own table:
background_tasks.add_task(...) schedules log_usage to run after the response has already been sent — the client gets its confirmation immediately, without waiting for the audit-log write to complete.
log_usage opens its own SessionLocal() rather than reusing mark_used's own db parameter — the same pattern Chapter 9's lookup_ingredient used, but for a genuinely different underlying reason. Chapter 9's issue was concurrency: several tasks running at the same time couldn't safely share one session. Here, the issue is lifecycle: by the time log_usage actually runs, the request is already fully complete, and get_db's own finally: db.close() has already closed mark_used's session. A background task reusing that closed session would fail outright — it isn't a race condition to avoid, it's a session that's simply already gone.
log_usage runs in the same process, after the response — genuinely useful for quick, best-effort extra work, but with a real limit worth stating plainly: if the server process crashes or restarts in the narrow window between sending the response and the task actually executing, that task is simply lost, with no retry and no record that it was ever supposed to run. A real job queue (Celery, RQ, or similar) persists tasks somewhere durable and can retry them after a crash — BackgroundTasks offers none of that. It's the right tool for a nice-to-have audit log; it would be the wrong tool for something that genuinely must happen, like sending a payment confirmation.
A Note on Connection Pooling
check_same_thread=False matters specifically because of this chapter's own material: FastAPI runs synchronous functions like log_usage in a background threadpool, a different thread than the one that originally handled the request. SQLite's default behavior disallows using a connection from a different thread than the one that created it, purely as a safety guard — this flag deliberately relaxes that, since SessionLocal()'s own connection handling (not manual thread-sharing) is what's actually managing safe access here.
Where This Course Is Headed
Deployment next — environment config, running with Uvicorn/Gunicorn, and a real production checklist.
Hands-On Exercises
Using this chapter's own table, explain the general rule for when async def genuinely helps a route, and why Chapter 6's get_alerts was written as a plain def despite this course being "async-native."
📄 View solutionExplain why log_usage needs its own SessionLocal() rather than reusing mark_used's own db parameter, and how this reason genuinely differs from why lookup_ingredient needed its own session in Chapter 9.
📄 View solutionExplain what "BackgroundTasks is not a durable job queue" actually means in practice, describing a concrete scenario where a scheduled background task could be silently lost.
📄 View solutionChapter 10 Quick Reference
- The rule: async def helps when a route waits on something slow and external — not every route benefits equally
- BackgroundTasks: background_tasks.add_task(...) runs work after the response is already sent
- log_usage's own session: a fresh SessionLocal(), because the request's own session is already closed by the time this runs — a lifecycle issue, distinct from Chapter 9's own concurrency issue
- Real limit: BackgroundTasks is in-process and best-effort — a crash before it runs loses it silently, with no retry
- check_same_thread=False: needed because sync background tasks run in a different thread than the request that scheduled them
- SQLite has no real connection pool — that concept matters for a networked database like PostgreSQL, not a local file
- Next chapter: Deployment
Deployment
Food Tracker (FastAPI)
Chapter 11 · Deployment
Every earlier chapter ran with uvicorn main:app --reload — a genuinely fine development server, and a genuinely wrong choice for real production traffic.
Running With Multiple Workers
Gunicorn manages several separate Uvicorn worker processes — a standard production pattern for FastAPI, giving real request-handling capacity beyond what a single process can provide, and automatically restarting a worker that crashes.
StaticFiles — the frontend and API have always shared one origin, in development and in production alike. There's no CORS middleware anywhere in this course, not because it was overlooked, but because Chapter 1's own architecture never created the cross-origin situation that would require it.
A Full-Circle Caveat on Chapter 1's Own Favorite Feature
Chapter 1 named automatic interactive docs at /docs as a genuine advantage none of this quartet's other courses get for free. Before a real deployment, that same feature deserves an honest second look:
/docs lets anyone browsing it try real API calls directly against the running app — fine, even valuable, during development; worth a deliberate decision, not an accidental default, before a real public launch. Disabling it conditionally, as above, keeps Chapter 1's own advantage intact during development while making its production exposure an explicit choice rather than something that just happens by default.
SQLite in Production: A Familiar Concern, and a New One
Food Tracker (React + Express)'s own deployment chapter named the risk of an ephemeral filesystem silently wiping the SQLite file on redeploy — the same risk applies here, for the same reason: confirm the deployment target keeps foodtracker.db on a volume that survives a restart.
This course introduces one genuinely new SQLite consideration the single-process Express deployment never had to face:
-w 4 above means four entirely separate operating-system processes, each with its own database connection to the same foodtracker.db file. SQLite handles multiple processes reasonably well for read-heavy workloads, but under genuine concurrent write contention across several processes at once, it can return real "database is locked" errors — a cost this course's own move to multiple worker processes trades away some of SQLite's single-process simplicity for. Chapter 10 already named that SQLite has no real connection pool the way a networked database does; this is the concrete, production-shaped consequence of that same limitation. At this app's own realistic single-household scale, simultaneous writes are genuinely rare, so a small worker count stays a reasonable choice — a busier real deployment would be a real, honest reason to move to PostgreSQL instead, exactly the same upgrade path Chapter 10 already named.
TLS Termination
Chapter 4's own camera access requires HTTPS in production. Gunicorn itself doesn't handle TLS certificates — a reverse proxy (nginx, the same pattern already covered in this site's own Nginx In Depth course) sits in front of it, terminating HTTPS and forwarding plain HTTP internally, the same general shape both the Django and Express siblings' own deployment chapters already used.
A Production Checklist
- Run with
gunicorn+ Uvicorn workers, never--reload. - Decide deliberately whether
/docsshould be public, and disable it if not. - Confirm the SQLite file lives on a volume that survives a redeploy.
- Keep worker count modest, or plan a PostgreSQL migration, if genuine concurrent writes are expected.
- Put a reverse proxy in front for TLS termination.
- Load real configuration (database path, environment name) from environment variables, not hardcoded values.
Where This Course Is Headed
One chapter left: a capstone tying every chapter into one complete, working app.
Hands-On Exercises
Explain why this course never needed CORS configuration anywhere, tracing the reason back to a decision made in Chapter 1.
📄 View solutionExplain why Chapter 1 named /docs as a genuine advantage, and why this chapter still recommends deliberately deciding whether to expose it in production rather than leaving the default as-is.
📄 View solutionExplain why running with multiple gunicorn workers introduces a SQLite concern that Food Tracker (React + Express)'s own single-process deployment never had to face, and how this connects back to a point Chapter 10 already made.
📄 View solutionChapter 11 Quick Reference
- Run with: gunicorn -k uvicorn.workers.UvicornWorker -w 4, never --reload in production
- No CORS anywhere: StaticFiles has served the frontend same-origin since Chapter 1 — a real simplicity payoff none of the three sibling courses share
- Full-circle caveat: /docs, Chapter 1's own praised feature, should be a deliberate production decision, not an accidental default
- Familiar SQLite risk: confirm a persistent volume, same as the Express sibling's own warning
- New SQLite risk: multiple worker processes writing to one file can hit real lock contention — a direct consequence of Chapter 10's own "no real connection pool" point
- TLS: a reverse proxy in front, same pattern as the Django and Express siblings
- Next chapter: Capstone
Capstone: A Complete, Working Food Tracker
Food Tracker (FastAPI)
Chapter 12 · Capstone: A Complete, Working Food Tracker
Priya keeps her own household pantry tracked with the app this course just spent eleven chapters building — one FastAPI process, one SQLite file, one deliberately plain frontend, all deployed and running for real. What follows is one ordinary session with it.
Before touching the frontend at all, Priya's own developer opens /docs to confirm the deployment is healthy — Chapter 1's own praised automatic API explorer, still available in this deployment since it was left enabled deliberately, per Chapter 11's own decision.
Priya scans the barcode. Chapter 4's plain-function startScanner decodes it client-side, calling Chapter 3's genuinely async lookup route — httpx.AsyncClient checking BarcodeCache first, falling back to a live Open Food Facts call, with the event loop free to serve other requests the whole time it waits.
The add-item view pre-fills from that lookup. Priya sets the expiry date and submits; Chapter 5's ItemCreate schema validates the request before create_item's own body ever runs — no hand-written checks anywhere in that route, because there's nothing left to check by the time it executes.
Chapter 6's alerts route surfaces the eggs once they're within three days of expiring — a real typed Date column comparison, correct regardless of formatting, and a plain def since a local SQLite query never needed async in the first place.
Priya searches "egg." Chapter 7's ilike() finds it instantly — no shadow field, the same real advantage Django and Express both claim in their own courses.
The eggs get used up. Chapter 8's PATCH /{item_id}/use clears expiry_date and guards against a duplicate click, then Chapter 10's BackgroundTasks schedules a UsageLog entry — written after the response is already back in Priya's browser, using its own dedicated session since the request's own is already closed by then.
The same markUsed function calls loadAlerts() and loadRecipeSuggestions() directly, by name — Chapter 8's own honest point still holds: this app never needed anything like the Express sibling's own Context, because there's no component tree creating that coordination problem in the first place.
With chicken and spinach both nearing expiry, Chapter 9's asyncio.gather() fans out to TheMealDB concurrently — each lookup_ingredient call opening its own dedicated session, genuinely safe under concurrency, unlike the bug that pattern was specifically designed to avoid — ranked by how many expiring ingredients each recipe actually uses.
Priya's whole session runs against a real deployment: gunicorn managing several Uvicorn workers, no CORS configuration anywhere since the frontend has shared FastAPI's own origin since Chapter 1, worker count kept modest specifically to avoid the SQLite lock contention Chapter 11 named honestly, and nginx terminating HTTPS in front of it all.
Chapter Attribution
| Step | Chapter(s) applied |
|---|---|
| 1 — /docs | Chapter 1 (automatic docs), Chapter 11 (deliberately kept enabled) |
| 2 — Scanning | Chapter 4 (camera scanning), Chapter 3 (async lookup + BarcodeCache) |
| 3 — Adding the item | Chapter 5 (ItemCreate validation), Chapter 2 (schema, INSERT) |
| 4 — Expiry alert | Chapter 6 (typed Date comparison, plain def) |
| 5 — Search | Chapter 7 (ilike(), native case-insensitive substring match) |
| 6 — Marking used + logging | Chapter 8 (PATCH, active-only guard), Chapter 10 (BackgroundTasks, dedicated session) |
| 7 — Direct updates | Chapter 8 (no Context needed) |
| 8 — Recipe suggestion | Chapter 9 (asyncio.gather(), per-task session, relevance sort) |
| 9 — Real deployment | Chapter 11 (gunicorn, no CORS, worker count, TLS) |
/docs, praised in Chapter 1, got an honest second look in Chapter 11 before Step 1 could responsibly happen at all. This course's own throughline was never "FastAPI does everything automatically" — it was "here's exactly where FastAPI's own design choices pay off, and here's exactly where they don't," the same calibrated honesty every course in this quartet has tried to hold itself to.
BackgroundTasks is not a durable job queue was never resolved with a real job queue; and no automated test suite or CI pipeline was covered anywhere in this course.
Hands-On Exercises
Trace Step 6 in detail: explain what happens to the response Priya's browser receives versus what happens afterward in the background, and why the background portion needs its own database session.
📄 View solutionPick any two steps from Priya's session and explain how each one depends on at least two earlier chapters working together, not just one chapter in isolation.
📄 View solutionExplain the finding-box's own claim that none of Chapter 1's three opening claims (async-native, Pydantic validation, automatic docs) were treated as unconditional wins across this course. Give one concrete example for each of the three claims.
📄 View solutionChapter 12 Quick Reference — Course Complete
- 9 steps, 11 prior chapters — one continuous, realistic session with the finished, deployed app
- This course's own throughline, closed out: exactly where FastAPI's own design pays off, and exactly where it doesn't — never an unconditional win
- Honest scope note: no meal planner, no offline/PWA, no multi-user ownership model (same gap as Django and Express), TheMealDB matching remains best-effort, BackgroundTasks isn't durable, no automated tests/CI
- Food Tracker (FastAPI) is now complete — 12/12 chapters
- The entire Food Tracker Quartet is now complete — 4 courses, 50 chapters, one app, four architectures