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

# models.py (appended) class BarcodeCache(Base): __tablename__ = "barcode_cache" barcode = Column(String, primary_key=True) name = Column(String, nullable=True) category = Column(String, nullable=True) cached_at = Column(DateTime, server_default=func.now())

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

# routers/lookup.py import httpx from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from database import get_db import models router = APIRouter() @router.get("/{barcode}") async def lookup_barcode(barcode: str, db: Session = Depends(get_db)): cached = db.query(models.BarcodeCache).filter_by(barcode=barcode).first() if cached: return cached async with httpx.AsyncClient() as client: try: response = await client.get( f"https://world.openfoodfacts.org/api/v2/product/{barcode}.json", timeout=5.0, ) except httpx.RequestError: raise HTTPException(status_code=502, detail="Open Food Facts is unreachable") data = response.json() if data.get("status") != 1: raise HTTPException(status_code=404, detail="Product not found") product = data.get("product", {}) entry = models.BarcodeCache( barcode=barcode, name=product.get("product_name"), category=(product.get("categories_tags") or [None])[0], ) db.add(entry) db.commit() db.refresh(entry) return entry
A real payoff of "async-native," not just a slogan
Food Tracker (Django)'s own equivalent view used the synchronous 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.
Open Food Facts' data is genuinely inconsistent
A valid barcode can still return a product with a missing name or no category at all, since Open Food Facts is crowdsourced. 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.
HTTPException is a structured way to fail
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

Exercise 1

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

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

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

Chapter 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)