Build an AI Roadmap Generator
Build a Node.js web app that generates personalized coding roadmaps with Claude AI.
Introduction
30 Second Summary
When you're learning to program, the hardest part isn't writing code. It's knowing what to learn next and in what order.
In this project, you will build an AI-powered web application that generates personalized learning roadmaps for programmers using the Claude API.
What You'll Build
A Node.js web app where users enter their programming goal, skill level, and timeframe, then receive a structured, phased learning roadmap generated by Claude AI in real time.
By the end of this project, you'll have:
- A Node.js/Express API server that communicates with the Claude API to generate roadmaps.
- A clean HTML/CSS/JavaScript frontend with a form for user input and a streaming display area.
- A well-engineered system prompt that consistently produces structured, actionable roadmaps.
- Secret Mission: Add the ability to export your generated roadmap as a downloadable Markdown file.
Are there any prerequisites?
You'll need a basic understanding of JavaScript and HTML. You'll also need an Anthropic Console account, which is free to create. The account requires adding credits for API usage, but generating a single roadmap costs less than $0.01.
Before We Start
Before We Start...
Set Up Your Development Environment
Before you can build the AI roadmap generator, you need a JavaScript runtime that can execute your server code and communicate with the Claude API.
In this step, you will install Node.js, create your project folder, and install the packages your app depends on.
In this step, get ready to:
- Verify that Node.js v18 or higher is installed on your machine.
- Create and initialize the project folder with npm.
- Install the Anthropic SDK and Express dependencies.
Install Node.js
Node.js is the JavaScript runtime that powers your server. The Anthropic SDK requires version 18 or higher.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal (macOS) or Command Prompt (Windows), and pressing Enter.
- Check your Node.js version by running this command:
node --version
✔️ I see version 18 or higher
Your terminal shows something like v22.x.x or v24.x.x. You are good to go.
ⓧ I see an older version
Your terminal shows a version number below 18 (for example, v16.x.x). You need to upgrade to version 18 or higher.
- Go to https://nodejs.org in your browser.
- Download the LTS version (currently v24.x).
- Run the installer. On macOS, open the .pkg file. On Windows, run the .msi file and accept the defaults.
- Close and reopen your terminal, then run node --version again to confirm the update.
ⓧ Command not found
Node.js is not installed on your machine yet. Follow these steps to install it.
- Go to https://nodejs.org in your browser.
- Download the LTS version (currently v24.x).
- Run the installer. On macOS, open the .pkg file. On Windows, run the .msi file and accept the defaults.
- Close and reopen your terminal, then run node --version again to confirm it shows v18 or higher.
Create your project folder and initialize it
Every Node.js project starts with a folder and a package.json file that tracks your project's name, version, and dependencies.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and initialize it by running these commands:
mkdir roadmap-generator
cd roadmap-generator
npm init -y
npm pkg set type=module
npm pkg set main=server.js
npm pkg set scripts.start="node server.js"
What do these commands do?
- mkdir roadmap-generator creates a new folder for your project.
- cd roadmap-generator moves your terminal into that folder.
- npm init -y creates a package.json file with default values.
- npm pkg set type=module enables ES module syntax (import/export) instead of older CommonJS (require).
- npm pkg set main=server.js tells Node.js the entry point file is server.js.
- npm pkg set scripts.start="node server.js" adds a start script so you can run your server with npm start.
- Confirm the file was created by running:
ls
You should see package.json listed in the output.
Don't see package.json?
- Make sure you ran cd roadmap-generator before running npm init -y. If you are in the wrong folder, the file will be created elsewhere.
- On Windows, if mkdir does not work, try md roadmap-generator instead.
Help me troubleshoot my project setup.
Install dependencies
Your project needs two packages: the Anthropic SDK to communicate with Claude, and Express to run your web server.
- Install both packages by running this command:
npm install @anthropic-ai/sdk express
What are these packages?
- @anthropic-ai/sdk is the official Anthropic SDK for calling the Claude API from JavaScript.
- express is a lightweight web framework that makes it easy to create API endpoints and serve static files.
- Confirm the installation succeeded by running:
ls
You should now see node_modules, package.json, and package-lock.json in the output.
Seeing permission errors or install failures?
- On macOS, if you see EACCES permission errors, try prefixing the command with sudo.
- Make sure you are inside the roadmap-generator folder (not your Desktop). Run pwd to check your current location.
- If you see network errors, check your internet connection and try the command again.
Help me fix my npm install error.
✔️ Awesome, I've got everything!
Great. Double check you have node_modules, package.json, and package-lock.json in your roadmap-generator folder.
ⓧ I'd like to double check the full code
Open package.json and confirm the key fields match this structure:
{
"name": "roadmap-generator",
"version": "1.0.0",
"type": "module",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.100.0",
"express": "^4.21.0"
}
}
What if I see extra fields?
Your file may have additional fields like description, keywords, or author that npm init added by default. These are harmless and will not affect your project.
Your development environment is ready. Next up, you will get your Anthropic API key so your app can communicate with Claude.
Get Your Anthropic API Key
Your server needs a way to authenticate with Claude's AI models. The Anthropic API uses an API key to verify that requests come from your account.
In this step, you will create an API key on the Anthropic Console and store it safely in your project using an environment variable file.
In this step, get ready to:
- Create an Anthropic API key for your project.
- Store the key in a secure environment file.
- Install dotenv to load your environment variables.
Sign up for the Anthropic Console and create an API key
The Anthropic Console is where you manage your API keys and monitor usage. You need an account and an active API key before your app can talk to Claude.
- Open your browser and go to console.anthropic.com.
- Sign up for a new account or log in if you already have one.
- Once logged in, click API Keys in the left sidebar.
- Click Create Key.
- Give your key a name (e.g. roadmap-generator) and confirm creation.
- Copy the key immediately. You will not be able to see it again after closing this dialog.
What is an API key?
An API key is a unique string that identifies your application to a service. When your server sends a request to the Claude API, it includes this key so Anthropic knows which account to charge.
Think of it like a password for your app. Keep it secret and never commit it to version control.
Copy your key now
Anthropic only shows your full API key once, at the moment of creation. If you close the dialog without copying it, you will need to create a new key.
Create the environment file
Hardcoding secrets directly in your source code is risky. A .env file keeps your API key separate from your code so it never accidentally gets shared.
- In your terminal (inside the roadmap-generator folder), create the .env file by running this command:
touch .env
- Open .env in your code editor and add this line, 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 environment variables as key-value pairs. The ANTHROPIC_API_KEY variable name is what the Anthropic SDK looks for automatically when connecting to the API.
- Save .env.
- Confirm the file exists by running:
ls -a
You should see .env listed among your project files. The -a flag shows hidden files that start with a dot.
Don't see .env in the list?
Make sure you ran the command from inside the roadmap-generator folder. Run pwd to check your current location.
Help me figure out what went wrong with my .env file
Install dotenv to load your environment variables
Node.js does not read .env files on its own. The dotenv package loads those variables into your app when the server starts.
- Install the dotenv package by running:
npm install dotenv
- Confirm the installation succeeded by checking your package.json. Run:
cat package.json
You should see dotenv listed in the dependencies section alongside @anthropic-ai/sdk and express.
Install failed?
Make sure you are connected to the internet and running the command from inside the roadmap-generator folder. If you see a permissions error, try running the command with sudo on macOS/Linux.
Help me troubleshoot my npm install error
✔️ Awesome, I've got everything!
Great. Double check you have saved your .env file with your real API key in place of the placeholder.
ⓧ I'd like to double check the full code
Here is what your .env file should look like (with your own key substituted):
ANTHROPIC_API_KEY=your-api-key-here
And your package.json should now include all three dependencies:
{
"name": "roadmap-generator",
"version": "1.0.0",
"type": "module",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.100.0",
"dotenv": "^16.5.0",
"express": "^4.21.0"
}
}
Your API key is securely stored and ready to use. Next up, you will build the Express server that sends requests to Claude and streams the roadmap back to the browser.
Build the Express Server with Claude Integration
Your project folder has all the dependencies installed and your API key stored safely. Now it's time to build the brains of the application.
In this step, you'll create the server that receives a learner's goal, sends it to Claude with a carefully designed prompt, and streams the response back in real time.
In this step, get ready to:
- Set up an Express server with middleware and the Anthropic client.
- Design a system prompt that produces structured, phased roadmaps.
- Build a streaming API endpoint that sends Claude's response to the browser in real time.
Create the server file and set up Express
The server is the middleman between your frontend and the Claude API. It receives form data, forwards it to Claude, and relays the AI's response back to the browser.
- In your editor, create a new file called server.js in the root of your roadmap-generator folder.
- Add the following code to set up the imports, Express server, and Anthropic client:
import 'dotenv/config';
import express from 'express';
import Anthropic from '@anthropic-ai/sdk';
const app = express();
const port = 3000;
app.use(express.json());
app.use(express.static('public'));
const client = new Anthropic();
What does this code do?
The first line loads your .env file so the Anthropic SDK can read your API key from the environment. No need to pass the key manually.
The express.json() middleware parses incoming JSON request bodies. The express.static('public') middleware serves your frontend files from a public/ folder (which you'll create in the next step).
The new Anthropic() call creates the client. It automatically reads ANTHROPIC_API_KEY from your environment variables.
You won't see any output yet. The next chunk defines the AI prompt that powers the roadmap.
Design the system prompt
A system prompt tells Claude how to behave and what format to use. The quality of your prompt directly determines the quality of the roadmap output.
- Add the following SYSTEM_PROMPT constant below your client line in server.js:
const SYSTEM_PROMPT = `You are an expert programming mentor who creates detailed, actionable learning roadmaps.
When given a programming goal, skill level, and timeframe, generate a structured roadmap with the following format:
# [Goal] Learning Roadmap
## Overview
A brief 2-3 sentence summary of the learning path.
## Phase 1: [Phase Title] (Week X-Y)
**Goal:** [What the learner will achieve in this phase]
- Topic 1: Brief description
- Topic 2: Brief description
- Topic 3: Brief description
**Milestone:** [Concrete deliverable or skill checkpoint]
## Phase 2: [Phase Title] (Week X-Y)
...continue the pattern...
## Final Milestone
[What the learner will be able to do/build after completing the roadmap]
## Recommended Resources
- Resource 1
- Resource 2
- Resource 3
Rules:
- Create 3-6 phases depending on the timeframe
- Each phase should have 3-5 topics
- Time estimates should be realistic and add up to the given timeframe
- Tailor complexity to the skill level (Beginner/Intermediate/Advanced)
- Include practical project suggestions in milestones
- Be specific about technologies, tools, and concepts`;
What makes this prompt effective?
This prompt uses three key techniques. First, it assigns Claude a clear role (expert programming mentor). Second, it provides an exact output template so every roadmap follows the same structure. Third, it sets rules that keep outputs consistent regardless of the input.
The format uses Markdown headings and bullet points, which makes the raw text readable even before any HTML rendering.
You still won't see output from this code. The next chunk builds the endpoint that uses this prompt.
Build the streaming API endpoint
When a user submits the form, the browser sends a POST request to your server. Your server calls Claude and streams the response back using Server-Sent Events (SSE). This lets the user see the roadmap appear word by word instead of waiting for the entire response.
- Add the following route handler and server start code below your SYSTEM_PROMPT in server.js:
app.post('/api/generate-roadmap', async (req, res) => {
const { goal, level, timeframe } = req.body;
if (!goal || !level || !timeframe) {
return res.status(400).json({ error: 'Missing required fields: goal, level, timeframe' });
}
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
const stream = client.messages.stream({
model: 'claude-haiku-4-5',
max_tokens: 4096,
system: SYSTEM_PROMPT,
messages: [
{
role: 'user',
content: `Create a learning roadmap for the following:\n- Goal: ${goal}\n- Current Skill Level: ${level}\n- Available Timeframe: ${timeframe}\n\nPlease generate a detailed, phase-by-phase roadmap.`
}
]
});
stream.on('text', (text) => {
res.write(`data: ${JSON.stringify({ type: 'text', content: text })}\n\n`);
});
stream.on('end', () => {
res.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
res.end();
});
stream.on('error', (error) => {
res.write(`data: ${JSON.stringify({ type: 'error', content: error.message })}\n\n`);
res.end();
});
} catch (error) {
res.write(`data: ${JSON.stringify({ type: 'error', content: error.message })}\n\n`);
res.end();
}
});
app.listen(port, () => {
console.log(`Roadmap Generator running at http://localhost:${port}`);
});
What does this endpoint do?
The endpoint first validates that all three fields (goal, level, timeframe) are present. If any are missing, it returns a 400 error.
The three setHeader calls configure the response as a Server-Sent Events stream. This tells the browser to keep the connection open and read data as it arrives.
The client.messages.stream() call sends your prompt to claude-haiku-4-5 and returns a stream object. The .on('text') listener fires each time Claude generates a new chunk of text, which gets written to the response immediately.
Why Claude Haiku 4.5?
Claude Haiku 4.5 is the fastest and most cost-effective model in the Claude family. For generating text-based roadmaps (no complex reasoning required), it produces excellent results at $1 per million input tokens and $5 per million output tokens. A typical roadmap costs less than $0.01 to generate.
- Save server.js.
- Verify your server starts correctly by running this command in your terminal:
node server.js
You should see this message printed in your terminal:
Roadmap Generator running at http://localhost:3000
- Stop the server by pressing Ctrl+C in the terminal. You'll restart it later once the frontend is built.
Server won't start?
- If you see an error about ANTHROPIC_API_KEY, double check your .env file exists in the same folder as server.js and contains your key.
- If you see a syntax error, make sure you copied all the code (especially the closing backtick on the template literal and all curly braces).
- If port 3000 is already in use, close any other servers or apps using that port and try again.
help me fix my server.js startup error
✔️ Awesome, I've got everything!
Great. Double check you've saved server.js and confirmed the server starts without errors.
ⓧ I'd like to double check the full code
Here is the complete server.js file. Compare yours against it:
import 'dotenv/config';
import express from 'express';
import Anthropic from '@anthropic-ai/sdk';
const app = express();
const port = 3000;
app.use(express.json());
app.use(express.static('public'));
const client = new Anthropic();
const SYSTEM_PROMPT = `You are an expert programming mentor who creates detailed, actionable learning roadmaps.
When given a programming goal, skill level, and timeframe, generate a structured roadmap with the following format:
# [Goal] Learning Roadmap
## Overview
A brief 2-3 sentence summary of the learning path.
## Phase 1: [Phase Title] (Week X-Y)
**Goal:** [What the learner will achieve in this phase]
- Topic 1: Brief description
- Topic 2: Brief description
- Topic 3: Brief description
**Milestone:** [Concrete deliverable or skill checkpoint]
## Phase 2: [Phase Title] (Week X-Y)
...continue the pattern...
## Final Milestone
[What the learner will be able to do/build after completing the roadmap]
## Recommended Resources
- Resource 1
- Resource 2
- Resource 3
Rules:
- Create 3-6 phases depending on the timeframe
- Each phase should have 3-5 topics
- Time estimates should be realistic and add up to the given timeframe
- Tailor complexity to the skill level (Beginner/Intermediate/Advanced)
- Include practical project suggestions in milestones
- Be specific about technologies, tools, and concepts`;
app.post('/api/generate-roadmap', async (req, res) => {
const { goal, level, timeframe } = req.body;
if (!goal || !level || !timeframe) {
return res.status(400).json({ error: 'Missing required fields: goal, level, timeframe' });
}
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
const stream = client.messages.stream({
model: 'claude-haiku-4-5',
max_tokens: 4096,
system: SYSTEM_PROMPT,
messages: [
{
role: 'user',
content: `Create a learning roadmap for the following:\n- Goal: ${goal}\n- Current Skill Level: ${level}\n- Available Timeframe: ${timeframe}\n\nPlease generate a detailed, phase-by-phase roadmap.`
}
]
});
stream.on('text', (text) => {
res.write(`data: ${JSON.stringify({ type: 'text', content: text })}\n\n`);
});
stream.on('end', () => {
res.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
res.end();
});
stream.on('error', (error) => {
res.write(`data: ${JSON.stringify({ type: 'error', content: error.message })}\n\n`);
res.end();
});
} catch (error) {
res.write(`data: ${JSON.stringify({ type: 'error', content: error.message })}\n\n`);
res.end();
}
});
app.listen(port, () => {
console.log(`Roadmap Generator running at http://localhost:${port}`);
});
Your server is wired up and ready to generate roadmaps. Next up, you'll build the frontend that lets users fill in their goals and see the AI response stream in live.
Build the Frontend Interface
Your server is ready to generate roadmaps, but it needs a way for users to actually interact with it. Right now, there's no interface for someone to type in their programming goal and see the AI's response.
In this step, you'll build the entire frontend: an HTML form to collect user input, CSS to make it look clean and modern, and JavaScript that sends requests to your server and displays the streaming response in real time.
In this step, get ready to:
- Create an HTML page with a form for goal, skill level, and timeframe inputs.
- Style the interface with CSS for a polished look.
- Write JavaScript that streams the AI response and renders it live.
Create the HTML form
Your Express server already serves static files from a public/ folder (thanks to express.static('public') in server.js). You need to create that folder and put your HTML file inside it.
- Create the public folder inside your roadmap-generator project by running this command:
mkdir public
- Confirm the folder exists by running:
ls
You should see public listed alongside server.js, package.json, and node_modules.
- Create a new file called index.html inside the public folder, and paste in the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Roadmap Generator</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header>
<h1>🗺️ AI Roadmap Generator</h1>
<p>Get a personalized learning roadmap powered by AI</p>
</header>
<form id="roadmap-form">
<div class="form-group">
<label for="goal">What do you want to learn?</label>
<input type="text" id="goal" placeholder="e.g., Become a full-stack web developer" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="level">Current Skill Level</label>
<select id="level" required>
<option value="">Select level</option>
<option value="Beginner">Beginner</option>
<option value="Intermediate">Intermediate</option>
<option value="Advanced">Advanced</option>
</select>
</div>
<div class="form-group">
<label for="timeframe">Available Timeframe</label>
<select id="timeframe" required>
<option value="">Select timeframe</option>
<option value="1 month">1 Month</option>
<option value="3 months">3 Months</option>
<option value="6 months">6 Months</option>
<option value="1 year">1 Year</option>
</select>
</div>
</div>
<button type="submit" id="generate-btn">Generate Roadmap</button>
</form>
<div id="roadmap-output" class="hidden">
<div id="roadmap-content"></div>
<button id="export-btn" class="hidden">📄 Export as Markdown</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
What does this HTML do?
- The <form> has three inputs: a text field for the goal, a dropdown for skill level, and a dropdown for timeframe.
- Each <select> element has a blank default option so the user must actively choose a value.
- The #roadmap-output div starts hidden (class="hidden") and only appears once a roadmap is being generated.
- The #export-btn also starts hidden and will appear after a roadmap finishes generating.
- Save public/index.html.
You won't see much in the browser yet. The page needs CSS for styling and JavaScript for interactivity.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Roadmap Generator</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header>
<h1>🗺️ AI Roadmap Generator</h1>
<p>Get a personalized learning roadmap powered by AI</p>
</header>
<form id="roadmap-form">
<div class="form-group">
<label for="goal">What do you want to learn?</label>
<input type="text" id="goal" placeholder="e.g., Become a full-stack web developer" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="level">Current Skill Level</label>
<select id="level" required>
<option value="">Select level</option>
<option value="Beginner">Beginner</option>
<option value="Intermediate">Intermediate</option>
<option value="Advanced">Advanced</option>
</select>
</div>
<div class="form-group">
<label for="timeframe">Available Timeframe</label>
<select id="timeframe" required>
<option value="">Select timeframe</option>
<option value="1 month">1 Month</option>
<option value="3 months">3 Months</option>
<option value="6 months">6 Months</option>
<option value="1 year">1 Year</option>
</select>
</div>
</div>
<button type="submit" id="generate-btn">Generate Roadmap</button>
</form>
<div id="roadmap-output" class="hidden">
<div id="roadmap-content"></div>
<button id="export-btn" class="hidden">📄 Export as Markdown</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
Style the interface with CSS
A clean visual design helps users focus on their inputs and makes the generated roadmap easy to read.
- Create a new file called style.css inside the public folder, and paste in the following code:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f0f4f8;
color: #1a202c;
line-height: 1.6;
min-height: 100vh;
padding: 2rem 1rem;
}
.container {
max-width: 800px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 2rem;
}
header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #2d3748;
}
header p {
color: #718096;
font-size: 1.1rem;
}
form {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
margin-bottom: 2rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #4a5568;
}
.form-group input,
.form-group select {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #4299e1;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
button[type="submit"] {
width: 100%;
padding: 1rem;
background: #4299e1;
color: white;
border: none;
border-radius: 8px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
button[type="submit"]:hover {
background: #3182ce;
}
button[type="submit"]:disabled {
background: #a0aec0;
cursor: not-allowed;
}
#roadmap-output {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
#roadmap-content {
white-space: pre-wrap;
font-size: 0.95rem;
line-height: 1.8;
}
#roadmap-content h1 {
font-size: 1.5rem;
color: #2d3748;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #e2e8f0;
}
#roadmap-content h2 {
font-size: 1.2rem;
color: #4299e1;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
#export-btn {
margin-top: 1.5rem;
padding: 0.75rem 1.5rem;
background: #48bb78;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
#export-btn:hover {
background: #38a169;
}
.hidden {
display: none;
}
.loading {
text-align: center;
color: #718096;
font-style: italic;
}
@media (max-width: 600px) {
.form-row {
grid-template-columns: 1fr;
}
}
What does this CSS do?
- The universal reset (*) removes default browser spacing so your layout is consistent.
- The form gets a white card with rounded corners and a subtle shadow, making it feel like a focused input area.
- The .form-row uses CSS Grid to place the skill level and timeframe dropdowns side by side.
- The .hidden class uses display: none to toggle elements on and off without removing them from the DOM.
- The media query stacks the two dropdowns vertically on screens narrower than 600px.
- Save public/style.css.
- Open public/index.html directly in your browser by pressing Cmd+O (macOS) or Ctrl+O (Windows) and navigating to the file inside your roadmap-generator/public folder.
You should see a centered page with a blue heading, a white card containing the form fields, and a blue Generate Roadmap button.
Don't see any styling?
- Make sure style.css is saved in the same public folder as index.html. The <link> tag in your HTML uses a relative path, so both files must be siblings.
- Check that the filename is exactly style.css (not styles.css or style.CSS).
Help me debug why my CSS isn't loading in the browser.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f0f4f8;
color: #1a202c;
line-height: 1.6;
min-height: 100vh;
padding: 2rem 1rem;
}
.container {
max-width: 800px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 2rem;
}
header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #2d3748;
}
header p {
color: #718096;
font-size: 1.1rem;
}
form {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
margin-bottom: 2rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #4a5568;
}
.form-group input,
.form-group select {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #4299e1;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
button[type="submit"] {
width: 100%;
padding: 1rem;
background: #4299e1;
color: white;
border: none;
border-radius: 8px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
button[type="submit"]:hover {
background: #3182ce;
}
button[type="submit"]:disabled {
background: #a0aec0;
cursor: not-allowed;
}
#roadmap-output {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
#roadmap-content {
white-space: pre-wrap;
font-size: 0.95rem;
line-height: 1.8;
}
#roadmap-content h1 {
font-size: 1.5rem;
color: #2d3748;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #e2e8f0;
}
#roadmap-content h2 {
font-size: 1.2rem;
color: #4299e1;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
#export-btn {
margin-top: 1.5rem;
padding: 0.75rem 1.5rem;
background: #48bb78;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
#export-btn:hover {
background: #38a169;
}
.hidden {
display: none;
}
.loading {
text-align: center;
color: #718096;
font-style: italic;
}
@media (max-width: 600px) {
.form-row {
grid-template-columns: 1fr;
}
}
Add JavaScript for streaming responses
The JavaScript file connects your form to the backend API. When the user submits the form, it sends a request to your Express server and reads the streamed response chunk by chunk so the roadmap appears in real time.
- Create a new file called script.js inside the public folder, and paste in the following code:
const form = document.getElementById('roadmap-form');
const generateBtn = document.getElementById('generate-btn');
const roadmapOutput = document.getElementById('roadmap-output');
const roadmapContent = document.getElementById('roadmap-content');
const exportBtn = document.getElementById('export-btn');
let fullRoadmapText = '';
form.addEventListener('submit', async (e) => {
e.preventDefault();
const goal = document.getElementById('goal').value;
const level = document.getElementById('level').value;
const timeframe = document.getElementById('timeframe').value;
// Disable the button and show loading state
generateBtn.disabled = true;
generateBtn.textContent = 'Generating...';
roadmapOutput.classList.remove('hidden');
roadmapContent.innerHTML = '<p class="loading">Generating your personalized roadmap...</p>';
exportBtn.classList.add('hidden');
fullRoadmapText = '';
try {
// Send the form data to the backend API
const response = await fetch('/api/generate-roadmap', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ goal, level, timeframe })
});
// Set up a reader for the streaming response
const reader = response.body.getReader();
const decoder = new TextDecoder();
roadmapContent.innerHTML = '';
// Read chunks as they arrive from the server
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
// Parse each SSE data line
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.type === 'text') {
fullRoadmapText += data.content;
roadmapContent.textContent = fullRoadmapText;
} else if (data.type === 'done') {
exportBtn.classList.remove('hidden');
} else if (data.type === 'error') {
roadmapContent.innerHTML = `<p style="color: red;">Error: ${data.content}</p>`;
}
} catch (parseError) {
// Skip malformed lines
}
}
}
}
} catch (error) {
roadmapContent.innerHTML = `<p style="color: red;">Failed to generate roadmap. Please check your connection and try again.</p>`;
}
// Re-enable the button after generation completes
generateBtn.disabled = false;
generateBtn.textContent = 'Generate Roadmap';
});
What does this code do?
- The top section grabs references to all the DOM elements the script needs to interact with.
- On form submit, it disables the button (preventing double-clicks), shows a loading message, and fires a POST request to /api/generate-roadmap.
- The response.body.getReader() call sets up a readable stream. This lets the browser process chunks of data as they arrive instead of waiting for the full response.
- Each chunk is decoded and split into lines. Lines prefixed with data: are Server-Sent Events messages from the backend.
- The fullRoadmapText variable accumulates the entire response so it can be exported later.
You won't see this in action yet. The next chunk adds the export feature, and then you'll test everything together in the next step.
- Now add the export button handler at the bottom of public/script.js, directly after the closing }); of the form event listener:
exportBtn.addEventListener('click', () => {
// Build a filename from the goal and today's date
const goal = document.getElementById('goal').value;
const sanitizedGoal = goal.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40);
const date = new Date().toISOString().slice(0, 10);
const filename = `roadmap-${sanitizedGoal}-${date}.md`;
// Create a downloadable Markdown file from the roadmap text
const blob = new Blob([fullRoadmapText], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
What does the export handler do?
- It takes the goal text and sanitizes it into a URL-safe string for the filename (e.g., "Learn Python for data science" becomes learn-python-for-data-science).
- It creates a Blob (a file-like object in JavaScript) containing the full roadmap text.
- A temporary <a> element with a download attribute triggers the browser's file download dialog.
- After the click fires, it cleans up the temporary URL and element to prevent memory leaks.
- Save public/script.js.
You won't see a change in the browser yet. The JavaScript connects to your backend API, so you'll test it once the server is running in the next step.
Seeing a console error about script.js?
- If you opened index.html directly in the browser, you may see a console error about getElementById returning null. This is expected because the <script> tag loads at the end of the body, and the DOM elements should exist. Check that the id attributes in your HTML match the strings in your JavaScript exactly.
- Make sure script.js is saved in the public folder alongside index.html.
Help me debug a script.js console error in my roadmap generator.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
const form = document.getElementById('roadmap-form');
const generateBtn = document.getElementById('generate-btn');
const roadmapOutput = document.getElementById('roadmap-output');
const roadmapContent = document.getElementById('roadmap-content');
const exportBtn = document.getElementById('export-btn');
let fullRoadmapText = '';
form.addEventListener('submit', async (e) => {
e.preventDefault();
const goal = document.getElementById('goal').value;
const level = document.getElementById('level').value;
const timeframe = document.getElementById('timeframe').value;
generateBtn.disabled = true;
generateBtn.textContent = 'Generating...';
roadmapOutput.classList.remove('hidden');
roadmapContent.innerHTML = '<p class="loading">Generating your personalized roadmap...</p>';
exportBtn.classList.add('hidden');
fullRoadmapText = '';
try {
const response = await fetch('/api/generate-roadmap', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ goal, level, timeframe })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
roadmapContent.innerHTML = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.type === 'text') {
fullRoadmapText += data.content;
roadmapContent.textContent = fullRoadmapText;
} else if (data.type === 'done') {
exportBtn.classList.remove('hidden');
} else if (data.type === 'error') {
roadmapContent.innerHTML = `<p style="color: red;">Error: ${data.content}</p>`;
}
} catch (parseError) {
// Skip malformed lines
}
}
}
}
} catch (error) {
roadmapContent.innerHTML = `<p style="color: red;">Failed to generate roadmap. Please check your connection and try again.</p>`;
}
generateBtn.disabled = false;
generateBtn.textContent = 'Generate Roadmap';
});
exportBtn.addEventListener('click', () => {
const goal = document.getElementById('goal').value;
const sanitizedGoal = goal.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40);
const date = new Date().toISOString().slice(0, 10);
const filename = `roadmap-${sanitizedGoal}-${date}.md`;
const blob = new Blob([fullRoadmapText], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
Your frontend is complete. Next up, you'll start the server and test the full application end-to-end with a real AI-generated roadmap.
Design the Prompt and Test
Your server, frontend, and streaming logic are all wired together. The final piece that determines the quality of your app is the system prompt.
A well-designed prompt is the difference between a generic AI response and a structured, actionable roadmap. In this step, you'll review your prompt's design, start the server, and test the full experience end to end.
In this step, get ready to:
- Review the system prompt design and understand its formatting rules.
- Start the server and test with real inputs in the browser.
- Iterate on the prompt for cleaner, more actionable roadmaps.
Review the system prompt design
The SYSTEM_PROMPT in your server.js is what shapes every roadmap Claude generates. Open server.js and scroll to the SYSTEM_PROMPT constant to review it.
Why does the prompt structure matter?
The system prompt uses a technique called few-shot formatting. By showing Claude the exact Markdown structure you want (headers, bold labels, bullet points, phase numbering), you get consistent, predictable output every time.
The "Rules" section at the bottom acts as guardrails. It tells Claude to create 3-6 phases depending on the timeframe, tailor complexity to the skill level, and include practical project milestones. Without these constraints, the output would vary wildly between requests.
Your prompt has three main sections working together:
- A role definition ("expert programming mentor") that sets the tone and expertise level.
- A format template showing the exact Markdown structure Claude should follow for every roadmap.
- A rules section with constraints on phase count, topic count, and how to adapt to different skill levels.
What is prompt engineering?
Prompt engineering is the practice of designing inputs to AI models that produce reliable, high-quality outputs. Instead of hoping the model guesses your intent, you make the desired format and constraints explicit.
Start the server and test in the browser
With your prompt reviewed, it's time to see your full app in action.
- Start your server by running this command in the terminal:
node server.js
You should see this output confirming the server is running:
Roadmap Generator running at http://localhost:3000
Seeing an error about the API key?
If you see an error mentioning the API key or authentication, make sure your .env file is in the root of your roadmap-generator folder (not inside public/) and contains your actual Anthropic API key.
help me debug my server startup error
- Open your web browser.
- Navigate to http://localhost:3000.
You should see the AI Roadmap Generator form with the goal input, skill level dropdown, and timeframe dropdown.
- Type Learn Python for data science in the goal field.
- Select Beginner from the skill level dropdown.
- Select 3 Months from the timeframe dropdown.
- Click Generate Roadmap.
Watch the roadmap stream in real time below the form. You should see text appearing progressively, with Markdown headers, bullet points, and phase breakdowns filling in as Claude generates the response.
What should I see?
The roadmap appears as plain Markdown text (with # headers and - bullet points visible). This is expected. The text streams in chunk by chunk, so you'll see it build up over a few seconds.
The button text changes to "Generating..." while the request is in progress, then reverts to "Generate Roadmap" when streaming completes.
Not seeing any response?
Check your browser's developer console (press Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) for network errors. A 401 error means your API key is invalid. A connection refused error means the server isn't running.
help me troubleshoot my roadmap generator
Iterate on the prompt for better results
Prompt engineering is iterative. Try a few more inputs to see how the prompt handles different scenarios.
- Try an advanced-level goal like Master distributed systems with a 1 Year timeframe.
- Try a short timeframe like 1 Month with a beginner goal to see how Claude compresses the phases.
- Compare the outputs. Notice how the phase count, topic depth, and milestone complexity change based on your inputs.
Tweaking the prompt
If you want different output, edit the SYSTEM_PROMPT in server.js. Try changing the number of phases, adding a section for "Common Pitfalls", or asking Claude to include difficulty ratings per topic. Save the file and restart the server to see changes.
Each time you modify the prompt, stop the server with Ctrl+C in the terminal and run node server.js again.
Why is iteration important?
In production AI applications, prompt design is rarely one-and-done. Teams test prompts against many edge cases and refine constraints until the output is consistently useful. The prompting skills you're building here apply to any LLM integration.
✔️ Awesome, I've got everything!
Great. Make sure your server is still running and you've tested at least two different input combinations.
ⓧ I'd like to double check the full code
Here is the full server.js for reference:
import 'dotenv/config';
import express from 'express';
import Anthropic from '@anthropic-ai/sdk';
const app = express();
const port = 3000;
app.use(express.json());
app.use(express.static('public'));
const client = new Anthropic();
const SYSTEM_PROMPT = `You are an expert programming mentor who creates detailed, actionable learning roadmaps.
When given a programming goal, skill level, and timeframe, generate a structured roadmap with the following format:
# [Goal] Learning Roadmap
## Overview
A brief 2-3 sentence summary of the learning path.
## Phase 1: [Phase Title] (Week X-Y)
**Goal:** [What the learner will achieve in this phase]
- Topic 1: Brief description
- Topic 2: Brief description
- Topic 3: Brief description
**Milestone:** [Concrete deliverable or skill checkpoint]
## Phase 2: [Phase Title] (Week X-Y)
...continue the pattern...
## Final Milestone
[What the learner will be able to do/build after completing the roadmap]
## Recommended Resources
- Resource 1
- Resource 2
- Resource 3
Rules:
- Create 3-6 phases depending on the timeframe
- Each phase should have 3-5 topics
- Time estimates should be realistic and add up to the given timeframe
- Tailor complexity to the skill level (Beginner/Intermediate/Advanced)
- Include practical project suggestions in milestones
- Be specific about technologies, tools, and concepts`;
app.post('/api/generate-roadmap', async (req, res) => {
const { goal, level, timeframe } = req.body;
if (!goal || !level || !timeframe) {
return res.status(400).json({ error: 'Missing required fields: goal, level, timeframe' });
}
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
const stream = client.messages.stream({
model: 'claude-haiku-4-5',
max_tokens: 4096,
system: SYSTEM_PROMPT,
messages: [
{
role: 'user',
content: `Create a learning roadmap for the following:\n- Goal: ${goal}\n- Current Skill Level: ${level}\n- Available Timeframe: ${timeframe}\n\nPlease generate a detailed, phase-by-phase roadmap.`
}
]
});
stream.on('text', (text) => {
res.write(`data: ${JSON.stringify({ type: 'text', content: text })}\n\n`);
});
stream.on('end', () => {
res.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
res.end();
});
stream.on('error', (error) => {
res.write(`data: ${JSON.stringify({ type: 'error', content: error.message })}\n\n`);
res.end();
});
} catch (error) {
res.write(`data: ${JSON.stringify({ type: 'error', content: error.message })}\n\n`);
res.end();
}
});
app.listen(port, () => {
console.log(`Roadmap Generator running at http://localhost:${port}`);
});
Secret mission
Export Roadmap as Markdown
Your roadmap generator creates great content, but it disappears when you close the browser. What if you could save it as a downloadable Markdown file to reference offline, share with others, or import into a notes app?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine. The only cost is Anthropic API usage, which is pay-per-use. If you stop generating roadmaps, you stop paying.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The roadmap-generator project folder on your Desktop.
- Node.js server process (if still running in your terminal).
- Anthropic API key (stored in your .env file and on the Anthropic Console).
Keep everything running
No action needed. Choose this if you plan to keep building on your roadmap generator or experimenting with different prompts.
- Your project files stay on your Desktop in the roadmap-generator folder.
- Your API key remains active on the Anthropic Console. You are only charged when you generate roadmaps.
- If the server is still running in your terminal, you can stop it anytime by pressing Ctrl+C.
Pause - I'll come back to this later
Stop the running server to free up your terminal. Your files and API key remain ready for next time.
- In the terminal where your server is running, press Ctrl+C to stop the process.
- Your roadmap-generator folder and .env file remain on your Desktop for when you return.
- To restart later, open a terminal, navigate to the roadmap-generator folder, and run node server.js.
Delete - I don't want to use this again
Remove all project files and disable your API key so nothing persists.
- In the terminal where your server is running, press Ctrl+C to stop the process.
- Delete the project folder by running the command for your operating system.
macOS
cd ~/Desktop
rm -rf roadmap-generator
Windows
cd ~\Desktop
rmdir /s /q roadmap-generator
- Delete your API key from the Anthropic Console. Go to console.anthropic.com, navigate to your API Keys settings page, and disable or delete the key you created for this project.
Nice Work!
Nice Work!
You just built a fully functional AI-powered Roadmap Generator that turns a programming goal into a structured, phased learning plan. This is exactly how production applications integrate LLM APIs to deliver real value to users.
You've learned how to:
- Build a Node.js/Express backend that integrates with the Claude API to generate AI-powered content.
- Design an effective system prompt that produces structured, phased learning roadmaps with consistent formatting.
- Implement real-time streaming with Server-Sent Events so users see the roadmap generate progressively in the browser.
- Added Markdown export functionality so users can download their generated roadmaps as .md files for offline reference.
Ready to quiz yourself?