top of page
Search

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.



Shivam D vibe codes Nutri AI 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



Architectural flow for Nutri AI Diet planner.



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


bottom of page