Build an AI Flashcard Generator

Build a full-stack app with React, Express, MongoDB, and Claude AI.

Introduction

30 Second Summary

You know those moments when you're staring at a wall of study notes, wishing someone would just turn them into neat flashcards? What if an AI could do that for you in seconds?

In this project, you will build a full-stack web app that takes messy notes, sends them to Claude AI, and gets back clean question-and-answer flashcards stored in a cloud database.

What You'll Build

You'll build an AI Flashcard Generator with a React frontend, Express.js backend, MongoDB Atlas database, and Claude AI integration working together as one application.

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

  • A React frontend with a form for pasting notes and a card-flip UI for reviewing flashcards.
  • An Express.js REST API backend with routes for generating and retrieving flashcard decks.
  • A MongoDB Atlas cloud database storing your flashcard data persistently with Mongoose schemas.
  • Secret Mission: Add a Quiz Mode that presents flashcards one at a time and tracks your score.

Are there any prerequisites?

This project assumes basic familiarity with React (components, state, props) and JavaScript/Node.js. You don't need to be an expert, but you should be comfortable reading JSX and working in a terminal.

Set Up Accounts and Scaffold the Project

Your flashcard app needs four layers to work: a React frontend, an Express backend, a MongoDB database, and the Claude AI API.

Before you can write any code, you need a place to store data and an AI service to generate flashcards. In this step, you will create both accounts and set up the project folder structure.

In this step, get ready to:
  • Create a free MongoDB Atlas cluster and get your connection string.
  • Generate an Anthropic API key for Claude.
  • Scaffold the project folder with a React client and Express server.
Create a MongoDB Atlas free cluster

Your flashcards need a home. MongoDB Atlas is a cloud database service that gives you a free cluster to store and retrieve data without managing any servers.

  • Open your browser and go to mongodb.com/cloud/atlas.
  • Click Try Free and create an account (no credit card required).
  • Once you are logged in, click Create to start building a new cluster.
  • Select the M0 free tier (shared cluster).
  • Choose any cloud provider (AWS is a good default) and a region close to you.
  • Click Create Cluster. The cluster takes 1-3 minutes to provision.

What is a cluster?

A cluster is a group of servers that store your database. The M0 free tier gives you 512 MB of storage, which is more than enough for this project.

While the cluster provisions, set up your database access credentials.

  • In the left sidebar, click Database Access.
  • Click Add New Database User.
  • Enter a username and password you will remember. Write these down somewhere safe.
  • For privileges, keep the default Read and write to any database.
  • Click Add User.

Next, allow your computer to connect to the database.

  • In the left sidebar, click Network Access.
  • Click Add IP Address.
  • Click Add Current IP Address to whitelist your machine, then click Confirm.

Finally, grab the connection string your server will use to talk to MongoDB.

  • Go back to Database in the left sidebar.
  • Click Connect on your cluster.
  • Select Drivers as the connection method.
  • Copy the connection string. It looks something like mongodb+srv://username:<password>@cluster0.xxxxx.mongodb.net/.
  • Replace <password> in the connection string with the database user password you just created.
  • Save this full connection string somewhere accessible. You will need it shortly.

Keep your connection string handy

Paste the connection string into a temporary note or text file. You will add it to your project's .env file in a few minutes.

Get your Anthropic API key

The Anthropic API gives your backend server access to Claude. You will use Claude Haiku 4.5 to generate flashcards from raw notes.

  • Open console.anthropic.com in your browser.
  • Sign in or create an account if you do not have one yet.
  • Navigate to Settings in the left sidebar or account menu.
  • Find the API Keys section.
  • Click Create Key to generate a new API key.
  • Give it a name like flashcard-app and confirm.
  • Copy the key immediately and paste it into a secure note. You will only see it once.

What about cost?

Claude Haiku 4.5 costs $1 per million input tokens and $5 per million output tokens. Generating 10 flashcards uses a tiny fraction of a cent.

New accounts come with $5 in free credits, so this project costs you nothing.

Can't find Settings or API Keys?

Make sure you are in the Anthropic Console (console.anthropic.com), not the Claude chat interface (claude.ai). The API key management lives in the developer console, not the chat product.

help me find where to create an API key in the Anthropic Console

Scaffold the project

Your project has two halves: a server folder for the Express backend and a client folder for the React frontend. You will create both now.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create the project folder and server subfolder by running these commands:
mkdir flashcard-app
cd flashcard-app
mkdir server
  • Scaffold the React client with Vite by running:
npm create vite@latest client -- --template react

What does this command do?

This uses Vite to scaffold a ready-to-go React project inside a client folder. Vite is a fast build tool that handles bundling, hot reload, and dev server setup for you.

  • Move into the server folder and initialize it with npm by running:
cd server
npm init -y

This creates a default package.json in the server folder. You need to update it to use ES modules and add custom scripts.

  • Press Cmd+Space (macOS) or the Windows key (Windows), type Cursor, and press Enter to open Cursor.
  • In Cursor, click File in the top menu bar.
  • Click Open Folder.
  • Select the flashcard-app folder on your Desktop, then click Open.
  • In the Explorer sidebar, expand the server folder and open package.json.
  • Replace the entire contents of package.json with the following:
{
  "name": "flashcard-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node index.js",
    "dev": "node --watch index.js"
  }
}

What does this configuration do?

  • "type": "module" tells Node.js to use ES module syntax (import/export) instead of the older require() style.
  • The dev script uses node --watch which automatically restarts the server when you save changes.
  • Save package.json by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Open the terminal inside Cursor by pressing Ctrl+` (the backtick key, above Tab).
  • Navigate to the server folder and install the backend dependencies by running:
cd server
npm install express mongoose cors dotenv @anthropic-ai/sdk

What are these packages?

  • express is the web framework for creating API routes.
  • mongoose connects to MongoDB and lets you define data schemas.
  • cors allows your frontend to talk to your backend across different ports.
  • dotenv loads environment variables from a .env file.
  • @anthropic-ai/sdk is the official library for calling the Claude API.
  • Confirm the packages installed by opening package.json in Cursor. You should see a dependencies section listing all five packages.

Install command failed?

Make sure you are inside the server folder when running npm install. Run pwd to check your current directory. If you see flashcard-app without /server at the end, run cd server first.

help me fix npm install errors in my flashcard project

Now create the .env file that stores your secret credentials.

  • In the Explorer sidebar in Cursor, right-click the server folder and select New File.
  • Name the file .env and press Enter.
  • Add the following content to the file:
MONGODB_URI=your-mongodb-connection-string-here
ANTHROPIC_API_KEY=your-api-key-here
PORT=3001
  • Replace your-mongodb-connection-string-here with the full connection string you copied from MongoDB Atlas (the one with your password already substituted in).
  • Replace your-api-key-here with the Anthropic API key you generated earlier.
  • Save the file by pressing Cmd+S (macOS) or Ctrl+S (Windows).

Why a .env file?

The .env file keeps secrets out of your code. The dotenv package loads these values into your server at runtime. Never commit this file to version control.

  • Verify your project structure by running this in the Cursor terminal:
cd ..
ls

You should see client and server listed in the output.

✔️ Awesome, I've got everything!

Great. Make sure you have saved your .env file with your real MongoDB connection string and Anthropic API key filled in.

ⓧ I'd like to double check the full code

Here is what your server/package.json should look like after installing dependencies:

{
  "name": "flashcard-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node index.js",
    "dev": "node --watch index.js"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.110.0",
    "cors": "^2.8.5",
    "dotenv": "^16.4.7",
    "express": "^4.19.2",
    "mongoose": "^9.7.3"
  }
}

And here is your server/.env file (with your real values substituted in):

MONGODB_URI=your-mongodb-connection-string-here
ANTHROPIC_API_KEY=your-api-key-here
PORT=3001

Your accounts are set up and your project is scaffolded. Next up, you will build the Express backend that connects to MongoDB and handles API requests.

Build the Backend API

Your project is scaffolded, your database is live, and your API key is ready. Now it is time to write the server code that ties everything together.

In this step, you will create the Express server that connects to MongoDB Atlas, defines a data schema for flashcard decks, and exposes two API routes. One route generates flashcards using Claude AI. The other retrieves saved decks from the database.

In this step, get ready to:
  • Set up Express with CORS, JSON parsing, and a MongoDB connection.
  • Create POST and GET API routes for generating and retrieving flashcards.
  • Test both routes to confirm they work.
Set up the Express server and database connection

Every backend starts with a foundation. You need to import your packages, create the Express app, attach middleware, and connect to your database before you can define routes.

  • In Cursor, create a new file called index.js inside your server folder.
  • Add the server foundation by pasting this code into server/index.js:
import express from 'express';
import cors from 'cors';
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import Anthropic from '@anthropic-ai/sdk';

dotenv.config();

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

// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI)
  .then(() => console.log('Connected to MongoDB Atlas'))
  .catch((err) => console.error('MongoDB connection error:', err));

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

What does this code do?

This sets up the entire server skeleton in one go.

  • dotenv.config() loads variables from your .env file (like MONGODB_URI and ANTHROPIC_API_KEY) into process.env.
  • app.use(cors()) allows your React frontend to make requests to this server without being blocked by the browser.
  • app.use(express.json()) parses incoming JSON request bodies so you can access them with req.body.
  • mongoose.connect() opens a connection to your MongoDB Atlas cluster using the connection string from .env.
  • Save server/index.js.
  • Open a terminal in your server folder and start the server by running:
npm run dev
  • You should see two lines in your terminal: Connected to MongoDB Atlas and Server running on port 3001.

Seeing a MongoDB connection error?

  • Double-check that the MONGODB_URI in your .env file is the full connection string from Atlas, with your actual password replacing the placeholder.
  • Make sure your current IP address is whitelisted in MongoDB Atlas under Network Access.
  • Confirm there are no extra spaces or quotes around the value in .env.

Help me debug my MongoDB connection error.

Now that the server connects to your database, you need to define what your data looks like and set up the AI client.

  • In server/index.js, find the line const PORT = process.env.PORT || 3001; and add the following code above it:
// Mongoose Schema
const deckSchema = new mongoose.Schema({
  title: { type: String, required: true },
  cards: [{
    question: { type: String, required: true },
    answer: { type: String, required: true }
  }],
  createdAt: { type: Date, default: Date.now }
});

const Deck = mongoose.model('Deck', deckSchema);

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

What does this code do?

This defines the shape of your data and initializes the AI client.

  • deckSchema tells Mongoose that each deck has a title (required string), a cards array where each card has a question and answer, and a createdAt timestamp that defaults to the current time.
  • Deck is the Mongoose model you will use to create and query deck documents in MongoDB.
  • The Anthropic client reads your ANTHROPIC_API_KEY from the environment so you can call Claude from your routes.
  • Save server/index.js. Your server auto-restarts (thanks to --watch) and should still show the same two log messages without errors.

Why Mongoose instead of the native MongoDB driver?

Mongoose adds schema validation on top of MongoDB. It enforces that every deck has a title and every card has both a question and answer before data reaches the database. Without Mongoose, you would need to write all those checks manually.

Create the API routes

Your server can connect to the database and knows the shape of your data. Now you need to give it something to do. You will add two routes: one that accepts notes from the user, sends them to Claude, and saves the generated flashcards. Another that retrieves all saved decks.

  • In server/index.js, find the line const PORT = process.env.PORT || 3001; and add the following code above it (below the Anthropic client):
// Generate flashcards route
app.post('/api/generate', async (req, res) => {
  try {
    const { notes } = req.body;

    if (!notes || notes.trim().length === 0) {
      return res.status(400).json({ error: 'Notes are required' });
    }

    const message = await anthropic.messages.create({
      model: 'claude-haiku-4-5',
      max_tokens: 1024,
      system: 'You are a flashcard generator. Given study notes or a topic, create 5-10 flashcards. Return ONLY a JSON object with this exact format: {"title": "Brief deck title", "cards": [{"question": "...", "answer": "..."}]}. No other text.',
      messages: [
        { role: 'user', content: notes }
      ],
    });

    const responseText = message.content[0].text;
    const flashcardData = JSON.parse(responseText);

    const deck = new Deck({
      title: flashcardData.title,
      cards: flashcardData.cards,
    });

    await deck.save();

    res.json(deck);
  } catch (error) {
    console.error('Generation error:', error);
    res.status(500).json({ error: 'Failed to generate flashcards' });
  }
});

// Get all decks route
app.get('/api/decks', async (req, res) => {
  try {
    const decks = await Deck.find({}).sort({ createdAt: -1 });
    res.json(decks);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch decks' });
  }
});

What do these routes do?

POST /api/generate is the core of the app. It:

  • Extracts notes from the request body and validates that it is not empty.
  • Sends the notes to Claude Haiku 4.5 with a system prompt instructing it to return a JSON array of flashcards.
  • Parses Claude's JSON response, saves a new Deck document to MongoDB, and returns it to the client.

GET /api/decks retrieves all saved decks from the database, sorted newest first, so your frontend can display them.

  • Save server/index.js. The server auto-restarts. You will test both routes in the next substep.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is what your complete server/index.js should look like at this point:

import express from 'express';
import cors from 'cors';
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import Anthropic from '@anthropic-ai/sdk';

dotenv.config();

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

// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI)
  .then(() => console.log('Connected to MongoDB Atlas'))
  .catch((err) => console.error('MongoDB connection error:', err));

// Mongoose Schema
const deckSchema = new mongoose.Schema({
  title: { type: String, required: true },
  cards: [{
    question: { type: String, required: true },
    answer: { type: String, required: true }
  }],
  createdAt: { type: Date, default: Date.now }
});

const Deck = mongoose.model('Deck', deckSchema);

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

// Generate flashcards route
app.post('/api/generate', async (req, res) => {
  try {
    const { notes } = req.body;

    if (!notes || notes.trim().length === 0) {
      return res.status(400).json({ error: 'Notes are required' });
    }

    const message = await anthropic.messages.create({
      model: 'claude-haiku-4-5',
      max_tokens: 1024,
      system: 'You are a flashcard generator. Given study notes or a topic, create 5-10 flashcards. Return ONLY a JSON object with this exact format: {"title": "Brief deck title", "cards": [{"question": "...", "answer": "..."}]}. No other text.',
      messages: [
        { role: 'user', content: notes }
      ],
    });

    const responseText = message.content[0].text;
    const flashcardData = JSON.parse(responseText);

    const deck = new Deck({
      title: flashcardData.title,
      cards: flashcardData.cards,
    });

    await deck.save();

    res.json(deck);
  } catch (error) {
    console.error('Generation error:', error);
    res.status(500).json({ error: 'Failed to generate flashcards' });
  }
});

// Get all decks route
app.get('/api/decks', async (req, res) => {
  try {
    const decks = await Deck.find({}).sort({ createdAt: -1 });
    res.json(decks);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch decks' });
  }
});

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
Test both routes

Before building the frontend, you should confirm that both API routes work correctly. You will use curl to send requests directly from your terminal.

  • Make sure your server is still running (you should see the connection log in your terminal).
  • Open a second terminal window. Test the generate route by running:
curl -X POST http://localhost:3001/api/generate \
  -H "Content-Type: application/json" \
  -d '{"notes": "Photosynthesis is the process by which plants convert sunlight into energy. It takes place in the chloroplasts. The main inputs are water and carbon dioxide. The outputs are glucose and oxygen."}'

What should I see?

After a few seconds, you should see a JSON response in your terminal containing a title, a cards array with question/answer pairs, a _id field (the MongoDB document ID), and a createdAt timestamp. This confirms Claude generated the flashcards and MongoDB saved them.

  • Now test the GET route by running:
curl http://localhost:3001/api/decks
  • You should see a JSON array containing the deck you just created. This confirms your retrieval route works.

Getting an empty array or an error?

  • If the POST route returns a 500 error, check your terminal where the server is running for a more detailed error message. The most common cause is an invalid ANTHROPIC_API_KEY in your .env file.
  • If you see [] from the GET route, the POST route may not have saved the deck. Run the POST command again and check for errors in the server terminal.
  • If you see a connection refused error from curl, make sure your server is running on port 3001.

Help me debug my API route errors.

Your backend is fully functional. Both routes work, Claude generates structured flashcards, and MongoDB stores them. Next up, you will build the React frontend that lets users interact with this API visually.

Build the React Frontend

Your backend is running and tested. Now it is time to build the interface that your users will actually see and interact with.

In this step, you will build the React frontend that lets users paste study notes, send them to your backend for AI processing, and view the resulting flashcards with a flip-to-reveal interaction.

In this step, get ready to:
  • Create the App component with a form, flashcard display, and saved decks list.
  • Style the interface with a clean card-based layout.
  • Configure the Vite dev server proxy and test the full-stack app end-to-end.
Create the App component

Your React app needs to handle three things: accepting user input, sending it to the backend, and displaying the results. All of this lives in a single App.jsx component.

  • In Cursor, open client/src/App.jsx from the Explorer sidebar.
  • Select all the existing default code and replace it with the following:
import { useState, useEffect } from 'react';
import './App.css';

function App() {
  // State for form input and loading indicator
  const [notes, setNotes] = useState('');
  const [loading, setLoading] = useState(false);
  // State for the currently displayed deck and all saved decks
  const [currentDeck, setCurrentDeck] = useState(null);
  const [decks, setDecks] = useState([]);
  // Tracks which cards have been flipped to show answers
  const [flippedCards, setFlippedCards] = useState({});

  // Fetch saved decks from the backend when the component loads
  useEffect(() => {
    fetchDecks();
  }, []);

  // Retrieves all saved decks from the GET /api/decks endpoint
  async function fetchDecks() {
    try {
      const res = await fetch('/api/decks');
      const data = await res.json();
      setDecks(data);
    } catch (error) {
      console.error('Failed to fetch decks:', error);
    }
  }

  // Handles form submission: sends notes to the backend for AI processing
  async function handleSubmit(e) {
    e.preventDefault();
    if (!notes.trim()) return;

    setLoading(true);
    try {
      const res = await fetch('/api/generate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ notes }),
      });
      const deck = await res.json();
      setCurrentDeck(deck);
      setNotes('');
      setFlippedCards({});
      fetchDecks();
    } catch (error) {
      console.error('Generation failed:', error);
    } finally {
      setLoading(false);
    }
  }

  // Toggles a card between showing the question and the answer
  function toggleCard(index) {
    setFlippedCards(prev => ({ ...prev, [index]: !prev[index] }));
  }

  return (
    <div className="app">
      <h1>AI Flashcard Generator</h1>

      <form onSubmit={handleSubmit} className="input-form">
        <textarea
          value={notes}
          onChange={(e) => setNotes(e.target.value)}
          placeholder="Paste your study notes or enter a topic..."
          rows={6}
        />
        <button type="submit" disabled={loading}>
          {loading ? 'Generating...' : 'Generate Flashcards'}
        </button>
      </form>

      {currentDeck && (
        <div className="deck">
          <h2>{currentDeck.title}</h2>
          <div className="cards-grid">
            {currentDeck.cards.map((card, index) => (
              <div
                key={index}
                className={`card ${flippedCards[index] ? 'flipped' : ''}`}
                onClick={() => toggleCard(index)}
              >
                <p>{flippedCards[index] ? card.answer : card.question}</p>
                <span className="hint">
                  {flippedCards[index] ? 'Answer' : 'Click to reveal'}
                </span>
              </div>
            ))}
          </div>
        </div>
      )}

      {decks.length > 0 && (
        <div className="saved-decks">
          <h2>Saved Decks</h2>
          {decks.map((deck) => (
            <div
              key={deck._id}
              className="deck-item"
              onClick={() => { setCurrentDeck(deck); setFlippedCards({}); }}
            >
              <h3>{deck.title}</h3>
              <p>{deck.cards.length} cards</p>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

export default App;

What does this component do?

This single component handles the entire frontend UI. Here is how it breaks down:

  • Five useState hooks manage the form input, loading state, current deck, all saved decks, and which cards are flipped.
  • useEffect calls fetchDecks() once on mount so the saved decks list populates immediately.
  • handleSubmit POSTs the user's notes to /api/generate, then updates state with the returned deck.
  • toggleCard flips individual cards between showing the question and the answer.
  • The JSX renders a form, a cards grid for the current deck, and a list of previously saved decks.
  • Save App.jsx.

You won't see a change yet because the styling and proxy still need to be configured.

Style the flashcard interface

The default Vite styles need to be replaced with styles for your flashcard layout. The CSS creates a responsive card grid and a visual flip effect.

  • Open client/src/App.css in Cursor.
  • Select all the existing default CSS and replace it with the following:
.app {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
  font-family: system-ui, sans-serif;
}

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

/* Form layout and input styling */
.input-form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-bottom: 2rem;
}

.input-form textarea {
  padding: 1rem;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  font-size: 1rem;
  resize: vertical;
}

.input-form button {
  padding: 0.75rem 1.5rem;
  background: #2563eb;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  cursor: pointer;
}

.input-form button:disabled {
  background: #93c5fd;
  cursor: not-allowed;
}

/* Flashcard grid and card styles */
.deck {
  margin-bottom: 2rem;
}

.cards-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 1rem;
}

.card {
  padding: 1.5rem;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  cursor: pointer;
  min-height: 120px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  transition: background 0.2s;
}

.card:hover {
  background: #f8fafc;
}

.card.flipped {
  background: #eff6ff;
  border-color: #2563eb;
}

.card .hint {
  font-size: 0.75rem;
  color: #94a3b8;
  margin-top: 0.5rem;
}

/* Saved decks section */
.saved-decks {
  border-top: 2px solid #e0e0e0;
  padding-top: 2rem;
}

.deck-item {
  padding: 1rem;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  margin-bottom: 0.5rem;
  cursor: pointer;
}

.deck-item:hover {
  background: #f8fafc;
}

What do these styles do?

The .cards-grid uses CSS Grid with auto-fill to create a responsive layout. Cards automatically wrap to new rows as the window shrinks.

The .card.flipped class adds a blue background and border to visually distinguish cards showing their answer from those still showing the question.

  • Save App.css.

You won't see the styled app yet because the Vite dev server does not know how to reach your Express backend. The next substep fixes that.

Configure the Vite proxy and test the app

Your React app runs on port 5173. Your Express backend runs on port 3001. When the frontend calls /api/generate, the browser sends that request to port 5173 by default. A proxy tells Vite to forward any /api request to your Express server instead.

  • Open client/vite.config.js in Cursor.
  • Replace the entire file contents with the following:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

// Proxy /api requests to the Express backend during development
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': 'http://localhost:3001',
    },
  },
});

Why do you need a proxy?

During development, Vite serves your React app on one port (5173) and Express runs on another (3001). Without a proxy, the browser would block fetch('/api/generate') as a cross-origin request.

The string shorthand '/api': 'http://localhost:3001' tells Vite to forward any request starting with /api to your Express server. Your frontend code uses simple relative paths like /api/decks and everything routes correctly.

  • Save vite.config.js.
  • Make sure your backend server is still running in its terminal (from the previous step).
  • Open a second terminal, navigate into the client folder, and start the Vite dev server by running:
cd client
npm run dev
  • Open your browser to the URL shown in the terminal output (typically http://localhost:5173).

You should see the "AI Flashcard Generator" heading, a textarea, and a blue "Generate Flashcards" button.

  • Type a topic like JavaScript closures into the textarea and click Generate Flashcards.

After a moment, you should see a grid of flashcard cards appear. Click any card to flip between the question and answer.

Not seeing flashcards after clicking Generate?

  • Check both terminals. The Express terminal should show "Connected to MongoDB Atlas" and no errors. The Vite terminal should show the dev server URL.
  • Open your browser's DevTools console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows). Look for network errors pointing to /api/generate. A 500 error usually means your Anthropic API key is invalid.
  • Make sure vite.config.js has the proxy pointing to http://localhost:3001. If you changed the port in your .env, update the proxy to match.

Help me debug why my flashcards aren't generating after clicking the button.

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files: App.jsx, App.css, and vite.config.js.

ⓧ I'd like to double check the full code

Here is the complete code for each file at this point:

import { useState, useEffect } from 'react';
import './App.css';

function App() {
  const [notes, setNotes] = useState('');
  const [loading, setLoading] = useState(false);
  const [currentDeck, setCurrentDeck] = useState(null);
  const [decks, setDecks] = useState([]);
  const [flippedCards, setFlippedCards] = useState({});

  useEffect(() => {
    fetchDecks();
  }, []);

  async function fetchDecks() {
    try {
      const res = await fetch('/api/decks');
      const data = await res.json();
      setDecks(data);
    } catch (error) {
      console.error('Failed to fetch decks:', error);
    }
  }

  async function handleSubmit(e) {
    e.preventDefault();
    if (!notes.trim()) return;

    setLoading(true);
    try {
      const res = await fetch('/api/generate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ notes }),
      });
      const deck = await res.json();
      setCurrentDeck(deck);
      setNotes('');
      setFlippedCards({});
      fetchDecks();
    } catch (error) {
      console.error('Generation failed:', error);
    } finally {
      setLoading(false);
    }
  }

  function toggleCard(index) {
    setFlippedCards(prev => ({ ...prev, [index]: !prev[index] }));
  }

  return (
    <div className="app">
      <h1>AI Flashcard Generator</h1>

      <form onSubmit={handleSubmit} className="input-form">
        <textarea
          value={notes}
          onChange={(e) => setNotes(e.target.value)}
          placeholder="Paste your study notes or enter a topic..."
          rows={6}
        />
        <button type="submit" disabled={loading}>
          {loading ? 'Generating...' : 'Generate Flashcards'}
        </button>
      </form>

      {currentDeck && (
        <div className="deck">
          <h2>{currentDeck.title}</h2>
          <div className="cards-grid">
            {currentDeck.cards.map((card, index) => (
              <div
                key={index}
                className={`card ${flippedCards[index] ? 'flipped' : ''}`}
                onClick={() => toggleCard(index)}
              >
                <p>{flippedCards[index] ? card.answer : card.question}</p>
                <span className="hint">
                  {flippedCards[index] ? 'Answer' : 'Click to reveal'}
                </span>
              </div>
            ))}
          </div>
        </div>
      )}

      {decks.length > 0 && (
        <div className="saved-decks">
          <h2>Saved Decks</h2>
          {decks.map((deck) => (
            <div
              key={deck._id}
              className="deck-item"
              onClick={() => { setCurrentDeck(deck); setFlippedCards({}); }}
            >
              <h3>{deck.title}</h3>
              <p>{deck.cards.length} cards</p>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

export default App;
.app {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
  font-family: system-ui, sans-serif;
}

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

.input-form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-bottom: 2rem;
}

.input-form textarea {
  padding: 1rem;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  font-size: 1rem;
  resize: vertical;
}

.input-form button {
  padding: 0.75rem 1.5rem;
  background: #2563eb;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  cursor: pointer;
}

.input-form button:disabled {
  background: #93c5fd;
  cursor: not-allowed;
}

.deck {
  margin-bottom: 2rem;
}

.cards-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 1rem;
}

.card {
  padding: 1.5rem;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  cursor: pointer;
  min-height: 120px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  transition: background 0.2s;
}

.card:hover {
  background: #f8fafc;
}

.card.flipped {
  background: #eff6ff;
  border-color: #2563eb;
}

.card .hint {
  font-size: 0.75rem;
  color: #94a3b8;
  margin-top: 0.5rem;
}

.saved-decks {
  border-top: 2px solid #e0e0e0;
  padding-top: 2rem;
}

.deck-item {
  padding: 1rem;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  margin-bottom: 0.5rem;
  cursor: pointer;
}

.deck-item:hover {
  background: #f8fafc;
}
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': 'http://localhost:3001',
    },
  },
});

Your full-stack app is working end-to-end. You have a React frontend talking to an Express backend, which calls Claude AI and saves results to MongoDB. Next up, you will take on the Secret Mission and add an interactive quiz mode to your flashcards.

Secret mission

Add Quiz Mode

Your flashcard app generates and displays cards. But passive reading is not the most effective way to study. What if you could quiz yourself on each deck, tracking how many you get right?

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 uses a free MongoDB Atlas cluster and costs less than $0.01 in Claude API usage. There are no significant ongoing costs.

Resources you used:

  • MongoDB Atlas free cluster (M0 tier, free forever).
  • Anthropic API key (billed per usage, you likely spent less than $0.01).
  • Local project files (client and server folders on your Desktop).

Keep everything running

No action needed. Choose this if you're still actively building or plan to extend your flashcard app.

  • Your MongoDB Atlas free cluster costs nothing. It stays active indefinitely.
  • Your Anthropic API key remains active but only charges when you make API calls.
  • Your local project files stay on your Desktop until you decide to move or delete them.

Pause - I'll come back to this later

Free up local resources while keeping your cloud setup intact for later.

  • Stop your Express server by pressing Ctrl+C in the terminal where it is running.
  • Stop your Vite dev server by pressing Ctrl+C in the terminal where it is running.
  • MongoDB Atlas automatically pauses free clusters after 30 days of no connections. No action needed.
  • Your Anthropic API key will not generate charges while unused.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Delete your MongoDB Atlas cluster: Go to cloud.mongodb.com, select your cluster, click the three-dot menu (...), and select Terminate. Confirm the deletion when prompted.
  • Revoke your Anthropic API key: Go to console.anthropic.com, navigate to Settings, then API Keys. Find your key and delete it.
  • Delete your local project files on macOS/Linux by running:
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name e.g. flashcard-generator"]]
  • On Windows, run:
cd ~\Desktop
Remove-Item -Recurse -Force [[PROJECT_FOLDER="your project folder name e.g. flashcard-generator"]]

That's a Wrap!

That's a Wrap!

Nice work! You just built a complete full-stack AI application from scratch, connecting a React frontend to an Express backend, a MongoDB database, and the Claude AI API.

You've learned how to:

  • Build a REST API with Express.js that handles POST and GET requests for generating and retrieving flashcard decks.
  • Connect to a cloud MongoDB Atlas database using Mongoose to persist structured data across sessions.
  • Integrate the Claude AI API to transform unstructured study notes into clean, structured flashcard data returned to your frontend.
  • Secret Mission: Added an interactive quiz mode with shuffled cards and score tracking.

Ready to quiz yourself?