What We're Building
In this guide, you will build an AI-powered workout generator that creates personalized exercise plans. Users input their fitness level, goals, and available equipment, and Claude generates tailored workouts complete with exercises, sets, reps, and rest periods.
By the end of this guide, you will have a fully functional fitness app with:
- User profile form capturing fitness level, goals, and equipment
- AI-generated personalized workout plans
- Workout display with exercises, sets, reps, and instructions
- Database storage for user profiles and workout history
- Progress tracking and workout completion logging
- Favorite workouts feature for quick access
This project demonstrates how AI can replace complex rule-based fitness algorithms with natural language understanding - creating workouts that feel like they were designed by a personal trainer rather than generated by a formula.
Prerequisites
Before starting this guide, make sure you have the following:
- Basic JavaScript/React knowledge - Understanding of components and state management
- Node.js 18+ installed locally - Run
node -vto check your version - A code editor - VS Code or Cursor recommended
You will also need free accounts for the following services:
- Supabase (supabase.com) - For database and authentication
- Vercel (vercel.com) - For deployment
- Anthropic (console.anthropic.com) - For Claude API access
Tech Stack Specification
Here is the technology stack we will use for this build, with reasoning for each choice:
| Layer | Technology | Why This Choice |
|---|---|---|
| Frontend | Next.js 14, Tailwind CSS | Quick styling with utility classes, server components for fast initial load |
| Backend | Next.js API Routes | Simple serverless functions, no separate backend needed, handles Claude API securely |
| Database | Supabase PostgreSQL | Stores user profiles, workout history, and favorites with real-time sync |
| AI | Claude API | Excellent at generating structured workout data with proper exercise form cues |
| Hosting | Vercel | Zero-config Next.js deployment, automatic HTTPS, easy environment variables |
AI Agent Workflow
Modern AI-assisted development works best when you use the right tool for each task. Here is how to leverage different AI tools throughout this build:
Project Scaffolding with Claude Code
Use Claude Code (Anthropic's CLI tool) for the initial project setup and implementing the core AI workout generation logic. Claude Code understands the full context of your project.
# Example prompt for Claude Code:
Create a Next.js 14 app with Tailwind CSS for an AI workout generator.
Include:
- A user profile form component with fitness level, goals, and equipment
- Supabase client setup for database operations
- An API route that calls Claude to generate workouts
- Types for UserProfile, Workout, and Exercise entities
- A workout display component showing exercises with sets/reps
Use the App Router and organize with /app, /components, /lib folders.
UI Generation with v0.dev
Use v0.dev for creating polished UI components quickly. Recommended prompts for this project:
- "Fitness profile form with sliders for fitness level, checkboxes for equipment"
- "Workout card showing exercise name, sets, reps, and rest time"
- "Progress tracker with completed workouts chart"
When building the workout display, include clear visual hierarchy - exercise names should be prominent, with sets/reps and instructions progressively less emphasized. This mimics how real workout apps guide users through their session.
Development with Cursor
Use Cursor for day-to-day development. It excels at debugging issues with your workout generation prompt and refining the UI based on user feedback.
Step-by-Step Build Guide
Phase 1: Project Setup
Start by creating a new Next.js project with the required dependencies:
# Create Next.js project
npx create-next-app@latest workout-generator --typescript --tailwind --eslint --app
# Navigate to project
cd workout-generator
# Install dependencies
npm install @supabase/supabase-js @anthropic-ai/sdk
Create a .env.local file in your project root:
# Supabase
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
# Anthropic (server-side only)
ANTHROPIC_API_KEY=your_anthropic_api_key
Phase 2: Database Schema
Create the database tables in Supabase. Go to the SQL Editor and run:
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- User profiles table
CREATE TABLE user_profiles (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
fitness_level TEXT NOT NULL CHECK (fitness_level IN ('beginner', 'intermediate', 'advanced')),
goals TEXT[] NOT NULL,
equipment TEXT[] NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Workouts table
CREATE TABLE workouts (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
exercises JSONB NOT NULL,
duration_minutes INTEGER,
is_favorite BOOLEAN DEFAULT false,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Enable Row Level Security
ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE workouts ENABLE ROW LEVEL SECURITY;
-- RLS Policies
CREATE POLICY "Users can manage own profile"
ON user_profiles FOR ALL
USING (auth.uid() = user_id);
CREATE POLICY "Users can manage own workouts"
ON workouts FOR ALL
USING (auth.uid() = user_id);
Phase 3: Claude Prompt Engineering
This is the core of the AI workout generator. Here is the API route that generates personalized workouts:
import { NextRequest, NextResponse } from 'next/server'
import Anthropic from '@anthropic-ai/sdk'
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
})
interface UserProfile {
fitnessLevel: 'beginner' | 'intermediate' | 'advanced'
goals: string[]
equipment: string[]
}
export async function POST(request: NextRequest) {
try {
const { profile }: { profile: UserProfile } = await request.json()
const prompt = `You are a certified personal trainer creating a workout plan.
User Profile:
- Fitness Level: ${profile.fitnessLevel}
- Goals: ${profile.goals.join(', ')}
- Available Equipment: ${profile.equipment.join(', ') || 'bodyweight only'}
Create a complete workout with 5-8 exercises. For each exercise include:
1. Exercise name
2. Number of sets (2-4 based on fitness level)
3. Number of reps or duration
4. Rest period between sets
5. Brief form instructions (1-2 sentences)
6. Muscle groups targeted
Respond ONLY with a valid JSON object in this exact format:
{
"name": "Workout name based on goals",
"description": "Brief description of the workout focus",
"duration_minutes": estimated total time,
"exercises": [
{
"name": "Exercise Name",
"sets": 3,
"reps": "10-12",
"rest_seconds": 60,
"instructions": "Brief form cue",
"muscles": ["muscle1", "muscle2"]
}
]
}
Adjust difficulty appropriately:
- Beginner: Focus on form, lower reps, longer rest
- Intermediate: Moderate intensity, compound movements
- Advanced: Higher volume, shorter rest, complex exercises`
const response = await anthropic.messages.create({
model: 'claude-3-haiku-20240307',
max_tokens: 2048,
messages: [{ role: 'user', content: prompt }],
})
const content = response.content[0]
if (content.type === 'text') {
const workout = JSON.parse(content.text)
return NextResponse.json({ workout })
}
return NextResponse.json({ error: 'Invalid response' }, { status: 500 })
} catch (error) {
console.error('Workout generation error:', error)
return NextResponse.json({ error: 'Failed to generate workout' }, { status: 500 })
}
}
The key to reliable workout generation is being specific about the output format. By providing an exact JSON structure, Claude will consistently return parseable data. Include fitness level adjustments in the prompt to ensure appropriate difficulty.
Phase 4: Workout Display Component
Create a component to display the generated workout in a user-friendly format:
'use client'
import { useState } from 'react'
interface Exercise {
name: string
sets: number
reps: string
rest_seconds: number
instructions: string
muscles: string[]
}
interface Workout {
name: string
description: string
duration_minutes: number
exercises: Exercise[]
}
export function WorkoutDisplay({ workout }: { workout: Workout }) {
const [completedExercises, setCompletedExercises] = useState<Set<number>>(new Set())
const toggleExercise = (index: number) => {
const newCompleted = new Set(completedExercises)
if (newCompleted.has(index)) {
newCompleted.delete(index)
} else {
newCompleted.add(index)
}
setCompletedExercises(newCompleted)
}
return (
<div className="bg-zinc-900 rounded-xl border border-zinc-800 overflow-hidden">
<div className="p-6 border-b border-zinc-800">
<h2 className="text-2xl font-bold text-green-400">{workout.name}</h2>
<p className="text-zinc-400 mt-2">{workout.description}</p>
<div className="flex gap-4 mt-4 text-sm">
<span className="text-zinc-500">
{workout.duration_minutes} min
</span>
<span className="text-zinc-500">
{workout.exercises.length} exercises
</span>
</div>
</div>
<div className="divide-y divide-zinc-800">
{workout.exercises.map((exercise, index) => (
<div
key={index}
className={`p-6 transition-colors cursor-pointer ${
completedExercises.has(index) ? 'bg-green-900/20' : 'hover:bg-zinc-800/50'
}`}
onClick={() => toggleExercise(index)}
>
<div className="flex justify-between items-start">
<div>
<h3 className="font-semibold text-lg">{exercise.name}</h3>
<p className="text-green-400 mt-1">
{exercise.sets} sets x {exercise.reps} reps
</p>
<p className="text-zinc-500 text-sm mt-1">
Rest: {exercise.rest_seconds}s
</p>
</div>
<div className="flex flex-wrap gap-2">
{exercise.muscles.map((muscle) => (
<span
key={muscle}
className="px-2 py-1 bg-zinc-800 rounded text-xs text-zinc-400"
>
{muscle}
</span>
))}
</div>
</div>
<p className="text-zinc-400 text-sm mt-3">{exercise.instructions}</p>
</div>
))}
</div>
</div>
)
}
Phase 5: Progress Tracking
Add functionality to track completed workouts and save them to the database:
import { supabase } from './supabase/client'
export async function saveWorkout(workout: any) {
const { data, error } = await supabase
.from('workouts')
.insert({
name: workout.name,
description: workout.description,
exercises: workout.exercises,
duration_minutes: workout.duration_minutes,
})
.select()
.single()
if (error) throw error
return data
}
export async function completeWorkout(workoutId: string) {
const { error } = await supabase
.from('workouts')
.update({ completed_at: new Date().toISOString() })
.eq('id', workoutId)
if (error) throw error
}
export async function toggleFavorite(workoutId: string, isFavorite: boolean) {
const { error } = await supabase
.from('workouts')
.update({ is_favorite: isFavorite })
.eq('id', workoutId)
if (error) throw error
}
export async function getWorkoutHistory() {
const { data, error } = await supabase
.from('workouts')
.select('*')
.order('created_at', { ascending: false })
.limit(20)
if (error) throw error
return data
}
Phase 6: Workout History and Favorites
Create a component to display past workouts and favorites:
'use client'
import { useState, useEffect } from 'react'
import { getWorkoutHistory, toggleFavorite } from '@/lib/workouts'
export function WorkoutHistory() {
const [workouts, setWorkouts] = useState<any[]>([])
const [filter, setFilter] = useState<'all' | 'favorites'>('all')
useEffect(() => {
getWorkoutHistory().then(setWorkouts)
}, [])
const filtered = filter === 'favorites'
? workouts.filter(w => w.is_favorite)
: workouts
return (
<div className="space-y-4">
<div className="flex gap-2">
<button
onClick={() => setFilter('all')}
className={`px-4 py-2 rounded-lg ${
filter === 'all' ? 'bg-green-600' : 'bg-zinc-800'
}`}
>
All Workouts
</button>
<button
onClick={() => setFilter('favorites')}
className={`px-4 py-2 rounded-lg ${
filter === 'favorites' ? 'bg-green-600' : 'bg-zinc-800'
}`}
>
Favorites
</button>
</div>
{filtered.map((workout) => (
<div key={workout.id} className="p-4 bg-zinc-900 rounded-lg border border-zinc-800">
<div className="flex justify-between items-center">
<div>
<h3 className="font-semibold">{workout.name}</h3>
<p className="text-sm text-zinc-400">
{new Date(workout.created_at).toLocaleDateString()}
</p>
</div>
<button
onClick={() => {
toggleFavorite(workout.id, !workout.is_favorite)
setWorkouts(workouts.map(w =>
w.id === workout.id ? { ...w, is_favorite: !w.is_favorite } : w
))
}}
className={workout.is_favorite ? 'text-yellow-400' : 'text-zinc-600'}
>
★
</button>
</div>
</div>
))}
</div>
)
}
Common Issues and Solutions
Here are the most common issues developers encounter when building this project:
Sometimes Claude includes markdown formatting around JSON. Add a fallback to strip code blocks: text.replace(/```json\n?|\n?```/g, '') before parsing.
If Claude generates too few or too many exercises, be more explicit in your prompt: "Create exactly 6 exercises" rather than "5-8 exercises".
Ensure your prompt explicitly states to only use the listed equipment. Add: "ONLY use exercises that require the equipment listed above. If no equipment is listed, use bodyweight exercises only."
Next Steps
You now have a functional AI workout generator. Here are ways to extend it further:
- Add Exercise Videos - Integrate exercise demonstration videos or GIFs for each movement
- Progressive Overload - Track weights used and suggest increases over time
- Weekly Programming - Generate full week workout splits with proper muscle group rotation
- Rest Day Suggestions - Use AI to recommend recovery based on workout intensity
- Social Features - Share workouts with friends or join challenges
Follow the Vibe Coding Enthusiast
Follow JD — product updates on LinkedIn, personal takes on X.