# How to Build Recipe app with V0

- Tool: v0
- Difficulty: Beginner
- Compatibility: V0 Free
- Last updated: April 2026

## TL;DR

Build a shareable recipe app with V0 using Next.js and Supabase that lets users store, discover, and share recipes with adjustable serving sizes and ingredient scaling. Features a recipe feed with tag filters, image galleries, and a favorites system — all in about 30-60 minutes.

## Before you start

- A V0 account (free tier works for this project)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A few recipes to add for testing

## Step-by-step guide

### 1. Set up the project and recipe schema

Open V0 and create a new project. Use the Connect panel to add Supabase. Prompt V0 to create the schema for recipes, ingredients, instructions, tags, and favorites.

```
// Paste this prompt into V0's AI chat:
// Build a recipe app. Create a Supabase schema with:
// 1. recipes: id (uuid PK), author_id (uuid FK), title (text), description (text), image_url (text), prep_time_minutes (integer), cook_time_minutes (integer), servings (integer), difficulty (text check easy/medium/hard), cuisine (text), is_published (boolean default false), created_at (timestamptz)
// 2. ingredients: id (uuid PK), recipe_id (uuid FK), name (text), amount (numeric), unit (text), position (integer)
// 3. instructions: id (uuid PK), recipe_id (uuid FK), step_number (integer), description (text), image_url (text nullable)
// 4. tags: id (uuid PK), name (text unique)
// 5. recipe_tags: recipe_id (uuid FK), tag_id (uuid FK), primary key (recipe_id, tag_id)
// 6. favorites: id (uuid PK), user_id (uuid FK), recipe_id (uuid FK), created_at (timestamptz) with unique constraint on (user_id, recipe_id)
// RLS: anyone can SELECT published recipes, only authors can INSERT/UPDATE their own.
// Generate SQL migration and TypeScript types.
```

> Pro tip: Use V0's Design Mode (Option+D) for tweaking the recipe card grid — adjust image heights, spacing, and typography completely free.

**Expected result:** Supabase is connected with all six tables created. RLS policies allow public reading of published recipes while protecting author-only editing.

### 2. Build the recipe feed with search and tag filters

Create the home page showing a grid of recipe cards with search and tag-based filtering. Each card displays the recipe image, title, prep/cook time, difficulty badge, and cuisine tag.

```
// Paste this prompt into V0's AI chat:
// Build a recipe feed at app/page.tsx.
// Requirements:
// - Top section: Input for search by recipe title, ToggleGroup for cuisine filters (All, Italian, Mexican, Asian, American, Indian, Other)
// - Grid of recipe Cards (3 columns desktop, 2 tablet, 1 mobile)
// - Each Card shows:
//   - Recipe image (Skeleton while loading)
//   - Title text
//   - Prep + cook time as "45 min total"
//   - difficulty Badge (easy=green, medium=yellow, hard=red)
//   - Cuisine tag as Badge
//   - Avatar for recipe author
// - Cards link to /recipes/[id]
// - Search filters update URL searchParams for shareable URLs
// - Server Components for data fetching from Supabase
// - Fetch published recipes ordered by created_at descending
```

**Expected result:** A recipe feed with search input, cuisine filter toggles, and a responsive grid of recipe Cards with images, timing info, and difficulty Badges.

### 3. Create the recipe detail page with ingredient scaling

Build the recipe detail page with the key feature — a serving size Slider that dynamically recalculates ingredient amounts. The scaler uses a client component since it needs interactivity.

```
'use client'

import { useState } from 'react'
import { Slider } from '@/components/ui/slider'
import { Card } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'

interface Ingredient {
  name: string
  amount: number
  unit: string
}

export function IngredientScaler({
  ingredients,
  baseServings,
}: {
  ingredients: Ingredient[]
  baseServings: number
}) {
  const [servings, setServings] = useState(baseServings)
  const ratio = servings / baseServings

  return (
    <Card className="p-6">
      <div className="flex items-center justify-between mb-4">
        <h3 className="text-lg font-semibold">Ingredients</h3>
        <span className="text-sm text-muted-foreground">
          {servings} servings
        </span>
      </div>
      <Slider
        value={[servings]}
        onValueChange={([v]) => setServings(v)}
        min={1}
        max={baseServings * 4}
        step={1}
        className="mb-6"
      />
      <ul className="space-y-2">
        {ingredients.map((ing, i) => {
          const scaled = Math.round(ing.amount * ratio * 100) / 100
          return (
            <li key={i} className="flex justify-between text-sm">
              <span>{ing.name}</span>
              <span className="font-medium">
                {scaled} {ing.unit}
              </span>
            </li>
          )
        })}
      </ul>
      <Separator className="mt-4" />
    </Card>
  )
}
```

**Expected result:** An ingredient list with a Slider that adjusts serving size. Moving the Slider recalculates all ingredient amounts proportionally in real time.

### 4. Build the recipe creation form

Create a form where users add recipes with structured ingredients, step-by-step instructions, and image upload. Each ingredient has name, amount, and unit fields. Instructions are numbered steps.

```
// Paste this prompt into V0's AI chat:
// Build a recipe creation form at app/recipes/new/page.tsx.
// Requirements:
// - Protected by auth
// - Form sections in shadcn/ui Cards:
//   - Basic Info: Input for title, Textarea for description, Select for difficulty (easy/medium/hard), Select for cuisine, Input for prep_time and cook_time, Input for servings
//   - Image: file upload zone with preview, uploads to Supabase Storage 'recipe-images' bucket
//   - Ingredients: dynamic list — each row has Input for name, Input type=number for amount, Select for unit (cup/tbsp/tsp/oz/g/lb/ml/whole). Add/Remove Buttons.
//   - Instructions: dynamic numbered list — each step has Textarea for description. Add/Remove Buttons with reorder.
//   - Tags: ToggleGroup for common tags (breakfast/lunch/dinner/dessert/snack/vegetarian/vegan/gluten-free)
// - Submit via Server Action createRecipe() that inserts recipe, ingredients, instructions, and tags
// - After creation, redirect to the new recipe page
// - 'use client' for the interactive form
```

> Pro tip: Queue this prompt right after the schema prompt using V0's prompt queuing. The form will be ready by the time you finish reviewing the schema.

**Expected result:** A multi-section recipe creation form with dynamic ingredient rows, numbered instruction steps, image upload, and tag selection. Submitting creates the full recipe.

## Complete code example

File: `app/actions/recipes.ts`

```typescript
'use server'

import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createRecipe(formData: FormData) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()

  const { data: recipe } = await supabase
    .from('recipes')
    .insert({
      author_id: user?.id,
      title: formData.get('title') as string,
      description: formData.get('description') as string,
      prep_time_minutes: parseInt(formData.get('prep_time') as string),
      cook_time_minutes: parseInt(formData.get('cook_time') as string),
      servings: parseInt(formData.get('servings') as string),
      difficulty: formData.get('difficulty') as string,
      cuisine: formData.get('cuisine') as string,
      image_url: formData.get('image_url') as string,
      is_published: true,
    })
    .select()
    .single()

  if (!recipe) throw new Error('Failed to create recipe')

  const ingredients = JSON.parse(formData.get('ingredients') as string)
  await supabase.from('ingredients').insert(
    ingredients.map((ing: any, i: number) => ({
      recipe_id: recipe.id,
      name: ing.name,
      amount: ing.amount,
      unit: ing.unit,
      position: i + 1,
    }))
  )

  const steps = JSON.parse(formData.get('instructions') as string)
  await supabase.from('instructions').insert(
    steps.map((step: string, i: number) => ({
      recipe_id: recipe.id,
      step_number: i + 1,
      description: step,
    }))
  )

  revalidatePath('/')
  redirect(`/recipes/${recipe.id}`)
}

export async function toggleFavorite(recipeId: string) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return

  const { data: existing } = await supabase
    .from('favorites')
    .select('id')
    .eq('user_id', user.id)
    .eq('recipe_id', recipeId)
    .single()

  if (existing) {
    await supabase.from('favorites').delete().eq('id', existing.id)
  } else {
    await supabase.from('favorites').insert({
      user_id: user.id,
      recipe_id: recipeId,
    })
  }

  revalidatePath(`/recipes/${recipeId}`)
  revalidatePath('/favorites')
}
```

## Common mistakes

- **Putting the ingredient Slider in a Server Component** — The Slider component uses useState for the serving value. Server Components cannot use React hooks or handle user interactions. Fix: Mark the ingredient scaler component with 'use client'. Pass the base ingredients and servings as props from the Server Component parent.
- **Using floating-point math for ingredient scaling without rounding** — Scaling 1/3 cup by 2x gives 0.6666666667 — displaying raw floating-point values looks messy and confusing to users. Fix: Round scaled amounts to 2 decimal places: Math.round(amount * ratio * 100) / 100. For common fractions, consider a fraction display library.
- **Not setting Supabase Storage bucket to public for recipe images** — Private buckets require signed URLs that expire. Recipe images need permanent public URLs for sharing and SEO. Fix: Create a public bucket named recipe-images in Supabase Storage Dashboard. Store the public URL directly in the image_url field.

## Best practices

- Use Server Components for the recipe feed and detail pages to optimize SEO and page load speed
- Use V0's Design Mode (Option+D) to visually adjust recipe Card image heights and grid spacing for free
- Store recipe images in a Supabase Storage public bucket for permanent, shareable URLs
- Use Server Actions for all mutations (creating recipes, toggling favorites) — no API routes needed
- Keep the ingredient Slider in a 'use client' component and calculate scaling client-side for instant response
- Use URL searchParams for search and filter state so recipe searches are shareable and bookmarkable

## Frequently asked questions

### Can I build this recipe app on V0's free tier?

Yes. The free tier gives you enough credits to generate the recipe feed, detail page, and creation form. Supabase free tier handles the database and image storage.

### How does the ingredient scaling work?

When you move the Slider to change serving size, a client-side component multiplies each ingredient amount by the ratio (desiredServings / baseServings). The calculation happens instantly in the browser with no server round-trip.

### Where are recipe images stored?

Recipe images upload to a Supabase Storage public bucket. The public URL is stored in the recipes.image_url field. Public buckets serve images directly without expiring signed URLs.

### Can users share recipes without signing up?

Anyone can browse and view published recipes without an account. Creating recipes and saving favorites requires authentication. Connect Supabase Auth via the Connect panel for sign-up/sign-in.

### How do I deploy the recipe app?

Click Share then Publish to Production in V0. Supabase credentials are auto-configured from the Connect panel. Your recipe pages are server-rendered for fast loading and good SEO.

### Can RapidDev help build a custom recipe or food platform?

Yes. RapidDev has built 600+ apps including food platforms with meal planning, nutritional analysis, and subscription meal kits. Book a free consultation to discuss your idea.

---

Source: https://www.rapidevelopers.com/how-to-build-v0/recipe-app
© RapidDev — https://www.rapidevelopers.com/how-to-build-v0/recipe-app
