Smart Task Breakdown Assistant

Build an AI-powered web app that breaks project goals into subtasks.

Introduction

30 Second Summary

Starting a new project can feel overwhelming when you don't know where to begin. Breaking a big goal into smaller steps makes everything more manageable.

In this project, you will build a web application that uses AI to turn vague project goals into structured, actionable task lists.

What You'll Build

A single-page web application powered by Claude AI that accepts a project goal and returns a prioritized breakdown of subtasks with descriptions and time estimates.

By the end of this project, you'll have:

  • A modern frontend built with Vite that displays an input form and styled task cards.
  • An Express backend server that securely proxies requests to the Claude API.
  • A working AI prompt that generates structured task breakdowns with titles, descriptions, and time estimates.
  • Secret Mission: Add priority levels with color coding and a copy-to-clipboard button.

Are there any prerequisites?

You should have basic knowledge of HTML and CSS. No prior JavaScript or AI experience is needed.

Set Up Your Environment

Building a web app that calls an AI API requires a few foundational tools. You need a JavaScript runtime to execute your code, a scaffolding tool to structure your project files, and access to the AI service itself.

In this step, you'll install Node.js, scaffold a fresh project with Vite, and create your Anthropic account. By the end, you'll have a fully configured development environment ready for coding.

In this step, get ready to:
  • Install Node.js and verify it works.
  • Scaffold the project with Vite.
  • Create your Anthropic API account and get an API key.
Install Node.js and verify it works

Node.js is the JavaScript runtime that lets you run JavaScript outside of a browser. Your backend server and build tools both depend on it.

  • Open a terminal in Cursor by pressing Cmd+` (macOS) or Ctrl+` (Windows).
  • Check whether Node.js is already installed by running:
node --version

✔️ I see version 20 or higher

Node.js is ready to go. Skip ahead to the next substep where you scaffold the project.

ⓧ I see an older version

Your current version is too old for Vite, which requires Node.js 20.19+ or 22.12+.

  • Go to https://nodejs.org/en/download in your browser.
  • Download the v24.16.0 LTS installer for your operating system.
  • Run the installer and accept the default options.
  • Close your Cursor terminal and reopen it with Cmd+` (macOS) or Ctrl+` (Windows).
  • Confirm the update by running:
node --version

You should see v24 or higher in the output.

ⓧ Command not found

Node.js is not installed yet.

  • Go to https://nodejs.org/en/download in your browser.
  • Download the v24.16.0 LTS installer for your operating system.
  • Run the installer and accept the default options.
  • Close your Cursor terminal and reopen it with Cmd+` (macOS) or Ctrl+` (Windows).
  • Verify the installation by running:
node --version

You should see v24.16.0 in the output.

  • Also confirm that npm (the Node.js package manager) is available by running:
npm --version

You should see a version number like 11.x.x or similar.

Still seeing 'command not found'?

If node or npm still isn't recognized after installing, close Cursor entirely and reopen it. New terminal sessions need to pick up the updated system PATH.

help me troubleshoot why node isn't recognized in my Cursor terminal after installing Node.js

Scaffold the project with Vite

Vite is a modern build tool that gives you instant hot-reload during development. You'll use it to serve your frontend files and see changes in real time.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create the project by running this command:
npm create vite@latest task-breakdown -- --template vanilla

What might I see in the terminal?

If this is your first time using Vite, npm will ask you to confirm installing the create-vite package. Type y and press Enter to continue.

Because the command includes the project name and template, it skips all other interactive prompts.

  • Move into the new project folder and install the base dependencies by running:
cd task-breakdown
npm install

This installs Vite and its supporting packages into a node_modules folder.

  • Install the backend dependencies by running:
npm install express @anthropic-ai/sdk

What do these packages do?

  • express is a lightweight web server framework for Node.js. It handles incoming requests and sends back responses.
  • @anthropic-ai/sdk is the official SDK for calling the Claude AI API. It manages authentication, request formatting, and response parsing.
  • Confirm the project files exist by running:
ls

You should see index.html, package.json, and a node_modules folder in the output.

Don't see these files?

Make sure you ran cd task-breakdown before running ls. If the folder doesn't exist, re-run the scaffold command from your Desktop.

help me troubleshoot my Vite project scaffold

Create your Anthropic API account and get a key

Anthropic is the company behind Claude, the AI model your app will call to generate task breakdowns. You need an API key to authenticate your requests.

  • Go to console.anthropic.com in your browser.
  • Create a free account (or sign in if you already have one).
  • Click API Keys in the left sidebar.
  • Click Create Key.
  • Give your key a name like task-breakdown and click Create Key.
  • Copy the key immediately. You will not be able to see it again after closing this dialog.

What about the cost?

New Anthropic accounts receive free credits to test the API. This project uses Claude Haiku 4.5, the most affordable model at $1 per million input tokens and $5 per million output tokens.

A typical request in this app uses roughly 500-1000 tokens total, costing a fraction of a cent.

  • Back in Cursor's terminal, create a .env file in your project root by running:
touch .env
  • Open .env in Cursor's editor by clicking on it in the Explorer sidebar.
  • Paste the following into the file, replacing your-api-key-here with the key you just copied:
ANTHROPIC_API_KEY=your-api-key-here

What does this file do?

The .env file stores sensitive values like API keys outside of your code. Your server reads this value at startup so the key never appears in your source files.

Never share your API key

Do not commit this file to version control or share it publicly. Anyone with your key can make API calls billed to your account.

  • Save the file.

Can't find .env in the sidebar?

Dotfiles (files starting with .) are sometimes hidden by default. In Cursor's Explorer sidebar, check that no filter is hiding dotfiles. You can also confirm the file exists by running ls -a in the terminal.

help me find my .env file in Cursor

✔️ Awesome, I've got everything!

Great. Double check that you've saved your .env file and that your terminal is inside the task-breakdown folder.

ⓧ I'd like to double check the full code

Here's what your .env file should look like (with your own key in place of the placeholder):

ANTHROPIC_API_KEY=your-api-key-here

And here's what your package.json should look like after the installs:

{
  "name": "task-breakdown",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "devDependencies": {
    "vite": "^7.0.0"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.102.0",
    "express": "^4.19.2"
  }
}

Your environment is ready. Next up, you'll build the frontend interface where users enter their project goals.

Build the Frontend Interface

Your development environment is ready with Node.js, Vite, and all the packages installed. Now it's time to build what users will actually see and interact with.

In this step, you'll create the HTML structure and CSS styles for your task breakdown app. The interface has a clean form where users type a project goal, and a results area where AI-generated subtasks will appear later.

In this step, get ready to:
  • Build the HTML form for entering project goals.
  • Style the app with a clean, card-based layout.
  • Preview the interface in your browser.
Replace the default HTML with your app structure

The Vite scaffold generated a default index.html with placeholder content. You need to replace it with your own app layout.

  • In Cursor, open index.html from the file explorer sidebar on the left.
  • Select all the content in the file (Cmd+A on macOS or Ctrl+A on Windows) and delete it.
  • Paste the following code into the empty file:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Task Breakdown Assistant</title>
    <link rel="stylesheet" href="/src/style.css" />
  </head>
  <body>
    <div class="container">
      <header>
        <h1>Task Breakdown Assistant</h1>
        <p>Paste a project goal and let AI break it into actionable subtasks.</p>
      </header>

      <main>
        <!-- Form for entering the project goal -->
        <form id="task-form">
          <label for="goal-input">Project Goal</label>
          <textarea
            id="goal-input"
            placeholder="e.g., Build a company blog with user authentication and an admin panel"
            rows="4"
            required
          ></textarea>
          <button type="submit" id="submit-btn">Break It Down</button>
        </form>

        <!-- Container for displaying AI-generated task cards -->
        <section id="results" class="results hidden">
          <h2>Your Task Breakdown</h2>
          <div id="tasks-container"></div>
        </section>

        <!-- Error message display -->
        <div id="error-message" class="error hidden"></div>
      </main>
    </div>

    <script type="module" src="/src/main.js"></script>
  </body>
</html>

What does this code do?

  • The <form> contains a textarea where users type their project goal and a submit button labelled "Break It Down".
  • The <section id="results"> is hidden by default and will display the AI-generated task cards once the backend responds.
  • The <div id="error-message"> is also hidden and only appears if something goes wrong with the API request.
  • The <script type="module"> tag loads your JavaScript file as an ES module, which you will write in a later step.
  • Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).

File not saving?

Make sure you opened the task-breakdown folder in Cursor, not just a single file. If the file tab shows a dot next to its name, it has unsaved changes.

Help me troubleshoot saving files in Cursor.

Style the app with CSS

Your app needs styling to look clean and professional. The Vite scaffold already created a src/style.css file that you will replace with your own styles.

  • Open src/style.css in Cursor's file explorer (expand the src folder).
  • Select all and delete the existing content.
  • Paste the following styles:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background-color: #f5f7fa;
  color: #333;
  line-height: 1.6;
  padding: 2rem 1rem;
}

.container {
  max-width: 720px;
  margin: 0 auto;
}

header {
  text-align: center;
  margin-bottom: 2rem;
}

header h1 {
  font-size: 1.8rem;
  color: #1a1a2e;
  margin-bottom: 0.5rem;
}

header p {
  color: #666;
  font-size: 1rem;
}

form {
  background: #fff;
  padding: 1.5rem;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  margin-bottom: 2rem;
}

label {
  display: block;
  font-weight: 600;
  margin-bottom: 0.5rem;
  color: #1a1a2e;
}

textarea {
  width: 100%;
  padding: 0.75rem;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  font-size: 1rem;
  font-family: inherit;
  resize: vertical;
  transition: border-color 0.2s;
}

textarea:focus {
  outline: none;
  border-color: #4a6cf7;
}

button[type="submit"] {
  display: block;
  width: 100%;
  margin-top: 1rem;
  padding: 0.75rem;
  background-color: #4a6cf7;
  color: #fff;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.2s;
}

button[type="submit"]:hover {
  background-color: #3b5de7;
}

button[type="submit"]:disabled {
  background-color: #a0a0a0;
  cursor: not-allowed;
}

.results {
  margin-top: 1rem;
}

.results h2 {
  font-size: 1.3rem;
  margin-bottom: 1rem;
  color: #1a1a2e;
}

.hidden {
  display: none;
}

.task-card {
  background: #fff;
  padding: 1.25rem;
  border-radius: 10px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
  margin-bottom: 1rem;
  border-left: 4px solid #4a6cf7;
}

.task-card h3 {
  font-size: 1.05rem;
  color: #1a1a2e;
  margin-bottom: 0.4rem;
}

.task-card p {
  color: #555;
  font-size: 0.9rem;
  margin-bottom: 0.4rem;
}

.task-card .time-estimate {
  font-size: 0.8rem;
  color: #888;
  font-style: italic;
}

.error {
  background: #fee2e2;
  color: #b91c1c;
  padding: 1rem;
  border-radius: 8px;
  margin-top: 1rem;
}

.loading-text {
  text-align: center;
  color: #666;
  font-style: italic;
  padding: 2rem;
}

What do these styles do?

The stylesheet uses a card-based layout where the form sits inside a white card with rounded corners and a subtle shadow. The .task-card class styles the individual subtask results with a blue left border for visual hierarchy.

The .hidden class uses display: none to keep the results section and error message invisible until your JavaScript reveals them. The disabled button style gives users visual feedback when their request is processing.

  • Save src/style.css.

Styles not looking right later?

Double check that index.html references the stylesheet at /src/style.css (with the leading slash and src folder). A wrong path means the browser cannot find the CSS file.

Help me debug why my CSS styles are not loading.

✔️ Awesome, I've got everything!

Great. Make sure both index.html and src/style.css are saved before moving on.

ⓧ I'd like to double check the full code

Here is the full index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Task Breakdown Assistant</title>
    <link rel="stylesheet" href="/src/style.css" />
  </head>
  <body>
    <div class="container">
      <header>
        <h1>Task Breakdown Assistant</h1>
        <p>Paste a project goal and let AI break it into actionable subtasks.</p>
      </header>

      <main>
        <form id="task-form">
          <label for="goal-input">Project Goal</label>
          <textarea
            id="goal-input"
            placeholder="e.g., Build a company blog with user authentication and an admin panel"
            rows="4"
            required
          ></textarea>
          <button type="submit" id="submit-btn">Break It Down</button>
        </form>

        <section id="results" class="results hidden">
          <h2>Your Task Breakdown</h2>
          <div id="tasks-container"></div>
        </section>

        <div id="error-message" class="error hidden"></div>
      </main>
    </div>

    <script type="module" src="/src/main.js"></script>
  </body>
</html>

And here is the full src/style.css:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background-color: #f5f7fa;
  color: #333;
  line-height: 1.6;
  padding: 2rem 1rem;
}

.container {
  max-width: 720px;
  margin: 0 auto;
}

header {
  text-align: center;
  margin-bottom: 2rem;
}

header h1 {
  font-size: 1.8rem;
  color: #1a1a2e;
  margin-bottom: 0.5rem;
}

header p {
  color: #666;
  font-size: 1rem;
}

form {
  background: #fff;
  padding: 1.5rem;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  margin-bottom: 2rem;
}

label {
  display: block;
  font-weight: 600;
  margin-bottom: 0.5rem;
  color: #1a1a2e;
}

textarea {
  width: 100%;
  padding: 0.75rem;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  font-size: 1rem;
  font-family: inherit;
  resize: vertical;
  transition: border-color 0.2s;
}

textarea:focus {
  outline: none;
  border-color: #4a6cf7;
}

button[type="submit"] {
  display: block;
  width: 100%;
  margin-top: 1rem;
  padding: 0.75rem;
  background-color: #4a6cf7;
  color: #fff;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.2s;
}

button[type="submit"]:hover {
  background-color: #3b5de7;
}

button[type="submit"]:disabled {
  background-color: #a0a0a0;
  cursor: not-allowed;
}

.results {
  margin-top: 1rem;
}

.results h2 {
  font-size: 1.3rem;
  margin-bottom: 1rem;
  color: #1a1a2e;
}

.hidden {
  display: none;
}

.task-card {
  background: #fff;
  padding: 1.25rem;
  border-radius: 10px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
  margin-bottom: 1rem;
  border-left: 4px solid #4a6cf7;
}

.task-card h3 {
  font-size: 1.05rem;
  color: #1a1a2e;
  margin-bottom: 0.4rem;
}

.task-card p {
  color: #555;
  font-size: 0.9rem;
  margin-bottom: 0.4rem;
}

.task-card .time-estimate {
  font-size: 0.8rem;
  color: #888;
  font-style: italic;
}

.error {
  background: #fee2e2;
  color: #b91c1c;
  padding: 1rem;
  border-radius: 8px;
  margin-top: 1rem;
}

.loading-text {
  text-align: center;
  color: #666;
  font-style: italic;
  padding: 2rem;
}
Preview your app in the browser

Vite includes a development server that hot-reloads your changes instantly. Let's start it up and see your styled form in the browser.

  • Start the Vite dev server by running this command in your Cursor terminal:
npm run dev
  • Open your browser and go to http://localhost:5173.

You should see a centered page with a "Task Breakdown Assistant" heading and a styled form containing a textarea and a blue "Break It Down" button. The form will not do anything when you click submit yet.

Don't see the styled form?

  • Make sure you saved both index.html and src/style.css before checking the browser.
  • Check that your terminal shows the Vite server running with a local URL. If you see an error, try stopping the server with Ctrl+C and running npm run dev again.
  • If the page is completely blank, open your browser's developer console (F12) and check for file-not-found errors pointing to your CSS or JS paths.

Help me troubleshoot my Vite dev server not showing the styled form.

Your frontend interface is looking sharp. Next up, you'll build the Express backend server that talks to the Claude API and turns project goals into structured task breakdowns.

Create the Backend API Server

Your frontend is styled and ready to accept input. Right now, though, pressing the submit button does nothing because there's no server to process the request.

In this step, you'll build an Express server that receives the learner's project goal from the frontend and forwards it to the Claude API. The server acts as a secure middleman that keeps your API key hidden from the browser.

In this step, get ready to:
  • Create an Express server file that calls the Claude API.
  • Add a startup script to your project.
  • Verify the server runs and is ready to accept requests.
Create server.js in the project root

Your Express server needs to do three things: accept incoming JSON requests, call the Claude API with a carefully crafted prompt, and return the structured response to the frontend.

Why do we need a backend server?

The Anthropic API cannot be called directly from a browser due to CORS restrictions. A backend server solves this by proxying requests to Claude on the browser's behalf.

This also keeps your API key secure. If you called the API from the browser, anyone could inspect your network requests and steal your key.

  • Create a new file called server.js in the root of your task-breakdown folder (the same level as package.json).
  • Add the server setup code by pasting this into server.js:
import express from "express";
import Anthropic from "@anthropic-ai/sdk";

// Create the Express app and enable JSON parsing
const app = express();
app.use(express.json());

// Initialize the Anthropic client with your API key
const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
});

What does this code do?

  • import express from "express" loads the Express framework for building your server.
  • import Anthropic from "@anthropic-ai/sdk" loads the official Anthropic SDK for calling Claude.
  • app.use(express.json()) tells Express to automatically parse JSON request bodies.
  • The Anthropic client reads your API key from the environment variable you set in .env.

You won't see any output yet. The next chunk defines the API endpoint that uses this client.

  • Add the API endpoint and server start code below the existing code in server.js:
app.post("/api/breakdown", async (req, res) => {
  const { goal } = req.body;

  if (!goal) {
    return res.status(400).json({ error: "Goal is required" });
  }

  try {
    const message = await anthropic.messages.create({
      model: "claude-haiku-4-5",
      max_tokens: 1024,
      system: `You are a project management assistant. When given a project goal, break it down into 5-7 actionable subtasks. Return ONLY valid JSON in this exact format, with no other text:
{
  "tasks": [
    {
      "title": "Short task title",
      "description": "One sentence describing what to do",
      "timeEstimate": "Estimated time (e.g., '30 minutes', '2 hours')"
    }
  ]
}`,
      messages: [
        {
          role: "user",
          content: `Break down this project goal into actionable subtasks: ${goal}`,
        },
      ],
    });

    // Parse the JSON response from Claude
    const responseText = message.content[0].text;
    const parsed = JSON.parse(responseText);
    res.json(parsed);
  } catch (error) {
    console.error("API Error:", error.message);
    res.status(500).json({ error: "Failed to generate task breakdown" });
  }
});

// Start the server on port 3001
const PORT = 3001;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

What does this code do?

  • app.post("/api/breakdown", ...) creates a POST endpoint that the frontend will call with the user's goal.
  • The system prompt tells Claude exactly how to respond: as a project management assistant that returns structured JSON with task titles, descriptions, and time estimates.
  • JSON.parse(responseText) converts Claude's text response into a JavaScript object so Express can send it back as proper JSON.
  • The catch block logs errors to the server console and sends a generic error message to the frontend.
  • app.listen(PORT, ...) starts the server on port 3001 and prints a confirmation message.

What is prompt engineering?

The system field is where you shape Claude's behavior. By specifying the exact JSON format you expect, Claude returns structured data your app can parse reliably.

Without this constraint, Claude might return plain text paragraphs that your frontend couldn't display as task cards.

  • Save server.js.

You can't run the server yet because you need a startup command. You'll add that next.

✔️ Awesome, I've got everything!

Great. Double check you've saved server.js before moving on.

ⓧ I'd like to double check the full code

import express from "express";
import Anthropic from "@anthropic-ai/sdk";

const app = express();
app.use(express.json());

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
});

app.post("/api/breakdown", async (req, res) => {
  const { goal } = req.body;

  if (!goal) {
    return res.status(400).json({ error: "Goal is required" });
  }

  try {
    const message = await anthropic.messages.create({
      model: "claude-haiku-4-5",
      max_tokens: 1024,
      system: `You are a project management assistant. When given a project goal, break it down into 5-7 actionable subtasks. Return ONLY valid JSON in this exact format, with no other text:
{
  "tasks": [
    {
      "title": "Short task title",
      "description": "One sentence describing what to do",
      "timeEstimate": "Estimated time (e.g., '30 minutes', '2 hours')"
    }
  ]
}`,
      messages: [
        {
          role: "user",
          content: `Break down this project goal into actionable subtasks: ${goal}`,
        },
      ],
    });

    const responseText = message.content[0].text;
    const parsed = JSON.parse(responseText);
    res.json(parsed);
  } catch (error) {
    console.error("API Error:", error.message);
    res.status(500).json({ error: "Failed to generate task breakdown" });
  }
});

const PORT = 3001;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
Add the server startup script to package.json

You need a simple command to start your backend server. Node.js has a built-in --env-file flag that loads your .env file automatically, so you don't need any extra packages.

  • Open package.json in Cursor.
  • Find the "scripts" section. It currently looks like this:
"scripts": {
  "dev": "vite",
  "build": "vite build",
  "preview": "vite preview"
}
  • Add the dev:server script so the section looks like this:
"scripts": {
  "dev": "vite",
  "dev:server": "node --env-file=.env server.js",
  "build": "vite build",
  "preview": "vite preview"
}

What does this script do?

  • node --env-file=.env server.js tells Node.js to load the variables from your .env file before running server.js.
  • This means your ANTHROPIC_API_KEY is available as process.env.ANTHROPIC_API_KEY inside the server without needing any third-party packages.
  • Save package.json.
  • Start the server by running this command in your Cursor terminal:
npm run dev:server
  • You should see Server running on port 3001 printed in the terminal.

Don't see the server running message?

  • If you see an error about ANTHROPIC_API_KEY, make sure your .env file is in the root of task-breakdown (same folder as server.js) and contains your actual API key.
  • If you see a syntax error, double check that your server.js matches the full code in the double-check tab above.
  • If the port is already in use, close any other terminals that might be running a server and try again.

Help me debug my server startup error.

  • Once the server is running, press Ctrl+C in the terminal to stop it. You'll start both the frontend and backend together in the next step.

Your backend is ready to receive requests and talk to Claude. Next up, you'll connect the frontend form to this server so the whole app works end-to-end.

Connect Frontend to Backend

Your backend server is ready and waiting for requests. Now you need the frontend to actually talk to it.

In this step, you will configure Vite to forward API requests to your backend, then write the JavaScript that sends the form data and displays the AI-generated task breakdown.

In this step, get ready to:
  • Configure Vite's proxy so the frontend can reach your Express backend.
  • Write the JavaScript that sends the user's goal to the API and renders the results.
  • Run both servers and see AI-generated task cards appear in the browser.
Configure Vite to proxy API requests

Your frontend runs on one port and your backend runs on another. Browsers block requests between different ports by default for security reasons.

Vite's built-in proxy solves this. It intercepts any request starting with /api and forwards it to your Express server automatically.

  • Create a new file called vite.config.js in the root of your task-breakdown folder, then paste the following code:
import { defineConfig } from "vite";

export default defineConfig({
  server: {
    proxy: {
      // Forward any request starting with /api to the Express backend
      "/api": {
        target: "http://localhost:3001",
        changeOrigin: true,
      },
    },
  },
});

What does this code do?

The server.proxy config tells Vite's dev server to intercept requests. Any request your frontend makes to /api/breakdown gets silently forwarded to http://localhost:3001/api/breakdown where your Express server is listening.

This means your frontend code can call /api/breakdown as if it lived on the same server. No cross-origin errors.

  • Save vite.config.js.

You will not see a visible change yet. The proxy takes effect once the frontend JavaScript makes a request to /api.

Write the frontend JavaScript

The src/main.js file currently has Vite's default template code. You will replace it with the logic that captures the form submission, calls your API, and displays the task breakdown.

  • Open src/main.js in Cursor and delete all of its contents.
  • Paste the following code that imports the stylesheet and grabs references to the DOM elements:
import "./style.css";

// Grab references to the DOM elements we'll interact with
const form = document.getElementById("task-form");
const goalInput = document.getElementById("goal-input");
const submitBtn = document.getElementById("submit-btn");
const resultsSection = document.getElementById("results");
const tasksContainer = document.getElementById("tasks-container");
const errorMessage = document.getElementById("error-message");

What does this code do?

  • import "./style.css" tells Vite to bundle and apply the CSS file. This is how Vite handles styles in JavaScript-driven projects.
  • Each getElementById call stores a reference to an HTML element you built in index.html. Caching these references avoids searching the DOM repeatedly later.
  • Below the DOM references, paste the form submission handler:
form.addEventListener("submit", async (event) => {
  event.preventDefault();

  const goal = goalInput.value.trim();
  if (!goal) return;

  // Show loading state
  submitBtn.disabled = true;
  submitBtn.textContent = "Breaking it down...";
  resultsSection.classList.add("hidden");
  errorMessage.classList.add("hidden");
  tasksContainer.innerHTML = "";

  try {
    // Send the goal to our Express backend
    const response = await fetch("/api/breakdown", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ goal }),
    });

    if (!response.ok) {
      throw new Error(`Server error: ${response.status}`);
    }

    const data = await response.json();
    displayTasks(data.tasks);
  } catch (error) {
    errorMessage.textContent = `Something went wrong: ${error.message}. Please try again.`;
    errorMessage.classList.remove("hidden");
  } finally {
    // Reset button regardless of success or failure
    submitBtn.disabled = false;
    submitBtn.textContent = "Break It Down";
  }
});

What does this code do?

  • event.preventDefault() stops the browser from refreshing the page when the form submits.
  • The loading state disables the button and changes its text so the user knows something is happening.
  • fetch("/api/breakdown") sends a POST request with the goal text. Thanks to the Vite proxy, this reaches your Express server on port 3001.
  • The finally block always runs, restoring the button whether the request succeeded or failed.
  • Below the event listener, paste the function that renders the task cards:
function displayTasks(tasks) {
  tasksContainer.innerHTML = "";

  // Create a styled card for each task
  tasks.forEach((task, index) => {
    const card = document.createElement("div");
    card.className = "task-card";
    card.innerHTML = `
      <h3>${index + 1}. ${task.title}</h3>
      <p>${task.description}</p>
      <span class="time-estimate">${task.timeEstimate}</span>
    `;
    tasksContainer.appendChild(card);
  });

  resultsSection.classList.remove("hidden");
}

What does this code do?

  • displayTasks() loops through the array of tasks that Claude returned and creates a styled card for each one.
  • Each card shows a numbered title, a description, and a time estimate. These match the .task-card styles you wrote in src/style.css.
  • After appending all cards, the function removes the hidden class from the results section to make it visible.
  • Save src/main.js.

✔️ Awesome, I've got everything!

Great. Double check you have saved src/main.js before moving on.

ⓧ I'd like to double check the full code

Here is the complete src/main.js file. Make sure yours matches exactly:

import "./style.css";

const form = document.getElementById("task-form");
const goalInput = document.getElementById("goal-input");
const submitBtn = document.getElementById("submit-btn");
const resultsSection = document.getElementById("results");
const tasksContainer = document.getElementById("tasks-container");
const errorMessage = document.getElementById("error-message");

form.addEventListener("submit", async (event) => {
  event.preventDefault();

  const goal = goalInput.value.trim();
  if (!goal) return;

  submitBtn.disabled = true;
  submitBtn.textContent = "Breaking it down...";
  resultsSection.classList.add("hidden");
  errorMessage.classList.add("hidden");
  tasksContainer.innerHTML = "";

  try {
    const response = await fetch("/api/breakdown", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ goal }),
    });

    if (!response.ok) {
      throw new Error(`Server error: ${response.status}`);
    }

    const data = await response.json();
    displayTasks(data.tasks);
  } catch (error) {
    errorMessage.textContent = `Something went wrong: ${error.message}. Please try again.`;
    errorMessage.classList.remove("hidden");
  } finally {
    submitBtn.disabled = false;
    submitBtn.textContent = "Break It Down";
  }
});

function displayTasks(tasks) {
  tasksContainer.innerHTML = "";

  tasks.forEach((task, index) => {
    const card = document.createElement("div");
    card.className = "task-card";
    card.innerHTML = `
      <h3>${index + 1}. ${task.title}</h3>
      <p>${task.description}</p>
      <span class="time-estimate">${task.timeEstimate}</span>
    `;
    tasksContainer.appendChild(card);
  });

  resultsSection.classList.remove("hidden");
}
Run both servers and test the app

Your app needs two processes running at the same time: the Vite frontend dev server and the Express backend. Each gets its own terminal.

  • In the terminal where your backend is already running, confirm it still shows Server running on port 3001. If you stopped it, start it again by running:
npm run dev:server
  • Open a second terminal in Cursor by clicking the + icon in the terminal panel.
  • Start the Vite dev server in this new terminal by running:
npm run dev
  • Open your browser to http://localhost:5173. You should see the styled Task Breakdown Assistant form.
  • Type a project goal into the textarea, for example: Build a company blog.
  • Click Break It Down.

After a few seconds, you should see 5-7 styled task cards appear below the form. Each card has a numbered title, a description, and a time estimate.

Not seeing task cards?

  • Check that BOTH terminals are running. One should show Vite's output, the other should show Server running on port 3001.
  • If you see a network error, make sure vite.config.js is saved in the project root (not inside src/). Restart the Vite dev server after adding the config file.
  • If the backend returns a 500 error, check that your .env file contains a valid API key and the backend was started with npm run dev:server (which loads the env file).

Help me debug why my task breakdown app is not returning results.

Your app is fully functional. You have a modern frontend calling an AI-powered backend, with Vite handling the bridge between them. Next up, you will extend this with priority levels and a copy button in the Secret Mission.

Secret mission

Add Priority Levels and Copy to Clipboard

Your task breakdowns work, but they treat every subtask as equally important. Real project managers prioritize ruthlessly. In this mission, you will teach Claude to assign priority levels and add a way to export the results.

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 locally with minimal API costs. The free credits Anthropic provides for new accounts cover many requests at Claude Haiku 4.5 pricing.

Resources you used:

  • Local project files (the task-breakdown folder on your Desktop).
  • Anthropic API key stored in your .env file.
  • Anthropic Console account.

Keep everything running

No action needed. Choose this if you're still actively building or want to experiment with the app further.

  • Your project files stay in the task-breakdown folder on your Desktop.
  • Your Anthropic API key remains in .env. There are no ongoing costs unless you make many API requests. A typical request uses roughly 500-1000 tokens total, costing a fraction of a cent ($1 per million input tokens, $5 per million output tokens).
  • Node.js remains installed on your machine for future projects.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your files and API key stay in place for when you return.

  • In each terminal where your servers are running, press Ctrl+C to stop the process.
  • Your project files and .env key remain on disk. When you're ready to resume, open the task-breakdown folder in Cursor and run npm run dev and npm run dev:server in separate terminals.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Stop any running servers by pressing Ctrl+C in each terminal.
  • Delete the project folder. On macOS/Linux, run:
rm -rf ~/Desktop/task-breakdown

On Windows, run:

rmdir /s /q %USERPROFILE%\Desktop\task-breakdown
  • Delete your Anthropic API key by going to console.anthropic.com, navigating to API Keys, and clicking the delete or revoke option next to the key you created for this project.

Nice Work!

Nice Work!

You just built a fully functional AI-powered productivity tool from scratch. Your Task Breakdown Assistant takes vague project goals and turns them into structured, actionable task lists using the Claude API.

You've learned how to:

  • Built a modern web application using Vite for hot module replacement and an Express backend server that securely proxies API requests.
  • Integrated the Claude AI API using the official Anthropic SDK with modern JavaScript patterns like ES modules, async/await, and environment variables.
  • Engineered an AI prompt that returns structured, actionable task breakdowns from plain-text project goals.
  • Secret Mission: Added priority-based color coding and a copy-to-clipboard button for exporting tasks.

Ready to quiz yourself?