How I Vibe Coded NutriAI — Diet Planner in Weeks?
- May 13
- 13 min read
The Complete Story of how I vibe coded NutriAI - Diet Planner.
Link to try the live product at the end of the article.
I am Shivam D. and I am a AI, Technology Analyst.
The Idea
The problem was simple: free diet planning tools online are either too generic (give me a 2000 kcal plan), too Western (chicken breast and broccoli), or locked behind a paywall (HealthifyMe charges ₹999/month). Indian users — especially in tier-2/3 cities — have specific needs: roti, dal, sabzi, regional cuisine, budget constraints, medical conditions like PCOS and diabetes that are extremely common, and no money to spend on a dietitian.
I wanted to build something that:
Gave a real 7-day personalised meal plan in under 60 seconds
Understood Indian food natively
Adapted for 15 medical conditions
Was completely free
Looked professional enough to share
Technical Stack I Chose
I didn't choose the stack analytically. I described what I wanted and Claude recommended it:
Backend: Python 3.11 + Flask 3.0.3 AI Engine: Google Gemini 1.5 Flash (free tier) Database: SQLite locally → PostgreSQL on Render (via universal adapter) Frontend: Bootstrap 5 + Chart.js + vanilla JS Hosting: Render.com (free tier) Auth: Werkzeug scrypt password hashing CSS: Custom design system with CSS variables
The choice of Flask over Django was deliberate — lighter, faster to prototype, and easier to reason about when you're vibe coding. Every route is visible in one file.
How I vibe coded Nutri AI- Diet planner?
Tools Used
Primary Tools
Claude (Anthropic) — AI pair programmer for all code, debugging, architecture decisions
Render.com — Deployment platform (free web service + free PostgreSQL)
Google AI Studio — Gemini API key management and usage monitoring
GitHub — Version control and deployment trigger (git push = auto-deploy)
VS Code — Local editing (though most code was written by Claude)
PowerShell — Terminal on Windows for git commands
APIs & Services
Google Gemini 1.5 Flash — Diet plan generation, meal swap, food image recognition, voice log parsing
Web Speech API — Browser-native voice input (zero cost)
Open Food Facts (planned) — Food database for diary search
SendGrid (planned) — Email delivery for password reset
Razorpay (planned) — Payments for freemium model
Python Packages
flask==3.0.3
google-generativeai==0.7.2
werkzeug==3.0.3
psycopg2-binary>=2.9.9
python-dotenv==1.0.1
gunicorn==22.0.0
openai==1.40.0
No Flask-SQLAlchemy. No Flask-Login. No Celery. Every abstraction was built from scratch so I could understand and control it completely. This turned out to be a key advantage when debugging.
Hope you are following how I vibe coded NutriAI- Diet planner.

The Build Process — Version by Version
v1: Proof of Concept (Day 1-2)
What I described: "Build me a Flask app where a user fills in a form with their age, weight, height, gender, and goal, and the AI generates a 7-day diet plan."
What Claude built:
Single app.py with 3 routes (/, /questionnaire, /analyze)
Basic HTML form with 8 fields
Gemini API call that returned raw text
Results displayed as plain text on a page
What worked: The AI actually generated real meal plans. The first time I saw "Monday Breakfast: Moong Dal Cheela × 2 + Mint Chutney — 280 kcal, 14g protein" I knew this was going to work.
First bug encountered: The AI returned markdown (with bold and - bullets). The results page showed raw markdown characters. Fix: Claude added a JSON format requirement to the prompt and a JSON parser.
Key learning: Always specify the output format in your AI prompt. "Return as JSON" is the single most important instruction you can give Gemini.
v2: Real Structure (Day 3-5)
What I described: "Split the code into models, services, and templates. Add BMR calculation, health score, and a proper results page."
What Claude built:
services/diet_calculator.py — Mifflin-St Jeor BMR formula, TDEE with activity multipliers, macro ratios by goal
services/ai_diet_generator.py — Prompt engineering with Harvard Plate + Eatwell Guide rules
services/health_analyzer.py — 0-100 health score from 12 lifestyle factors
models/user_model.py and models/diet_plan_model.py — SQLite tables
A proper results page with Bootstrap cards per meal
Configuration added:
GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY')
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-key')
ADMIN_USERNAME = os.environ.get('ADMIN_USERNAME', 'admin')
ADMIN_PASSWORD = os.environ.get('ADMIN_PASSWORD', 'nutriai@admin2025')
Challenge: The AI kept generating chicken dishes for vegetarian users. Claude solved this by adding an explicit NEVER list in the prompt: "NEVER include: chicken, fish, tuna, salmon, mutton, beef, pork, prawn, or any seafood." And then added a post-generation safety validator that rejected plans containing banned ingredients and auto-triggered fallback.
Bug: SQLite INTEGER PRIMARY KEY AUTOINCREMENT syntax — I didn't know you need AUTOINCREMENT explicitly. Spent 30 minutes confused why IDs weren't incrementing. Claude spotted it immediately.
v3: User Auth + Admin (Day 6-8)
What I described: "Add user accounts so people can save their plans. Also add an admin panel where I can see all users and their data."
What Claude built:
models/auth_model.py — register, login, session management with Werkzeug scrypt hashing
models/admin_model.py — full user table, stats, signups chart
templates/auth/ — register, login pages with split-screen design
templates/admin/ — dashboard with 4 Chart.js charts, searchable user table, user detail page
@login_required and @admin_required decorators
Admin credentials set: username admin, password nutriai@admin2025 (in config.py, overridable via env vars)
Challenge: The admin dashboard was showing raw Python dict format for goals and diet types instead of rendering the Chart.js pie charts. The issue was that Jinja2's |tojson filter was converting Python dict keys to quoted JSON but Chart.js wanted specific formatting. Claude refactored get_admin_stats() to return the data pre-formatted for Chart.js.
Challenge 2: Password hashing was using MD5 initially (a placeholder). Before deployment Claude switched to Werkzeug's generate_password_hash with scrypt algorithm. Existing test passwords stopped working — had to re-register.
v4: PostgreSQL + SEO + PWA (Week 2)
What I described: "The app loses all data when Render redeploys. Fix this. Also add SEO and make it installable on mobile."
What Claude built:
db.py — Universal database adapter. Detects DATABASE_URL env var → uses PostgreSQL with psycopg2. Falls back to SQLite locally. All queries use q() helper that converts ? to %s for PostgreSQL.
render.yaml — Blueprint config that auto-creates PostgreSQL database on Render
robots.txt route, sitemap.xml route, manifest.json route
sw.js — Service worker for PWA
Full Open Graph + JSON-LD structured data in base.html
Keep-alive ping every 13 minutes (prevents Render cold starts)
The robots.txt bug: The first version had Allow: / before Disallow: /admin/. This is actually a conflict in strict parsers — many crawlers were blocking the entire site because Allow and Disallow for * were both present. I discovered this when Claude couldn't fetch the homepage — it got ROBOTS_DISALLOWED error. Fix: remove the Allow: / line entirely. When you don't specify Allow, everything is allowed by default.
The psycopg2 crash: After connecting PostgreSQL on Render, the app crashed immediately with:
ImportError: PyInterpreterStateGet undefined symbol
Render had defaulted to Python 3.14 (brand new at the time). psycopg2-binary 2.9.9 has no pre-compiled binary for Python 3.14 yet. Fix: add runtime.txt containing python-3.11.9 to pin the Python version.
Configuration at this point:
GEMINI_API_KEY=AIza... # Google AI Studio
DATABASE_URL=postgresql://... # Render Internal URL
SECRET_KEY=random-hex-64 # Flask session signing
ADMIN_USERNAME=admin
ADMIN_PASSWORD=nutriai@admin2025
Hope you are still following how I vibe coded NutriAI- Diet planner.
v5: Security + Features + Password Reset (Week 2-3)
What I described: "Add rate limiting so bots can't abuse the Gemini API. Add password reset. Add food diary. Add a printable plan page."
What Claude built:
services/rate_limiter.py — In-memory rate limiter (no Redis needed). Per-IP tracking. 6 endpoints: analyze (5/10min), login (10/5min), register (5/hr), admin (5/5min), diary (30/min), swap (20/min)
models/reset_model.py — Token-based password reset. secrets.token_urlsafe(32) generates 43-char tokens. 1-hour expiry. Single-use. Email enumeration protection.
models/diary_model.py — Food diary with AJAX add/delete, real-time macro totals vs plan target
templates/print_plan.html — Print-optimised 4-column day grid, Ctrl+P → PDF
Medical disclaimer banner on results page for users with medical conditions
Bug found in testing: Age validation existed in the HTML form (min=10, max=100) but had no server-side check. A user could bypass the browser and POST age=0 or age=999. The AI would generate a diet plan for a 0-year-old. Fix: min(max(int(age), 10), 100) on the server.
Bug 2: get_ip() in the rate limiter returned an empty string in test context when request.remote_addr was None. This meant all test-context requests shared one rate-limit key and tests were blocking each other. Fix: return '127.0.0.1' as fallback.
The test suite: At this point I asked Claude to write a comprehensive test suite. It wrote 103 test cases across 20 categories. Running them revealed 2 real bugs (age validation, IP fallback) and 6 test bugs (wrong expected values). Pass rate: 90/103 (87.4%). After fixing test assertions: 103/103.
v6: Full Feature Sprint (Week 3)
What I described: "Add two questionnaire modes (quick 60-second and pro 13-section), meal swap button, food image recognition, voice input, smart grocery list, and plan caching."
What Claude built in one session:
1. Two-Mode Questionnaire
/quick — templates/quick_questionnaire.html. 5-step chat flow with animated AI bubble, tap-to-select cards, gender picker, medical condition toggles, Pro upsell at final step, loading overlay with 5-step animation
/questionnaire (Pro) — Updated with Quick ↔ Pro mode switcher at the top
Both modes submit to the same /analyze route with a mode=basic or mode=pro field
2. Meal Swap (services/meal_swap.py)
"Swap Meal" button on every meal card in results
Calls /api/swap-meal which calls Gemini with: current meal name, exact calorie/macro targets, diet type, cuisine preference
Veg/vegan safety enforced on the swap output
4-hour cache per swap (same meal swapped twice returns same alternative)
Fallback pool of 4 alternatives per meal type if Gemini is unavailable
Rate-limited: 20/minute per IP
3. Food Image Recognition (services/vision_service.py)
Drag-drop or click-to-upload photo zone on results page
Sends base64 image to Gemini Vision API
Returns: list of identified foods with portions, total calories/macros, health rating, confidence level
"Log to Diary" button auto-fills the diary
10MB size limit, rate-limited 10/minute
4. Voice Input (Web Speech API + Gemini)
"Start Recording" button uses window.SpeechRecognition — browser-native, zero cost
User says: "I had two boiled eggs and a piece of whole wheat toast"
Transcript sent to /api/voice-log → Gemini parses → returns nutrition estimate
"Confirm & Log" button adds to diary
Works on Chrome, Edge. Not supported on Firefox (graceful fallback message)
5. Smart Grocery List
Auto-generated from the 7-day meal plan
7 categories: Grains & Staples, Proteins, Vegetables, Fruits, Dairy, Nuts & Seeds, Oils & Spices
Keyword matching algorithm categorises each ingredient
WhatsApp export: opens wa.me/?text=... with formatted list
Clipboard copy fallback
6. Plan Cache (services/plan_cache.py)
In-memory dictionary, no Redis
Key = MD5 hash of: diet_type, goal, gender, age_band (decade), BMI category, calorie_band (100s), activity, medical conditions, cuisine
6-hour TTL
Prunes oldest 50 entries when cache exceeds 200 items
Reduces Gemini API calls by ~60% for similar profiles
Integrated transparently into /analyze route — users never know

The Prompt Engineering That Made It Work
The single most important thing in an AI diet app is the AI prompt. Here's the philosophy behind NutriAI's prompt:
The Diet Safety Rules
DIET TYPE: VEGETARIAN
NEVER include: chicken, fish, tuna, salmon, mutton, beef, pork, prawn, or any seafood.
This is a HARD CONSTRAINT. Do not include any meat or fish under any circumstances.
The word "NEVER" in caps was tested specifically. Without it, Gemini would occasionally suggest "grilled fish" for a vegetarian user, citing "you can substitute with paneer." With "NEVER" in caps, violations dropped to near zero.
The Science Framework
Follow Harvard Healthy Eating Plate principles:
1. Fill ½ plate with vegetables and fruits
2. Fill ¼ plate with whole grains
3. Fill ¼ plate with healthy proteins
4. Use healthy oils in moderation
5. Drink water, not sugary beverages
Follow UK Eatwell Guide:
- 5 portions fruit/vegetables daily
- Base meals on starchy carbohydrates
- Include dairy or alternatives
- Include proteins: beans, pulses, fish, eggs, meat
- Limit saturated fat, salt, sugar
This gave the AI a structured nutritional framework instead of letting it improvise. Users would ask "how is this scientifically validated?" — the answer: Harvard + Eatwell Guide, both embedded in every generation.
Medical Condition Adaptation
Medical conditions: Type 2 Diabetes
DIABETES GUIDELINES: Low glycaemic index foods only. No refined sugar or white flour.
Limit portion sizes for all carbohydrates. Include high-fibre options at every meal.
Choose complex carbohydrates: brown rice, daliya, whole wheat roti.
Avoid: maida, white rice, sugary drinks, sweets, fried foods.
Each of the 15 conditions had its own set of specific rules appended to the prompt. This was not generic advice — it was targeted nutritional modification per condition.
The Output Format
{
"week_plan": {
"Monday": {
"breakfast": {
"meal": "Moong Dal Cheela × 2 + Mint Chutney",
"description": "High-protein gram flour pancakes...",
"calories": 280,
"protein": 14,
"carbs": 35,
"fats": 7,
"fibre": 5,
"prep_time": "15 min",
"eatwell_segments": ["protein", "whole_grains"]
}
}
},
"lifestyle_tips": ["..."],
"grocery_list": ["..."]
}
Strict JSON output meant the app could parse and display data reliably without any text cleanup.
Deployment Configuration
render.yaml (Infrastructure as Code)
services:
- type: web
name: nutriai-diet-planner
runtime: python
buildCommand: pip install -r requirements.txt
startCommand: gunicorn app:app --workers 2 --timeout 120
envVars:
- key: DATABASE_URL
fromDatabase:
name: nutriai-db
property: connectionString
- key: SECRET_KEY
generateValue: true
- key: GEMINI_API_KEY
sync: false
healthCheckPath: /ping
databases:
- name: nutriai-db
plan: free
One file. Push to GitHub, connect on Render → entire infrastructure created automatically.
runtime.txt
python-3.11.9
One line that saved the entire deployment. Without this, Render used Python 3.14 and psycopg2 crashed.
The Database Adapter Pattern
# db.py — the most important 50 lines in the project
import os
PG = bool(os.environ.get('DATABASE_URL'))
def get_db():
if PG:
import psycopg2, psycopg2.extras
conn = psycopg2.connect(os.environ['DATABASE_URL'])
conn.cursor_factory = psycopg2.extras.RealDictCursor
else:
import sqlite3
conn = sqlite3.connect('database/nutriai.db')
conn.row_factory = sqlite3.Row
return conn
def q(sql):
"""Convert SQLite ? placeholders to PostgreSQL %s"""
return sql.replace('?', '%s') if PG else sql
This 50-line file meant the entire application worked identically on both SQLite (local development) and PostgreSQL (production) with zero code changes elsewhere.
Bugs - Hall of Fame
Bug 1: The Veg Violation Bug
What happened: Gemini would generate "Grilled Fish with Vegetables" for vegetarian users and justify it as "you can substitute with tofu." How long to fix: 45 minutes of prompt iterationFix: Added NEVER in all caps, explicit banned word list, and a post-generation safety validator that scanned the JSON for banned words and auto-triggered fallback if found.
Bug 2: The robots.txt Blocking Bug
What happened: Added Allow: / to robots.txt thinking it was required. It created a conflict that caused strict crawlers (including Claude's web fetch tool) to block the entire site. Discovery:Tried to fetch the homepage and got ROBOTS_DISALLOWED error for the homepage itself. Fix: Remove Allow: / entirely. Absence of Allow = everything allowed.
Bug 3: Python 3.14 psycopg2 Crash
What happened: Render deployed on Python 3.14. psycopg2-binary has no pre-compiled binary for 3.14 yet. App crashed on startup. Error: ImportError: PyInterpreterStateGet undefined symbol Fix: Add runtime.txt with python-3.11.9. One line fix.
Bug 4: Admin Dashboard Shows Homepage
What happened: Admin dashboard was crashing with a 500 error. The error handler was silently showing index.html. So it looked like the homepage was loading at /admin/dashboard.Discovery: The URL bar showed /admin/dashboard but the content was the homepage. Something was 500ing silently. Fix: Changed the 500 error handler to show actual error details when on /admin/* routes. Error was a PostgreSQL INTERVAL syntax issue (date('now','-7 days') is SQLite syntax, not PostgreSQL). Fixed to NOW() - INTERVAL '7 days'.
Bug 5: Rate Limiter Empty IP Bug
What happened: In test context, request.remote_addr is None. The get_ip() function returned ''. All test requests shared one empty-string key and were blocking each other after 5 calls. Discovery: E2E tests were getting 429 responses even on fresh clients. Fix: Return '127.0.0.1' as fallback when remote_addr is empty.
Bug 6: Age Validation Server-Side Missing
What happened: The HTML form had min="10" max="100" on the age field. But the server just did int(form.get('age', 25)) with no range check. A direct POST with age=0 or age=999 would succeed and generate a diet plan for an impossible age. Discovery: Comprehensive test suite caught this when testing boundary inputs. Fix: min(max(int(age), 10), 100) on the server side.
Bug 7: SQLite Data Loss (recurring)
What happened: Every git push triggered a Render redeploy. Render's filesystem is ephemeral — the SQLite .db file is destroyed on each deploy. All users, plans, and progress entries vanished. Discovery: After multiple test registrations, the user count would reset to 0 after each deploy. Fix: Build db.py universal adapter + connect Render PostgreSQL + set DATABASE_URL env var. PostgreSQL data persists across deploys.
Bug 8: BMR Test Wrong Expected Values
What happened: Test suite had 3 BMR tests with wrong expected values. The test used age=25 in the input but compared against a value calculated for age=40. Tests failed even though the formula was mathematically correct. Discovery: Manual verification of Mifflin-St Jeor formula confirmed the code output was right. Fix: Test bug, not code bug. Corrected the expected values to match age=25.
The Numbers
Total versions: v1 → v6
Lines of code: ~5,000+ across 30+ files
Test cases written: 103
Real code bugs found by tests: 2
Test bugs found: 6
API calls to build this: Hundreds of Claude conversations
Time to first working prototype: 2 days
Time to production-ready v6: ~3 weeks
Cost of building: ₹0 (Claude + free API tiers)
Cost to run: ₹0 (Render free tier + Gemini free tier)
Monthly API limit: ~15,000 plan generations on free Gemini tier
SWOT of the Final Product
Strengths
Only free tool with Harvard Plate + Eatwell Guide science built in
15 medical conditions adapted (Diabetes, PCOS, Thyroid, Hypertension, Cholesterol...)
13-section questionnaire — no free competitor collects this much health data
Two-mode design: Quick (60s) + Pro (13 sections)
Indian food native — roti, dal, khichdi, sabzi as defaults
Meal swap, food scan, voice log, smart grocery list
Plan cache reduces API costs ~60%
Full admin panel with analytics charts
PostgreSQL + SQLite universal adapter
Weaknesses
Food diary requires manual entry (no calorie search yet)
Password reset shows link on screen (no email delivery yet)
No native mobile app (PWA only)
English only (no Hindi)
Opportunities
Open Food Facts API for food search (free)
SendGrid for email reminders (100/day free)
Razorpay freemium ₹99/month
Hindi language toggle (3× addressable market)
B2B white-label for gyms/hospitals
Threats
HealthifyMe (30M users, ₹40M funded)
Gemini free tier exhausts at ~500 plans/day
SQLite data loss if DATABASE_URL not set
What Vibe Coding Taught Me
1. Describe outcomes, not code. Instead of "write a function that calculates BMR," say "I need to calculate how many calories this person burns at rest, using the scientific Mifflin-St Jeor formula, adjusted for their activity level." The AI produces better code when it understands the goal.
2. The first version is always wrong. Every feature I built went through at least 3 iterations. The AI's first output is 80% right. The remaining 20% comes from testing it, explaining what broke, and asking for fixes.
3. Test aggressively. Ask the AI to write tests for everything. The test suite found 2 real bugs I would never have found manually (age validation, IP fallback). It also exposed 6 test bugs which revealed that my mental model of expected values was wrong.
4. Security is not optional. I didn't plan for rate limiting, password hashing, or SQL injection protection. Claude added all of it automatically while building features. When I reviewed the code, I realised how much security groundwork had been laid that I hadn't explicitly asked for.
5. Deployment is a product feature. The render.yaml, runtime.txt, db.py universal adapter, .env.example, .gitignore — these aren't afterthoughts. They're what makes the difference between "code that runs on my laptop" and "a real product."
6. Every bug teaches you something. The robots.txt bug taught me how search crawler rules work. The psycopg2 crash taught me that Python version compatibility is a real constraint. The admin dashboard bug taught me to never silently swallow errors.
What's Next
SendGrid email integration — proper password reset + welcome emails + daily meal reminders
Open Food Facts API — searchable food database in the diary
Razorpay freemium — ₹99/month for unlimited plans
Hindi language toggle — Flask-Babel i18n for tier-2/3 India
Google Fit integration — real step count instead of manual activity selection
Daily streak gamification — CRED-style retention mechanics
Try The Live Product and let me know how you feel.
Quick mode: /quick — 5 questions, 60 seconds, free plan
Pro mode: /questionnaire — 13 sections, ~5 minutes, most personalised
Admin: /admin/login — username: admin, password: nutriai@admin2025
Built entirely through vibe coding with Claude. No prior Flask experience required. Just a clear vision, fast iteration, and a willingness to learn from every bug.
Built by Shivam Dube · Raipur, Chhattisgarh, India




Comments