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

# routers/items.py from datetime import datetime @router.patch("/{item_id}/use", response_model=schemas.ItemResponse) def mark_used(item_id: int, db: Session = Depends(get_db)): item = ( db.query(models.Item) .filter(models.Item.id == item_id, models.Item.status == "active") .first() ) if item is None: raise HTTPException(status_code=404, detail="Item not found, or already used") item.status = "used" item.used_at = datetime.utcnow() item.expiry_date = None db.commit() db.refresh(item) return item

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.

A small, real advantage in the path parameter too
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.
This must be a PATCH, never a plain link
A state-changing action like this must never be reachable via a plain 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

// static/app.js async function markUsed(id) { await fetch(`/api/items/${id}/use`, { method: "PATCH" }); loadAlerts(); if (!document.getElementById("recipes-view").classList.contains("hidden")) { loadRecipeSuggestions(); } }
Why this course never needed anything like Food Tracker (React + Express)'s own Context
That course's own Chapter 10 built a shared 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

Exercise 1

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

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

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

Chapter 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