Building reliable barcode lookup for a nutrition app
A successful scan is not the same thing as a successful product lookup. The camera may decode a valid GTIN that your database has never seen, or multiple source rows may share the identifier. Treat “not found” and ambiguity as normal product states.
Keep identifiers as strings
UPC, EAN and GTIN values are identifiers, not quantities. Store them as text so leading zeroes survive serialization and spreadsheet imports. GS1 notes that applications using a uniform 14-digit GTIN format add leading zeroes to shorter forms.
function cleanBarcode(raw) {
return raw.replace(/[^0-9]/g, "");
}
const code = cleanBarcode(scannerValue);
if (![8, 12, 13, 14].includes(code.length)) {
showError("That does not look like a supported GTIN");
}
Decide whether your API stores the scanned representation or canonical GTIN-14. If you normalize, do it during both ingestion and lookup, and preserve the original value for debugging.
Validate the check digit before calling the API
A scanner library can still return truncated input or a manually entered typo. GTIN uses a modulo-10 check digit with alternating weights. Rejecting an invalid code saves a network request and gives a faster explanation.
function validGtin(code) {
if (!/^\d{8}$|^\d{12,14}$/.test(code)) return false;
const digits = [...code].map(Number);
const check = digits.pop();
const sum = digits.reverse().reduce(
(total, digit, i) => total + digit * (i % 2 === 0 ? 3 : 1), 0);
return (10 - (sum % 10)) % 10 === check;
}
Use an exact index, never fuzzy barcode matching
Names benefit from fuzzy search. Barcodes do not. A one-digit difference identifies another item or an invalid code. Put a B-tree index on the canonical barcode column and use equality.
CREATE INDEX CONCURRENTLY foods_barcode_idx ON foods (barcode);
SELECT id, name, brand, confidence
FROM foods
WHERE barcode = $1;
Resolve duplicates deterministically
Multiple source records can share a barcode because of imports, translations or packaging updates. Dietly prefers a row with an image, then higher internal confidence, then the lowest stable ID. Your exact signals may differ, but the order must be explicit and stable.
ORDER BY
(image_url IS NOT NULL) DESC,
confidence DESC NULLS LAST,
id ASC
LIMIT 1;
For high-stakes workflows, return several candidates or source metadata instead of silently selecting one. A calorie tracker can usually show one best record plus an “incorrect product?” action.
A 404 is a user journey
When the API returns no match, offer text search, manual nutrition entry and, when your data source supports it, a contribution flow. Do not generate nutrition values merely because a barcode exists.
const response = await fetch(`${API}/barcode/${code}`);
if (response.status === 404) {
showNoMatch({ actions: ["Search by name", "Enter label manually"] });
return;
}
if (!response.ok) throw new Error("Lookup temporarily unavailable");
showProduct(await response.json());
Measure the right things
Scanner decode success
Valid-GTIN rate
Database hit rate by country and device locale
Duplicate rate and winner changes
User corrections after a successful match
Latency from scan to rendered product
Do not market a universal barcode coverage percentage unless you have a representative, reproducible test set.
Normalize symbologies without erasing meaning
A camera SDK may report UPC-A, EAN-13 or another symbology separately from the digits. Capture both. A 12-digit UPC can be represented as a 13-digit EAN with a leading zero, and GTIN systems may pad to 14 digits, but arbitrary padding is not a substitute for a documented canonicalization rule. Apply the same rule when importing suppliers, reading CSV files and serving API requests.
Be careful with spreadsheet tooling. It may remove leading zeroes or display a long identifier in scientific notation. Validate imports before they reach the production table and keep rejected rows with reasons. Treating a barcode as an integer also makes JSON clients vulnerable to language-specific numeric limits. A string avoids all of these problems.
Separate scan errors from catalog misses
Users need different messages for an unreadable image, an invalid check digit, a valid but unsupported code type, a valid GTIN absent from the catalog and a temporary server failure. Combining them into “product not found” sends people toward the wrong remedy. Let them rescan when decoding failed, correct digits when validation failed, search by name for a catalog miss and retry later for a network error.
Mobile cameras often decode the same barcode repeatedly while the product remains in frame. Debounce identical scan events and cancel in-flight duplicates. Lock the result screen only after a valid response or deliberate no-match state. This avoids flicker and prevents one physical scan from consuming many API requests.
Cache hits and misses differently
A successful product lookup can usually be cached because labels change less frequently than users revisit them. A miss should receive a shorter negative cache: the catalog may add that barcode in the next import or after a community contribution. Include the normalized identifier and relevant dataset edition in the cache key. Invalidate or version cached records when your response contract changes.
For offline-first apps, store the product together with source and retrieval time. Make it clear when the result is cached, especially if users rely on recently changed allergens or formulations. The physical package remains the authoritative source for immediate safety decisions.
Duplicate handling needs observability
Record how often an exact barcode returns multiple rows, which tie-breaker selected the winner and whether users reject that choice. A rising duplicate rate may indicate an ingestion regression. If the winner changes after a refresh, compare the old and new source, serving and nutrient values before assuming the higher confidence record is better.
Do not deduplicate solely on a shared name or barcode without considering market and lifecycle information. Manufacturers sometimes reuse packaging conventions, databases contain historic records, and regional versions can differ. Deterministic ranking provides a safe default while preserving the underlying rows for review.
Test with a representative barcode set
Build a dated test pack covering the countries and retailers your users actually scan. Include known hits, known misses, damaged labels, short GTINs, leading-zero cases and duplicated records. Publish the methodology if you make coverage claims. A random sample from your own database measures whether you can rediscover records you already possess; it does not measure real-world barcode coverage.
Run the set through the full mobile path, not only the database query. Camera focus, lighting, orientation, network latency and result rendering all contribute to the experience. The lookup SQL can be instantaneous while the feature still feels unreliable.
Try it: GET https://api.getdietly.com/barcode/{code} performs indexed EAN/UPC lookup and returns one normalized food object or a normal 404 miss.
Identifier reference: GS1 identification keys.
Originally published at getdietly.com. Data from the Dietly Nutrition API — 4.7M+ indexed foods, free tier available.
Top comments (1)
Variable weight products are the one case an exact match can never win. Stores embed the price or weight in the code itself, so the same cheese scans differently on every package. Detecting those store prefix ranges and sending the user straight to name search would save them a dead 404.