Fantasy Football Draft Assistant
Build a full-stack AI app that recommends fantasy football picks.
Introduction
30 Second Summary
Fantasy football draft day is high-stakes. One wrong pick and your season is toast. What if you had an AI analyst whispering the best available pick in your ear based on your exact roster and scoring format?
In this project, you will build a full-stack web app that takes your fantasy football draft context and returns AI-powered pick recommendations from Claude. You'll wire up a React frontend to a Node.js backend that calls the Anthropic API and parses structured JSON responses.
What You'll Build
You'll type in your current roster, browse a pre-loaded pool of available players, hit "Get Recommendation," and watch Claude analyze your positional needs to suggest the perfect draft pick with detailed reasoning and sleeper picks.
By the end of this project, you'll have:
- A live draft form where you enter your roster and available players, then get an instant AI recommendation explaining who to pick, why, and which sleepers are undervalued.
- A backend API that crafts structured prompts and validates Claude's JSON responses so the frontend always receives clean, reliable draft advice.
- A polished user experience with loading spinners during the API call and clear error messages when something goes wrong.
- Secret Mission: AI-estimated confidence levels and ranked alternative picks so you can weigh your options before committing.
Are there any prerequisites?
You'll need Node.js (version 20 or higher) installed on your machine, VS Code as your editor, and an Anthropic API key.
No prior experience with AI APIs or TypeScript is required. The project walks you through everything from scratch.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters.
You're going to build a full-stack Fantasy Football Draft Assistant that takes your current roster, available players, and league scoring format, then calls the Claude API to return structured, reasoned draft recommendations.
Set Up the Project
Your Draft Assistant has two parts: a React frontend where users enter their draft context, and an Express backend that talks to Claude.
In this step, you'll scaffold both projects in side-by-side folders so each can run independently. By the end, you'll have a React app serving a page in your browser and an Express server responding on its own port.
In this step, get ready to:
- Scaffold the React frontend with Vite and verify it runs.
- Create the Express backend with TypeScript and install dependencies.
- Add a dev script to the server and confirm it starts on port 3001.
Scaffold the React frontend
Vite is a build tool that scaffolds a ready-to-run React project with TypeScript in seconds. You'll use it to create the client/ folder.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), then type Terminal and press Enter.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir draft-assistant
cd draft-assistant
- Scaffold the React frontend by running this command:
npm create vite@latest client -- --template react-ts
What does this command do?
npm create vite@latest downloads and runs the Vite project scaffolder.
client is the folder name it creates. The --template react-ts flag tells Vite to use React with TypeScript, skipping the template selection prompt.
💡 You may see a prompt
Vite may ask "Install with npm and start now?" after scaffolding. Choose No so you can install and start manually in the next commands.
- Move into the new client folder and install its dependencies by running:
cd client
npm install
- Before you start the dev server, what do you think you'll see in the browser when a brand-new Vite React project runs?
- Start the development server by running:
npm run dev
Open http://localhost:5173 in your browser. You should see the Vite + React starter page with spinning logos and a counter button.
Don't see the page?
- Make sure npm install finished without errors before running npm run dev.
- Check that nothing else is already running on port 5173. If so, stop the other process or Vite will pick a different port shown in the terminal output.
- help me debug why my Vite dev server isn't showing the starter page
- Stop the dev server by pressing Ctrl+C in the terminal. You'll restart it later.
Create the Express backend project
The server needs its own folder with its own dependencies. You'll install Express for the web server, the Anthropic SDK for talking to Claude, and a few TypeScript utilities.
- Navigate back to the parent draft-assistant folder by running:
cd ..
- Create the server folder and initialize a new Node.js project inside it by running:
mkdir server
cd server
npm init -y
You should see a confirmation that package.json was created.
- Install the runtime dependencies by running:
npm install express cors @anthropic-ai/sdk dotenv
What are these packages?
- express is the web server framework that handles HTTP requests.
- cors allows the React app on port 5173 to talk to the server on port 3001.
- @anthropic-ai/sdk is the official SDK for calling Claude.
- dotenv loads your API key from a .env file so you never hardcode secrets.
- Install the development dependencies by running:
npm install -D typescript ts-node nodemon @types/express @types/cors @types/node
Why so many dev packages?
typescript and ts-node let you run TypeScript files directly without a separate compile step. nodemon auto-restarts the server whenever you save a file. The @types/ packages give TypeScript the type definitions for Express, cors, and Node.js.
- Create the .env file that holds your API key by running:
echo "ANTHROPIC_API_KEY=your-api-key-here" > .env
- Create the .env.example template file by running:
echo "ANTHROPIC_API_KEY=your-api-key-here" > .env.example
- Open the .env file in VS Code. Replace your-api-key-here with your actual Anthropic API key, then save the file.
Keep your key safe
The .env file holds your real secret. The .env.example file is a safe template you could share with others to show them what variables the project needs.
- Confirm the server folder has everything by running:
ls -a
You should see .env, .env.example, node_modules, package-lock.json, and package.json listed in the output.
Add a dev script and start the server
The server needs a TypeScript config so ts-node understands your imports, a startup file, and a dev script that auto-restarts on changes.
- Open the draft-assistant folder in VS Code by pressing Cmd+Space (macOS) or the Windows key (Windows), typing VS Code, and pressing Enter.
- Click File in the top menu bar.
- Click Open Folder.
- Select the draft-assistant folder on your Desktop, then click Open.
- In the Explorer sidebar, expand the server folder.
- Create a new file called tsconfig.json inside the server folder by right-clicking server and selecting New File.
- Add the following TypeScript configuration:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
What does this config do?
esModuleInterop lets you write import express from "express" instead of clunky namespace imports. rootDir tells TypeScript your source code lives in the src/ folder.
- Open server/package.json in VS Code.
- Find the "scripts" section. It currently looks like this:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
- Replace the entire "scripts" block with:
"scripts": {
"dev": "nodemon --exec ts-node src/index.ts"
}
- Save package.json.
- Create a src folder inside server by right-clicking server in the Explorer and selecting New Folder.
- Create a new file called index.ts inside server/src and add the following code:
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
// Load environment variables from .env file
dotenv.config();
// Exit early if the API key is missing
if (!process.env.ANTHROPIC_API_KEY) {
console.error("ERROR: ANTHROPIC_API_KEY is not set in your .env file.");
process.exit(1);
}
const app = express();
const PORT = 3001;
// Enable cross-origin requests from the React app
app.use(cors());
// Parse incoming JSON request bodies
app.use(express.json());
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
What does this code do?
- dotenv.config() loads your .env file so process.env.ANTHROPIC_API_KEY is available.
- The if (!process.env.ANTHROPIC_API_KEY) check stops the server immediately with a clear error if you forgot to add your key.
- cors() allows the React frontend on port 5173 to make requests to this server on port 3001.
- app.listen starts the server and prints the URL so you know it's running.
- Save index.ts.
- What do you think will print in the terminal when the server starts successfully?
- Open the VS Code integrated terminal by pressing Ctrl+` (backtick key, both macOS and Windows).
- Navigate into the server folder and start it by running:
cd server
npm run dev
You should see Server running on http://localhost:3001 printed in the terminal. Nodemon is now watching for file changes and will auto-restart whenever you save.
Server won't start?
- If you see "ERROR: ANTHROPIC_API_KEY is not set," open server/.env and confirm your key is on the line after the = sign with no extra spaces.
- If you see "Cannot find module," make sure you ran npm install inside the server folder and that node_modules exists.
- If you see a TypeScript error about imports, confirm tsconfig.json exists in the server folder with "esModuleInterop": true.
- help me debug why my Express server won't start with ts-node
✔️ Awesome, I've got everything!
Great. Double check you saved both tsconfig.json and src/index.ts in the server folder.
ⓧ I'd like to double check the full code
Here is the full server/src/index.ts file at this point:
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
// Load environment variables from .env file
dotenv.config();
// Exit early if the API key is missing
if (!process.env.ANTHROPIC_API_KEY) {
console.error("ERROR: ANTHROPIC_API_KEY is not set in your .env file.");
process.exit(1);
}
const app = express();
const PORT = 3001;
// Enable cross-origin requests from the React app
app.use(cors());
// Parse incoming JSON request bodies
app.use(express.json());
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
And your server/tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Both halves of your app are ready. The React frontend scaffolded with Vite serves on port 5173, and the Express backend starts on port 3001. Next up, you'll build the draft input form that collects your fantasy roster data.
Build the Draft Input Form
Your backend is ready to receive requests, but you have no way to send data to it yet. The app needs a form where users enter their draft context: available players, their current roster, and the league scoring format.
In this step, you will build a React form component with pre-populated player data, input validation, and a temporary JSON preview to confirm data flows correctly before any backend is connected.
In this step, get ready to:
- Create shared TypeScript interfaces for draft input and recommendation data.
- Build a DraftForm component with pre-populated fields and input validation.
- Wire up the form in App.tsx and display submitted data as a JSON preview.
Define the shared type interfaces
Before building the form, you need TypeScript interfaces that define the shape of data flowing between the frontend and backend. Both sides will reference these same types to stay in sync.
- Create a new file called types.ts inside client/src/ by pasting the following code:
export interface DraftInput {
availablePlayers: string[];
currentRoster: string[];
leagueType: "PPR" | "Half-PPR" | "Standard";
}
export interface RecommendationResponse {
bestPick: string;
reasoning: string;
positionalNeeds: string[];
sleepers: string[];
}
What does this code do?
DraftInput defines what the form collects: a list of available players, the user's current roster, and the league scoring format.
RecommendationResponse defines the shape of data that will eventually come back from the AI. You define it now so the frontend knows what to expect later.
- Save types.ts.
Build the DraftForm component
The form needs three inputs: a textarea pre-loaded with realistic player names, a textarea for the user's current roster, and a dropdown for the league scoring format. You will also add validation to make sure the user provides enough data before submitting.
- Create a components folder inside client/src/ by running this command from the client folder:
mkdir src/components
- Create a new file called DraftForm.tsx inside client/src/components/.
- Start by pasting the imports, default player data, and component props at the top of the file:
import { useState } from "react";
import type { DraftInput } from "../types";
const DEFAULT_AVAILABLE_PLAYERS = `Ja'Marr Chase, WR
Derrick Henry, RB
Travis Kelce, TE
Amon-Ra St. Brown, WR
Saquon Barkley, RB
Garrett Wilson, WR
Josh Allen, QB
Davante Adams, WR
Bijan Robinson, RB
Mark Andrews, TE
Chris Olave, WR
De'Von Achane, RB`;
interface DraftFormProps {
onSubmit: (input: DraftInput) => void;
isLoading: boolean;
}
What does this code do?
DEFAULT_AVAILABLE_PLAYERS is a template string containing 12 real NFL players with their positions. This pre-populates the form so the user can immediately test without typing from scratch.
DraftFormProps defines the two props the component receives: an onSubmit callback that fires with validated data, and isLoading to disable the button while a request is in flight.
- Below the props interface, paste the component function with state and validation logic:
export function DraftForm({ onSubmit, isLoading }: DraftFormProps) {
const [availablePlayers, setAvailablePlayers] = useState(
DEFAULT_AVAILABLE_PLAYERS
);
const [currentRoster, setCurrentRoster] = useState("");
const [leagueType, setLeagueType] = useState<DraftInput["leagueType"]>("PPR");
const [validationError, setValidationError] = useState<string | null>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setValidationError(null);
const players = availablePlayers
.split("\n")
.map((p) => p.trim())
.filter((p) => p.length > 0);
const roster = currentRoster
.split("\n")
.map((p) => p.trim())
.filter((p) => p.length > 0);
if (players.length < 2) {
setValidationError("Please enter at least 2 available players.");
return;
}
if (roster.length < 1) {
setValidationError(
"Please enter at least 1 player on your current roster."
);
return;
}
onSubmit({ availablePlayers: players, currentRoster: roster, leagueType });
};
How does the validation work?
The handleSubmit function splits each textarea into lines, trims whitespace, and filters out blank lines. It then checks that at least 2 available players and 1 roster player are provided.
If validation fails, validationError state updates and the form shows a red error message. If it passes, the parsed arrays are sent to the parent via onSubmit.
- Below the handleSubmit function (still inside the component), paste the first half of the form JSX with the two textarea fields:
return (
<form onSubmit={handleSubmit} style={{ marginBottom: "2rem" }}>
<div style={{ marginBottom: "1rem" }}>
<label htmlFor="availablePlayers">
<strong>Available Players</strong> (one per line, format: Name,
Position)
</label>
<textarea
id="availablePlayers"
value={availablePlayers}
onChange={(e) => setAvailablePlayers(e.target.value)}
rows={8}
style={{ width: "100%", marginTop: "0.5rem", fontFamily: "monospace" }}
/>
</div>
<div style={{ marginBottom: "1rem" }}>
<label htmlFor="currentRoster">
<strong>Your Current Roster</strong> (one per line, format: Name,
Position)
</label>
<textarea
id="currentRoster"
value={currentRoster}
onChange={(e) => setCurrentRoster(e.target.value)}
rows={4}
placeholder={"Patrick Mahomes, QB\nTyreek Hill, WR"}
style={{ width: "100%", marginTop: "0.5rem", fontFamily: "monospace" }}
/>
</div>
What do the textareas do?
The first textarea is pre-filled with 12 players from DEFAULT_AVAILABLE_PLAYERS. Users can edit this list freely. The second textarea starts empty with a placeholder showing the expected format.
- Directly below (still inside the <form> tag), paste the league type dropdown, validation error display, and submit button to close out the component:
<div style={{ marginBottom: "1rem" }}>
<label htmlFor="leagueType">
<strong>League Scoring Format</strong>
</label>
<select
id="leagueType"
value={leagueType}
onChange={(e) =>
setLeagueType(e.target.value as DraftInput["leagueType"])
}
style={{ display: "block", marginTop: "0.5rem", padding: "0.5rem" }}
>
<option value="PPR">PPR (Points Per Reception)</option>
<option value="Half-PPR">Half-PPR</option>
<option value="Standard">Standard</option>
</select>
</div>
{validationError && (
<p style={{ color: "red", marginBottom: "1rem" }}>{validationError}</p>
)}
<button
type="submit"
disabled={isLoading}
style={{
padding: "0.75rem 1.5rem",
fontSize: "1rem",
cursor: isLoading ? "not-allowed" : "pointer",
}}
>
{isLoading ? "Analyzing..." : "Get Recommendation"}
</button>
</form>
);
}
What does the rest of the form do?
The <select> dropdown lets the user pick between PPR, Half-PPR, and Standard scoring. Each format changes how players are valued.
The validationError paragraph only renders when there is an error. The button shows "Analyzing..." and becomes disabled when isLoading is true, preventing double submissions.
- Save DraftForm.tsx.
Wire up the form and display a JSON preview
The component exists but nothing renders it yet. You need to import DraftForm into App.tsx and temporarily show the submitted data as JSON so you can confirm the form works before connecting the backend.
- Open client/src/App.tsx and replace all of its content with the following:
import { useState } from "react";
import { DraftForm } from "./components/DraftForm";
import type { DraftInput } from "./types";
function App() {
// Temporarily store submitted data to preview it
const [submitted, setSubmitted] = useState<DraftInput | null>(null);
const handleSubmit = (input: DraftInput) => {
setSubmitted(input);
};
return (
<div style={{ maxWidth: "800px", margin: "0 auto", padding: "2rem" }}>
<h1>Fantasy Football Draft Assistant</h1>
<p>Enter your draft context and get AI-powered pick recommendations.</p>
<DraftForm onSubmit={handleSubmit} isLoading={false} />
{submitted && (
<pre style={{ marginTop: "1.5rem", background: "#f5f5f5", padding: "1rem", borderRadius: "8px" }}>
{JSON.stringify(submitted, null, 2)}
</pre>
)}
</div>
);
}
export default App;
What does this code do?
App.tsx renders the DraftForm and stores whatever the form submits in submitted state. When data exists, it displays it as formatted JSON in a <pre> tag below the form.
This is a temporary preview you will replace with the actual AI recommendation display in a later step.
- Save App.tsx.
Before you check the browser, what do you expect to see at http://localhost:5173? Think about what the form should look like with the pre-populated data.
- Switch to your browser and refresh http://localhost:5173.
You should see a heading that says "Fantasy Football Draft Assistant", a textarea pre-filled with 12 player names, an empty "Your Current Roster" textarea with placeholder text, and a league scoring dropdown defaulting to PPR.
- Type Patrick Mahomes, QB in the **Your Current Roster** textarea.
- Click the **Get Recommendation** button.
A JSON block appears below the form showing your parsed data: availablePlayers as an array of 12 strings, currentRoster with your entry, and leagueType set to "PPR".
Don't see the form?
Make sure your Vite dev server is still running in the client folder. If it stopped, run npm run dev again.
If you see a compile error, check that client/src/types.ts exists and that the import path in DraftForm.tsx uses "../types" (with two dots, since the component is one folder deeper).
help me debug my DraftForm not rendering
- Now test the validation: clear the **Your Current Roster** textarea so it is completely empty.
- Click **Get Recommendation** again.
You should see a red error message: "Please enter at least 1 player on your current roster." The JSON preview does not appear because the form blocked submission.
✔️ Awesome, I've got everything!
Great. Double check you have saved both DraftForm.tsx and App.tsx.
ⓧ I'd like to double check the full code
Scroll up and confirm you have all four chunks in DraftForm.tsx in order: imports and constants, the component function with state and handleSubmit, the two textarea fields, and the dropdown with button. Then confirm App.tsx and types.ts match the code blocks above.
Your form collects data, validates it, and confirms the output shape matches what the backend will expect. Next, you will build the Express endpoint that receives this data and returns a recommendation.
Wire Up the Express Backend
Your form collects valid draft data and displays it as a JSON preview. That's useful for debugging, but the data has nowhere to go.
In this step, you'll build the Express API endpoint that receives draft context and validates it. You'll connect the frontend to the backend so data flows end-to-end. The endpoint returns a placeholder response for now, which exposes a clear gap: the backend accepts data but has no intelligence.
In this step, get ready to:
- Create an Express server with input validation and a placeholder response.
- Connect the React form to the backend API.
- See the placeholder response appear in the browser after submitting the form.
Create the Express server with validation
The server needs to validate incoming draft data before doing anything with it. Invalid requests should be rejected with clear error messages so the frontend can display helpful feedback.
- Create the file server/src/index.ts by running this command from your server folder:
mkdir src
touch src/index.ts
- Confirm the file exists by running:
ls src
You should see index.ts in the output.
- Open server/src/index.ts in VS Code and add the server setup code:
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
// Load environment variables from .env file
dotenv.config();
// Exit immediately if the API key is missing
if (!process.env.ANTHROPIC_API_KEY) {
console.error("ERROR: ANTHROPIC_API_KEY is not set in your .env file.");
process.exit(1);
}
const app = express();
const PORT = 3001;
// Enable cross-origin requests from the React dev server
app.use(cors());
app.use(express.json());
// Shape of incoming draft requests
interface DraftRequest {
availablePlayers: string[];
currentRoster: string[];
leagueType: "PPR" | "Half-PPR" | "Standard";
}
What does this code do?
dotenv.config() loads your .env file so the API key is available as an environment variable.
The startup check calls process.exit(1) if the key is missing. This catches misconfiguration immediately instead of failing silently when the first API request arrives.
cors() allows the React dev server (port 5173) to talk to Express (port 3001). Without it, the browser blocks cross-origin requests.
- Below the interface, add the route handler with validation and a placeholder response:
app.post("/api/recommend", async (req, res) => {
const { availablePlayers, currentRoster, leagueType } =
req.body as DraftRequest;
// Reject if fewer than 2 available players
if (
!availablePlayers ||
!Array.isArray(availablePlayers) ||
availablePlayers.length < 2
) {
res.status(400).json({ error: "At least 2 available players required." });
return;
}
// Reject if no current roster players
if (
!currentRoster ||
!Array.isArray(currentRoster) ||
currentRoster.length < 1
) {
res.status(400).json({ error: "At least 1 current roster player required." });
return;
}
// Reject invalid league types
if (!["PPR", "Half-PPR", "Standard"].includes(leagueType)) {
res.status(400).json({ error: "Invalid league type." });
return;
}
// Placeholder response - AI integration comes in the next step
res.json({
bestPick: "Recommendation engine not yet connected",
reasoning: "This is a placeholder. The AI integration will replace this in the next step.",
positionalNeeds: [],
sleepers: [],
});
});
What does this validation do?
Each if block checks one requirement: at least 2 available players, at least 1 roster player, and a valid league type. If any check fails, the server responds with a 400 status and a clear error message, then return stops further processing.
The placeholder res.json() at the end matches the shape of the final recommendation (bestPick, reasoning, positionalNeeds, sleepers). This lets the frontend develop against the real structure before the AI is connected.
- Finally, add the server listener at the bottom of the file:
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
- Save server/src/index.ts.
Start the server and verify it runs
- Add a dev script to server/package.json. Open that file and find the "scripts" section. Add this script:
"scripts": {
"dev": "npx tsx src/index.ts"
}
What is tsx?
tsx runs TypeScript files directly without a separate compilation step. It uses esbuild under the hood for fast execution during development.
- Save server/package.json.
- Start the server by running this command from the server folder:
npm run dev
You should see Server running on http://localhost:3001 in your terminal.
Seeing an ANTHROPIC_API_KEY error?
If the server exits immediately with the API key error, make sure your .env file exists in the server folder (not inside server/src) and contains your key with no spaces around the = sign.
Help me debug my Express server startup
Connect the React form to the backend
The server is running and ready to receive requests. Now you need to update the React app to send the form data to your API instead of just displaying it as JSON.
- Switch back to client/src/App.tsx in VS Code. Replace the entire file contents with this code:
import { useState } from "react";
import { DraftForm } from "./components/DraftForm";
import type { DraftInput, RecommendationResponse } from "./types";
function App() {
// Track the API response, loading state, and errors
const [recommendation, setRecommendation] =
useState<RecommendationResponse | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (input: DraftInput) => {
setIsLoading(true);
setError(null);
setRecommendation(null);
try {
const response = await fetch("http://localhost:3001/api/recommend", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to get recommendation");
}
const data: RecommendationResponse = await response.json();
setRecommendation(data);
} catch (err) {
setError(
err instanceof Error ? err.message : "An unexpected error occurred"
);
} finally {
setIsLoading(false);
}
};
What does this code do?
handleSubmit sends a POST request to your Express server with the form data as JSON. It clears any previous recommendation and error before each request.
If the server responds with a non-OK status (like 400 for validation errors), it reads the error message from the response and throws it. The finally block always resets the loading state, whether the request succeeds or fails.
- Below the handleSubmit function, add the JSX return statement to complete the component:
return (
<div style={{ maxWidth: "800px", margin: "0 auto", padding: "2rem" }}>
<h1>Fantasy Football Draft Assistant</h1>
<p>Enter your draft context and get AI-powered pick recommendations.</p>
<DraftForm onSubmit={handleSubmit} isLoading={isLoading} />
{isLoading && <p>Analyzing your draft situation...</p>}
{error && <p style={{ color: "red" }}>{error}</p>}
{recommendation && (
<div style={{ marginTop: "1.5rem" }}>
<h2>Draft Recommendation</h2>
<p><strong>Best Pick:</strong> {recommendation.bestPick}</p>
<p><strong>Reasoning:</strong> {recommendation.reasoning}</p>
</div>
)}
</div>
);
}
export default App;
What does this render?
The JSX conditionally renders three states. While loading, it shows a text message. If an error occurs, it shows the error in red. When a recommendation arrives, it displays the bestPick and reasoning fields.
- Save client/src/App.tsx.
Before you test, think about what you expect to see. The form sends data to the backend, and the backend returns a placeholder. What do you think the recommendation will say?
- Return to your browser at http://localhost:5173.
- Type Patrick Mahomes, QB in the **Your Current Roster** field.
- Leave the available players pre-populated and click Get Recommendation.
You should see "Recommendation engine not yet connected" appear as the Best Pick, and the placeholder reasoning text below it. The data flows end-to-end, but there's no intelligence yet.
Not seeing any response?
Make sure your Express server is still running in a separate terminal (you should see the Server running on http://localhost:3001 message). The React dev server and Express server need to run simultaneously in different terminal windows.
If you see a CORS or network error in the browser console, check that you imported cors and called app.use(cors()) in your server file.
Help me debug my frontend-to-backend connection
✔️ Awesome, I've got everything!
Great. Double check you have saved both server/src/index.ts and client/src/App.tsx.
ⓧ I'd like to double check the full code
Here is the complete server/src/index.ts at this point. Confirm each section is present in order: imports, dotenv config, env check, app setup with cors/json middleware, DraftRequest interface, POST route with three validation checks and placeholder response, and the app.listen call.
And here is the complete client/src/App.tsx at this point. Confirm it imports DraftForm and the types, has three useState hooks (recommendation, isLoading, error), a handleSubmit that fetches from the backend, and conditionally renders loading text, error text, or the recommendation data.
Data flows from the form, through the API, and back to the browser. But the response is static and unintelligent. Next up, you'll replace that placeholder with a real Claude API call that analyzes the draft context and returns thoughtful recommendations.
Integrate the Claude API
In the last step, you connected your frontend form to the backend. But when you submitted the form, the response was a hardcoded placeholder with no real intelligence behind it.
This step fills that gap. You will replace the static placeholder with a real call to the Claude API, building a structured prompt that instructs the model to analyze your draft context and return validated JSON recommendations.
In this step, get ready to:
- Add a response validation function and instantiate the Anthropic client.
- Build a system prompt and call the Claude Messages API with your draft data.
- Handle API-specific errors like invalid keys and rate limits.
Add the type guard and Anthropic client
Before calling Claude, you need two pieces of infrastructure. First, a client instance that authenticates with your API key. Second, a type guard function that validates whether Claude's response has the correct shape.
- In server/src/index.ts, find the DraftRequest interface (above the app.post route). Copy the following code directly below it:
// Shape of the AI recommendation response
interface RecommendationResponse {
bestPick: string;
reasoning: string;
positionalNeeds: string[];
sleepers: string[];
}
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
function isValidRecommendation(data: unknown): data is RecommendationResponse {
if (typeof data !== "object" || data === null) return false;
const obj = data as Record<string, unknown>;
return (
typeof obj.bestPick === "string" &&
typeof obj.reasoning === "string" &&
Array.isArray(obj.positionalNeeds) &&
obj.positionalNeeds.every((item) => typeof item === "string") &&
Array.isArray(obj.sleepers) &&
obj.sleepers.every((item) => typeof item === "string")
);
}
What does this code do?
The RecommendationResponse interface defines the exact shape of data you expect Claude to return: a best pick, reasoning, positional needs, and sleepers. This gives TypeScript the information it needs to validate the response at runtime.
The new Anthropic() call creates a client instance authenticated with your API key from the .env file.
The isValidRecommendation function is a TypeScript type guard. It checks that a response from Claude actually contains bestPick, reasoning, positionalNeeds, and sleepers with the correct types. If Claude returns anything unexpected, your server catches it rather than crashing.
- Save server/src/index.ts.
- Check your terminal where the server is running. It should restart without any errors.
Seeing an error on save?
Make sure you placed the code below the DraftRequest interface and above the app.post line. The Anthropic import should already exist from the previous step. If you see a "Cannot find name" error for Anthropic, confirm you have import Anthropic from "@anthropic-ai/sdk"; at the top of the file.
Help me debug this error
Build the prompt and call Claude
Now you will replace the hardcoded placeholder response with real AI logic. The system prompt tells Claude exactly what role to play and what JSON shape to return. The user message passes in your draft context.
- In server/src/index.ts, find the route handler app.post("/api/recommend", .... First, add async before the callback so it reads async (req, res) =>.
- Delete the hardcoded placeholder response (the res.json({ bestPick: 'Recommendation engine not yet connected' ... block). In its place, paste the following system prompt and user message:
const systemPrompt = `You are a fantasy football draft analyst. Your job is to recommend the best available pick based ONLY on the information provided. Do NOT use assumed live rankings, injury data, or external information. Base your analysis solely on the submitted roster, available players, and scoring format.
Respond ONLY with valid JSON matching this exact schema (no additional text before or after):
{
"bestPick": "Player Name, Position",
"reasoning": "2-3 sentences explaining why this is the best pick given the roster composition and scoring format",
"positionalNeeds": ["Position 1 - brief explanation", "Position 2 - brief explanation"],
"sleepers": ["Player Name - why they are undervalued given the context"]
}
Scoring format context:
- PPR: Receptions are worth 1 point each, making pass-catching RBs and high-volume WRs/TEs more valuable.
- Half-PPR: Receptions are worth 0.5 points each.
- Standard: No points for receptions, making volume rushers and TDs more valuable.`;
const userMessage = `League Type: ${leagueType}
My Current Roster:
${currentRoster.map((p) => `- ${p}`).join("\n")}
Available Players:
${availablePlayers.map((p) => `- ${p}`).join("\n")}
Who should I draft next?`;
What do these prompts do?
The systemPrompt gives Claude its persona and strict constraints. It must respond with JSON only, act as a fantasy football analyst, and use only the data you provide. This prevents the model from hallucinating player rankings or injury news.
The userMessage formats your input (league type, roster, available players) into a readable prompt. Claude reads both the system and user messages together to produce its recommendation.
- Directly below the userMessage, paste the API call that sends these prompts to Claude and extracts the text response:
try {
const message = await anthropic.messages.create({
model: "claude-sonnet-5",
max_tokens: 1024,
system: systemPrompt,
messages: [{ role: "user", content: userMessage }],
});
const textBlock = message.content.find((block) => block.type === "text");
if (!textBlock || textBlock.type !== "text") {
res.status(500).json({ error: "No text response from AI." });
return;
}
const jsonMatch = textBlock.text.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
res.status(500).json({ error: "Could not parse AI response as JSON." });
return;
}
What is the regex doing?
Claude sometimes includes preamble text before the JSON. The regex /\{[\s\S]*\}/ extracts the first complete JSON object from the response text, making the parsing resilient even if Claude adds a brief note around the JSON block.
- Continuing directly below, paste the code that parses the extracted JSON, validates it, and sends the response:
let parsed: unknown;
try {
parsed = JSON.parse(jsonMatch[0]);
} catch {
res.status(500).json({ error: "AI returned malformed JSON." });
return;
}
if (!isValidRecommendation(parsed)) {
res.status(500).json({
error:
"AI response is missing required fields. Expected bestPick, reasoning, positionalNeeds, and sleepers.",
});
return;
}
res.json(parsed);
} catch (err: unknown) {
res.status(500).json({ error: "An unexpected error occurred." });
}
What does the validation check?
After parsing the JSON, isValidRecommendation confirms all four required fields exist with the correct types. If Claude returns an incomplete response, the server sends a clear 500 error to the frontend rather than passing through broken data.
- Save server/src/index.ts.
- Check that the server restarts without errors in the terminal.
Before you test, what do you think will happen when you submit the form now? Will the placeholder text still appear, or something different?
- Switch to your browser at http://localhost:5173.
- Type a player name in the Your Current Roster field (for example, Patrick Mahomes, QB).
- Leave the pre-loaded available players as they are.
- Click Get Recommendation.
After a few seconds, you should see a real AI-generated recommendation with a specific player pick, reasoning, positional needs, and sleeper picks. The placeholder is gone.
Not seeing a recommendation?
Check the terminal for error messages. If you see a 401 error, your ANTHROPIC_API_KEY in server/.env may be incorrect or expired. Double check that the key starts with sk-ant- and has no extra spaces.
If you see a syntax error, make sure you added async before (req, res) in the route handler. Without async, the await keyword inside the handler will fail.
Help me debug this API call
Handle API errors gracefully
The basic catch block currently returns a generic error for every failure. In production, different failures need different responses. An invalid API key is a configuration problem. A rate limit means the user should wait and retry.
- In server/src/index.ts, find the catch block at the end of the route handler:
} catch (err: unknown) {
res.status(500).json({ error: "An unexpected error occurred." });
}
- Replace that entire catch block with this detailed error handling:
} catch (err: unknown) {
if (err instanceof Anthropic.APIError) {
if (err.status === 401) {
res
.status(500)
.json({ error: "Invalid API key. Check your ANTHROPIC_API_KEY." });
return;
}
if (err.status === 429) {
res
.status(429)
.json({
error: "Rate limited. Please wait a moment and try again.",
});
return;
}
res.status(500).json({ error: `API error: ${err.message}` });
return;
}
res.status(500).json({ error: "An unexpected error occurred." });
}
What does this error handling do?
The Anthropic SDK throws an Anthropic.APIError for any HTTP error from the API. The status property tells you the HTTP status code.
- 401 means the API key is invalid or missing.
- 429 means you have hit a rate limit and should wait before retrying.
- Any other API error gets a generic message with the error details.
- Non-API errors (like network failures) fall through to the final generic response.
- Save server/src/index.ts.
To verify the error handling works, you can temporarily break your API key and see what happens.
- Open server/.env and change ANTHROPIC_API_KEY to sk-ant-BROKEN.
- Save .env and let the server restart.
- Submit the form again in your browser.
You should see an error message like "Invalid API key. Check your ANTHROPIC_API_KEY." displayed in the response area instead of a crash or a generic failure.
- Switch back to server/.env and restore your real API key.
- Save the file and confirm the server restarts.
Still seeing a generic error?
Make sure the err instanceof Anthropic.APIError check comes before the generic fallback. The Anthropic class is imported at the top of the file. If your catch block is not detecting the error type, confirm the import reads import Anthropic from "@anthropic-ai/sdk".
Help me fix my error handling
✔️ Awesome, I've got everything!
Great. Double check you restored your real API key in server/.env and that the server is running without errors.
ⓧ I'd like to double check the full code
Your server/src/index.ts is assembled from several chunks. Scroll through your file and confirm these sections appear in order:
- Imports (express, cors, Anthropic, dotenv) and dotenv.config().
- ANTHROPIC_API_KEY startup check with process.exit(1).
- Express app setup (cors(), express.json()).
- Anthropic client instantiation (const anthropic = new Anthropic(...)).
- DraftRequest and RecommendationResponse interfaces.
- isValidRecommendation function.
- POST /api/recommend route with validation, systemPrompt, userMessage, anthropic.messages.create, JSON extraction, and error handling.
- app.listen(PORT, ...) at the bottom.
Your backend now has real intelligence. Next up, you will add loading spinners and error displays to the frontend so the user experience matches the power of the API behind it.
Add Loading and Error States
Real API calls to Claude take a few seconds to return. Without visual feedback, users click the button and stare at a frozen screen, unsure if anything happened.
In this step, you'll create dedicated components for loading, error, and recommendation display. Then you'll wire them into App.tsx so the UI responds to every state of the request lifecycle.
In this step, get ready to:
- Create a loading spinner and error message component.
- Build a styled Recommendation component with clear sections.
- Update App.tsx to conditionally render loading, error, and recommendation states.
Create the loading spinner and error message components
These two small components handle the two most common states a user encounters while waiting for an API response: progress and failure.
- Create a new file at client/src/components/LoadingSpinner.tsx and add the following code:
export function LoadingSpinner() {
return (
<div style={{ textAlign: "center", padding: "2rem" }}>
<div
style={{
display: "inline-block",
width: "40px",
height: "40px",
border: "4px solid #f3f3f3",
borderTop: "4px solid #4caf50",
borderRadius: "50%",
animation: "spin 1s linear infinite",
}}
/>
<p>Analyzing your draft situation...</p>
<style>{`@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }`}</style>
</div>
);
}
What does this code do?
The spinner is a 40x40 pixel div with a partially colored border. The CSS @keyframes spin animation rotates it 360 degrees every second, creating the spinning effect.
The text below the spinner tells the user exactly what's happening so they don't wonder if the app froze.
- Create another new file at client/src/components/ErrorMessage.tsx and add this code:
interface ErrorMessageProps {
message: string;
}
export function ErrorMessage({ message }: ErrorMessageProps) {
return (
<div
style={{
border: "2px solid #f44336",
borderRadius: "8px",
padding: "1rem",
marginTop: "1.5rem",
backgroundColor: "#ffebee",
}}
>
<strong style={{ color: "#f44336" }}>Error:</strong> {message}
</div>
);
}
What does this code do?
The ErrorMessage component accepts a message string prop and renders it inside a red-bordered container with a light red background. The bold "Error:" label makes the problem immediately visible.
- Save both files.
- Confirm both files exist by running this in the client/src/components/ directory:
ls client/src/components/
You should see DraftForm.tsx, ErrorMessage.tsx, and LoadingSpinner.tsx listed.
Don't see the new files?
Make sure you created the files inside client/src/components/ and not in the project root or server/ folder.
help me find where I saved my component files
Create the styled recommendation display
The recommendation component takes Claude's structured response and renders each field in its own clearly labeled section.
- Create a new file at client/src/components/Recommendation.tsx and add the top portion of the component:
import type { RecommendationResponse } from "../types";
interface RecommendationProps {
data: RecommendationResponse;
}
export function Recommendation({ data }: RecommendationProps) {
return (
<div
style={{
border: "2px solid #4caf50",
borderRadius: "8px",
padding: "1.5rem",
marginTop: "1.5rem",
}}
>
<h2 style={{ color: "#4caf50", marginTop: 0 }}>Draft Recommendation</h2>
<div style={{ marginBottom: "1rem" }}>
<h3>Best Pick</h3>
<p style={{ fontSize: "1.25rem", fontWeight: "bold" }}>
{data.bestPick}
</p>
</div>
<div style={{ marginBottom: "1rem" }}>
<h3>Reasoning</h3>
<p>{data.reasoning}</p>
</div>
What does this code do?
The component wraps everything in a green-bordered card. The bestPick field renders in a larger, bold font so the recommendation jumps out. The reasoning section displays Claude's explanation as a plain paragraph.
- Directly below, add the positional needs and sleepers sections to complete the component:
<div style={{ marginBottom: "1rem" }}>
<h3>Positional Needs</h3>
<ul>
{data.positionalNeeds.map((need, i) => (
<li key={i}>{need}</li>
))}
</ul>
</div>
<div>
<h3>Sleeper Picks</h3>
<ul>
{data.sleepers.map((sleeper, i) => (
<li key={i}>{sleeper}</li>
))}
</ul>
</div>
</div>
);
}
What does this code do?
Both positionalNeeds and sleepers are arrays of strings. The component maps over each array and renders the items as an unordered list so the reader can scan them quickly.
- Save Recommendation.tsx.
Seeing a TypeScript error in VS Code?
If you see a red underline on the import, make sure the file is saved inside client/src/components/ and that types.ts exists at client/src/types.ts.
help me fix my Recommendation.tsx import error
Wire all components into App.tsx
With all three display components created, you can now replace App.tsx to import them and conditionally render each based on the request state.
- Open client/src/App.tsx and replace its entire contents with the following imports, state, and submit handler:
import { useState } from "react";
import { DraftForm } from "./components/DraftForm";
import { Recommendation } from "./components/Recommendation";
import { LoadingSpinner } from "./components/LoadingSpinner";
import { ErrorMessage } from "./components/ErrorMessage";
import type { DraftInput, RecommendationResponse } from "./types";
function App() {
const [recommendation, setRecommendation] =
useState<RecommendationResponse | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (input: DraftInput) => {
// Clear previous state and show loading
setIsLoading(true);
setError(null);
setRecommendation(null);
try {
const response = await fetch("http://localhost:3001/api/recommend", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to get recommendation");
}
const data: RecommendationResponse = await response.json();
setRecommendation(data);
What does this code do?
The handleSubmit function immediately sets isLoading to true and clears any previous error or recommendation. This ensures the UI shows a clean loading state every time the user submits.
If the server returns a non-OK status, the code reads the error message from the JSON body and throws it so the catch block can handle it uniformly.
- Directly below the code you just pasted, add the error handling and component render:
} catch (err) {
setError(
err instanceof Error ? err.message : "An unexpected error occurred"
);
} finally {
setIsLoading(false);
}
};
return (
<div style={{ maxWidth: "800px", margin: "0 auto", padding: "2rem" }}>
<h1>Fantasy Football Draft Assistant</h1>
<p>Enter your draft context and get AI-powered pick recommendations.</p>
<DraftForm onSubmit={handleSubmit} isLoading={isLoading} />
{isLoading && <LoadingSpinner />}
{error && <ErrorMessage message={error} />}
{recommendation && <Recommendation data={recommendation} />}
</div>
);
}
export default App;
What does this code do?
The finally block sets isLoading to false regardless of success or failure. This guarantees the spinner disappears and the button re-enables.
The JSX uses conditional rendering: LoadingSpinner appears only while loading, ErrorMessage appears only when there's an error, and Recommendation appears only when data arrives. Only one of these three states is visible at a time.
- Save App.tsx.
Before you test, what do you think you'll see when you click the submit button now? Will the recommendation appear instantly or will there be a delay?
- Switch to your browser at http://localhost:5173 and fill in the form with at least one roster player.
- Click Get Recommendation.
You should see a green spinning circle with the text "Analyzing your draft situation..." appear below the form. The button changes to "Analyzing..." and becomes unclickable. After a few seconds, the spinner disappears and a green-bordered card appears with your recommendation, positional needs, and sleeper picks.
Spinner doesn't appear?
Check the browser console for import errors. If you see "Failed to resolve import", confirm that all four files (DraftForm.tsx, LoadingSpinner.tsx, ErrorMessage.tsx, Recommendation.tsx) exist inside client/src/components/.
help me debug why my loading spinner is not showing up
Now verify that errors display correctly. This confirms the error path works when something goes wrong with the API.
- Open server/.env and change your API key to something invalid like broken-key.
- Restart the server (press Ctrl+C in the server terminal, then run npm run dev again).
- Submit the form in the browser.
You should see a red-bordered error box appear with a message about the invalid API key. The spinner disappears and the button re-enables so you can try again.
- Change your API key back to the real value in server/.env and restart the server.
Error message not showing?
Make sure you restarted the server after changing .env. Environment variables are only read at startup.
help me figure out why the error message doesn't display
✔️ Awesome, I've got everything!
Great. Double check you've saved all files in the client/src/components/ folder and App.tsx.
ⓧ I'd like to double check the full code
Scroll up and confirm each of these files matches what you pasted. They should appear in this order in your project:
- client/src/components/LoadingSpinner.tsx (the spinning div with keyframes).
- client/src/components/ErrorMessage.tsx (red border, #ffebee background).
- client/src/components/Recommendation.tsx (green border, four sections: Best Pick, Reasoning, Positional Needs, Sleepers).
- client/src/App.tsx (imports all four components, three useState hooks, handleSubmit with try/catch/finally, conditional rendering).
Your app now handles the full request lifecycle gracefully. Users see exactly what's happening at every moment. Next up, you'll take on the Secret Mission and extend the AI response with confidence scores and alternative picks.
Secret mission
Confidence Scores and Alternative Recommendations
Your draft assistant gives one recommendation, but how confident is it? A single pick with no context is hard to trust. In this secret mission, you'll extend the AI to return a confidence score and ranked alternative picks, giving users the full picture before they make their draft decision.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so the only ongoing cost is the Anthropic API usage when you actively submit requests (fractions of a cent per request).
Resources you used:
- The client/ folder (React frontend with Vite, including node_modules).
- The server/ folder (Express backend, including node_modules and your .env file with your API key).
- Running dev servers (Vite on port 5173 and Express on port 3001).
Keep everything running
No action needed. Choose this if you are still actively building or want to keep experimenting with the draft assistant.
- Your dev servers will keep running until you close the terminal windows or press Ctrl+C.
- The Anthropic API only charges when you actively send requests. Leaving the project files on disk costs nothing.
- Your API key remains stored in server/.env. Keep this file out of version control if you push the project to GitHub.
Pause - I'll come back to this later
Shut down running processes to free up terminal sessions and memory. Your project files stay on disk so you can pick up where you left off.
- In each terminal window running a dev server, press Ctrl+C to stop the process.
- Close VS Code if you want to free additional memory.
- To resume later, open your project folder in VS Code and run npm run dev in both the client/ and server/ folders.
Delete - I don't want to use this again
Remove all project files and start fresh. This permanently deletes your code and local configuration.
- In each terminal window running a dev server, press Ctrl+C to stop the process.
- Delete the project folder by running this command from your Desktop:
macOS
cd ~/Desktop
rm -rf client server
Windows
cd ~\Desktop
Remove-Item -Recurse -Force client, server
- This removes both the client/ and server/ folders, including all node_modules, source code, and your .env file.
- If you no longer need your Anthropic API key, you can revoke it from your account's key management page in the Claude Console.
Nice Work!
Nice Work!
You just built a full-stack Fantasy Football Draft Assistant powered by Claude. From a blank folder to an AI-integrated app with structured prompting, loading states, and real-time recommendations.
You've learned how to:
- Build a full-stack TypeScript app with a React frontend (Vite) and Express backend running side-by-side.
- Integrate the Claude API with structured prompting and JSON response parsing that validates every field before displaying results.
- Craft an AI prompt that constrains recommendations to only submitted data, preventing hallucinated rankings or external information from leaking in.
- Secret Mission: Added AI-estimated confidence levels and ranked alternative draft picks to give users more actionable information.
Ready to quiz yourself?