Beginner ~3 hours

Build an AI Workout Generator

Create a personalized workout generator that builds custom exercise plans based on fitness level, goals, and available equipment using Claude's AI capabilities.

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 -v to check your version
  • A code editor - VS Code or Cursor recommended

You will also need free accounts for the following services:

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"
Pro Tip

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:

Terminal
# 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:

.env.local
# 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:

schema.sql
-- 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:

src/app/api/generate-workout/route.ts
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 })
  }
}
Prompt Engineering Tips

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:

src/components/WorkoutDisplay.tsx
'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:

src/lib/workouts.ts
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:

src/components/WorkoutHistory.tsx
'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:

JSON Parsing Errors from Claude

Sometimes Claude includes markdown formatting around JSON. Add a fallback to strip code blocks: text.replace(/```json\n?|\n?```/g, '') before parsing.

Inconsistent Exercise Counts

If Claude generates too few or too many exercises, be more explicit in your prompt: "Create exactly 6 exercises" rather than "5-8 exercises".

Equipment Mismatch

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