This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Recipe Parser & Shopping List App - A Val Town application that captures, parses, stores recipes from multiple sources and generates smart shopping lists.
├── backend/
│ ├── database/
│ │ ├── migrations.ts # Database schema (4 tables)
│ │ └── queries.ts # Database operations
│ ├── routes/
│ │ ├── recipes.ts # Recipe CRUD operations
│ │ ├── parse.ts # Recipe parsing endpoints
│ │ └── shopping-lists.ts # Shopping list operations
│ └── index.ts # Main API entry point
├── frontend/
│ ├── components/
│ │ ├── App.tsx # Main app component
│ │ ├── RecipeForm.tsx # Recipe input form
│ │ ├── RecipeList.tsx # Recipe listing
│ │ ├── RecipeView.tsx # Individual recipe display
│ │ ├── ShoppingListCreator.tsx # Create shopping lists
│ │ ├── ShoppingListList.tsx # Shopping list overview
│ │ └── ShoppingListView.tsx # Interactive shopping list
│ ├── index.html # Main HTML template
│ └── index.tsx # Frontend entry point
└── shared/
├── types.ts # Shared TypeScript types
└── utils.ts # Shared utility functions
Since this is a Val Town project, there are no traditional build/test commands. Development happens directly in the Val Town environment:
GET /api/health
- Health checkGET /api/test-delete
- Test database operationsGET /api/test-transaction
- Test transaction rollback behaviorGET /api/test-ingredients
- Test optimized ingredient updatesGET /api/test-pagination
- Test pagination functionalitymigrations.ts
GET /api/recipes
- Get all recipes (supports filtering and pagination)
?search=pasta&difficulty=easy&maxPrepTime=30
?paginated=true&page=1&limit=20
?search=pasta&page=2&limit=10
POST /api/recipes
- Save a new recipeGET /api/recipes/:id
- Get specific recipePUT /api/recipes/:id
- Update recipeDELETE /api/recipes/:id
- Delete recipePOST /api/parse/url
- Parse from URLPOST /api/parse/pdf
- Parse from PDF (base64)POST /api/parse/image
- Parse from image (base64)POST /api/parse/text
- Parse from textGET /api/shopping-lists
- Get all lists (supports pagination)
?paginated=true&page=1&limit=25
POST /api/shopping-lists
- Create from recipe IDsGET /api/shopping-lists/:id
- Get specific listPUT /api/shopping-lists/:id
- Update list nameDELETE /api/shopping-lists/:id
- Delete listPOST /api/shopping-lists/:id/items
- Add item manually to shopping listPUT /api/shopping-lists/items/:id
- Toggle item checkedDELETE /api/shopping-lists/items/:id
- Remove item from listrecipeIds: []
)sqlite.batch()
method for atomic operationsgetAllRecipes()
and getAllShoppingLists()
CREATE TABLE IF NOT EXISTS
patternsimport { sqlite } from "https://esm.town/v/stevekrouse/sqlite";
const TABLE_NAME = 'recipes_v1';
// Change table name when modifying schema (e.g., _v2, _v3)
await sqlite.execute(`CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
)`);
import { OpenAI } from "https://esm.town/v/std/openai";
const openai = new OpenAI();
const completion = await openai.chat.completions.create({
messages: [{ role: "user", content: "Parse this recipe" }],
model: "gpt-4o-mini",
max_tokens: 1000,
});
import { blob } from "https://esm.town/v/std/blob";
await blob.setJSON("myKey", { hello: "world" });
let blobDemo = await blob.getJSON("myKey");
All imports use ESM.sh for browser/server compatibility:
// Use Val Town utility functions, NOT Hono's serveStatic
app.get("/frontend/*", c => serveFile(c.req.path, import.meta.url));
app.get("/shared/*", c => serveFile(c.req.path, import.meta.url));
/** @jsxImportSource https://esm.sh/react@18.2.0 */
// Unwrap Hono errors to see original error details
app.onError((err) => Promise.reject(err));
// In Hono
c.redirect('/new-path');
// Outside Hono
return new Response(null, { status: 302, headers: { Location: "/new-path" }});
import { serveStatic } from 'hono/middleware'
import { cors } from "@hono/cors"
- Val Town handles CORS automaticallyalert()
, prompt()
, or confirm()
in browser coderecipes_v1
→ recipes_v2
)Deno
keyword in shared/
directoryDeno.env.get('keyname')
for custom environment variablesTests database transaction rollback behavior by:
Expected Success Response:
{ "success": true, "message": "Transaction rollback working correctly - no orphaned data found" }
Tests optimized ingredient update performance by:
Expected Success Response:
{ "success": true, "message": "Optimized ingredient updates working correctly - bulk INSERT with 3 ingredients" }
Tests pagination functionality by:
Expected Success Response:
{ "success": true, "message": "Pagination working correctly - tested 5 recipes with page limits" }
// Convert queries to Val Town batch format
const batchQueries = queries.map(query => ({
sql: query.sql,
args: query.params // Note: 'args' not 'params'
}));
// Execute as atomic transaction
const results = await sqlite.batch(batchQueries);
// Paginated query with count
const { page, limit, offset } = validatePaginationParams(pagination);
// Get total count and paginated data in parallel
const [countResult, dataResult] = await Promise.all([
sqlite.execute('SELECT COUNT(*) as total FROM recipes_v1', params),
sqlite.execute('SELECT * FROM recipes_v1 ORDER BY created_at DESC LIMIT ? OFFSET ?', [...params, limit, offset])
]);
// Return with pagination metadata
return {
recipes: processedRecipes,
paginationInfo: createPaginationInfo(page, limit, total)
};
Response Format:
{ "success": true, "data": [...], "pagination": { "page": 1, "limit": 50, "total": 150, "totalPages": 3, "hasNext": true, "hasPrev": false } }
API Usage:
GET /api/recipes?paginated=true&page=2&limit=10
GET /api/recipes?search=pasta&page=1&limit=20
GET /api/shopping-lists?page=1&limit=25
The shopping list creation process includes intelligent ingredient consolidation that normalizes ingredient names (e.g., "kosher salt" → "salt", "fresh basil leaves" → "basil"). To optimize performance, an in-memory cache has been implemented:
Implementation:
// In-memory cache for ingredient name normalization (performance optimization)
const normalizationCache = new Map<string, string>();
function normalizeIngredientName(name: string): string {
// Check cache first for O(1) lookup
if (normalizationCache.has(name)) {
return normalizationCache.get(name)!;
}
// Perform normalization logic (20+ regex operations)
const result = performNormalization(name);
// Cache the result for future lookups (reduces O(n×m) to O(1) for repeat ingredients)
normalizationCache.set(name, result);
return result;
}
Performance Impact:
Test Endpoint: GET /api/test-normalization-cache