Barcode Lookup: Integrating Open Food Facts

Food Tracker (React + Express)

Chapter 3 · Barcode Lookup: Integrating Open Food Facts

Every course in this quartet integrates Open Food Facts the same way in spirit: through a server the client trusts, not directly from the browser. Here, that server is the same Express process built in Chapter 2 — one more route, reusing the exact same db connection.

Why Proxy Through the Server At All

Open Food Facts needs no API key — there's no secret to hide, so this isn't about security the way an authenticated third-party API would demand. The real reasons to route the lookup through Express rather than calling Open Food Facts directly from React are the same ones the Django and Firebase courses reasoned through in their own Chapter 3/4:

  • Caching. The same barcode gets scanned repeatedly over time — caching the result server-side avoids re-querying Open Food Facts for a product this app has already looked up.
  • Consistency. Every client — this React app today, a future mobile app tomorrow — gets identical lookup behavior, defined in one place.
  • Future-proofing. If Open Food Facts' own API shape ever changes, or a second data source gets added later, only the server needs to change.

Extending the Schema: A Barcode Cache

Reusing Chapter 2's own schema.sql file and db.js connection, one more table:

-- schema.sql (appended) CREATE TABLE IF NOT EXISTS barcode_cache ( barcode TEXT PRIMARY KEY, name TEXT, category TEXT, cached_at TEXT NOT NULL DEFAULT (datetime('now')) );

barcode is the primary key here — deliberately different from Chapter 2's own items table, where the same barcode can legitimately appear on many separate rows (many separate purchases of the same product over time). One barcode maps to exactly one cached product lookup, but potentially many pantry items.

The Lookup Route

// routes/lookup.js import { Router } from "express"; import db from "../db.js"; const router = Router(); router.get("/:barcode", async (req, res) => { const { barcode } = req.params; const cached = db.prepare("SELECT * FROM barcode_cache WHERE barcode = ?").get(barcode); if (cached) return res.json(cached); const response = await fetch( `https://world.openfoodfacts.org/api/v2/product/${barcode}.json` ); const data = await response.json(); if (data.status !== 1) { return res.status(404).json({ error: "Product not found" }); } const name = data.product.product_name || null; const category = data.product.categories_tags?.[0] || null; db.prepare( "INSERT INTO barcode_cache (barcode, name, category) VALUES (?, ?, ?)" ).run(barcode, name, category); res.json({ barcode, name, category }); }); export default router; // server/index.js import lookupRouter from "./routes/lookup.js"; app.use("/api/lookup", lookupRouter);
No extra package needed for the HTTP call
fetch is a genuine Node.js global as of Node 18 — no axios or node-fetch dependency required to call an external API from the server. The exact same fetch API a browser already knows now works identically on the server side, one more small piece of "the same language, both ends" this course keeps returning to.
Open Food Facts' data is genuinely inconsistent
Because Open Food Facts is a crowdsourced, community-maintained database, a valid barcode can still return a product with a missing name, no category, or sparse data generally — data.status === 1 only means "a product exists for this barcode," not "this product has complete data." The route above already returns null for missing fields rather than throwing — Chapter 5's own add-item form has to be built expecting that, with a manual-entry fallback for whatever the lookup didn't provide.
Same lesson, reused route pattern
Django's own view called requests; Firebase's Cloud Function called fetch in an isolated serverless context. Here, the exact same integration lives as one more route in the same Express app already serving /api/items — no new deployment target, no new runtime, just another file mounted the same way Chapter 2 established.

Where This Course Is Headed

The camera-scanning React component next — shared almost verbatim with Food Tracker (React + Firebase), since decoding a barcode client-side has nothing to do with which backend receives it afterward.

Hands-On Exercises

Exercise 1

Explain the three reasons this chapter gives for proxying the Open Food Facts lookup through Express rather than calling it directly from React, given that no API key is involved.

📄 View solution
Exercise 2

Explain why barcode is the primary key in barcode_cache but not in the items table, even though both tables have a barcode column.

📄 View solution
Exercise 3

Explain why data.status === 1 is not the same guarantee as "this product has complete data," and what the lookup route does about that in practice.

📄 View solution

Chapter 3 Quick Reference

  • Why proxy: caching, consistency across clients, future-proofing — not secrecy (no API key needed)
  • New table: barcode_cache, keyed by barcode (one lookup per barcode, unlike items)
  • Route: GET /api/lookup/:barcode — checks the cache first, else calls Open Food Facts and caches the result
  • Node's built-in fetch: no axios/node-fetch dependency needed since Node 18
  • Real gotcha: Open Food Facts data is crowdsourced and often incomplete — null fields are expected, not an error
  • Next chapter: Camera-Based Barcode Scanning in React