Engine Architecture & Automated Pipelines
System Architecture
To support our meal-planning e-commerce app, we designed a backend that transforms unstructured text input (such as copying a food recipe URL or raw text ingredients list) into a list of purchasable organic products.
The service is built on top of a modular NestJS framework, chosen for its strong TypeScript architecture, dependency injection patterns, and decoupled service layers. The architecture is split into three main modules:
- Recipe Parser Gateway: Communicates with OpenAI's API (`gpt-4o-mini`) using strict JSON schema validation to extract ingredients, quantities, and units from plain text.
- Normalizer Engine: Translates arbitrary culinary descriptions (like "half a cup", "a bunch", or "a pinch") into standardized SI units (grams, milliliters) or unit counts using conversion coefficients.
- Catalog Matching Service: Interfaces with a PostgreSQL database via Prisma ORM, executing fuzzy string searches and word-similarity matches to align standardized ingredients with real, available inventory SKUs.
Database Schema & Prisma
Our entity relations map weekly schedules, user plans, parsed recipes, catalog items, and supplier details. We use Prisma to enforce type safety and write clean relational queries.
In alignment with our business strategy, our sourcing system prioritizes local family-farms practicing agroecology. If a product is unavailable, it falls back to wholesale backup supplies (Assaí Atacadista). The relational schema enforces this by linking each SKU to a specific supplier type:
enum SupplierType {
LOCAL_AGROECOLOGICAL // Local agroecological family farms
WHOLESALE_BACKUP // Assaí Atacadista (Fulfillment backup)
}
model Supplier {
id String @id @default(uuid())
name String // e.g. "Cooperativa Agroecológica de Irecê"
type SupplierType
skus Sku[]
createdAt DateTime @default(now())
}
model Sku {
id String @id @default(uuid())
name String // e.g. "Organic Hass Avocado (Pack of 4)"
price Decimal
stock Int
supplierId String
supplier Supplier @relation(fields: [supplierId], references: [id])
ingredients RecipeIngredient[]
}
model Recipe {
id String @id @default(uuid())
title String
sourceUrl String?
ingredients RecipeIngredient[]
createdAt DateTime @default(now())
}
model RecipeIngredient {
id String @id @default(uuid())
recipeId String
recipe Recipe @relation(fields: [recipeId], references: [id])
rawText String // e.g. "3 ripe Hass avocados, cubed"
standardName String // e.g. "Organic Hass Avocado"
quantity Float // e.g. 3.0
unit String // e.g. "pieces"
matchedSkuId String?
matchedSku Sku? @relation(fields: [matchedSkuId], references: [id])
}LLM Recipe Scraper
To parse raw recipe details, our service uses the OpenAI API. Ingesting unstructured text relies on structured prompt engineering to coerce the model to output strict JSON schemas.
The service passes the raw text to `gpt-4o-mini` with a system prompt detailing the output template and forcing a strict JSON format structure:
const systemPrompt = `
You are an expert culinary parser. Given an unstructured list of ingredients, extract them into a valid JSON array.
JSON Schema format:
[
{
"ingredient": "string (lowercase, singular, standard name)",
"quantity": "number (float)",
"unit": "string (pieces, grams, ml, cup, pinch, tablespoon, teaspoon)"
}
]
Only output JSON. Do not add markdown blocks or notes.
`;We perform secondary runtime validation on the returned payload using Zod schemas. If the model fails validation (e.g. returning strings instead of floats for quantities), NestJS captures the exception and runs a fallback Regex parser.
Ingredient Matching Pipeline
Once ingredients are standardized, we need to map them to active store items. Standardizing quantities is crucial: we convert cups, tablespoons, and ounces to SI units using localized coefficients. For example:
To query SKUs matching the parsed ingredients, we use PostgreSQL's pg_trgm trigram word-similarity matching, but filter results based on supplier strategy.
Our query prioritization order is:
- Local Agroecological: Find the best fuzzy match supplied by local chemical-free family farms with
stock > 0. - Wholesale Backup: If no local supplier has stock, fall back to the backup supplier (Assaí Atacadista) to avoid order failure.
// Query mapping that prioritizes local agroecological suppliers
async findSkuBySimilarity(name: string, threshold = 0.4) {
// 1. Try local agroecological suppliers first
const localMatch = await this.prisma.$queryRaw<Sku[]>`
SELECT s.id, s.name, s.price, s.stock,
similarity(s.name, ${name}) AS score
FROM "Sku" s
JOIN "Supplier" sup ON s."supplierId" = sup.id
WHERE similarity(s.name, ${name}) > ${threshold}
AND s.stock > 0
AND sup.type = 'LOCAL_AGROECOLOGICAL'
ORDER BY score DESC
LIMIT 1
`;
if (localMatch.length > 0) return localMatch[0];
// 2. Fallback to wholesale backup (e.g. Assaí Atacadista)
const backupMatch = await this.prisma.$queryRaw<Sku[]>`
SELECT s.id, s.name, s.price, s.stock,
similarity(s.name, ${name}) AS score
FROM "Sku" s
JOIN "Supplier" sup ON s."supplierId" = sup.id
WHERE similarity(s.name, ${name}) > ${threshold}
AND s.stock > 0
AND sup.type = 'WHOLESALE_BACKUP'
ORDER BY score DESC
LIMIT 1
`;
return backupMatch[0] || null;
}Interactive Pipeline Sandbox
Test out the interactive pipeline below to see how a raw avocado salad recipe text block progresses through our parsing, normalizer, trigram database matching, and cart-loading operations:
Healthy Avocado Salad - 3 ripe Hass avocados, cubed - 1 cup fresh organic cherry tomatoes, halved - 1/2 cup organic extra virgin olive oil - 1 pinch of sea salt