Imagine someone hands you a random topic and says, "Speak for 60 seconds. Go." Most people freeze. It's not a knowledge problem, it's a practice problem. The best way to get better at thinking on your feet is to practice often, with topics you didn't choose yourself.
That's exactly what MinuteTalk AI does. It's an impromptu speaking trainer that gives you an AI-generated topic, starts a 60-second countdown, and reveals talking points when you get stuck.
In this first part of the tutorial, you'll build the app from scratch:
- A React frontend (Vite + Tailwind CSS) with a category picker, a countdown timer, and a hints panel
- A FastAPI backend that generates topics and hints with Groq's Llama 3.1 model
- A Mangum handler so the same backend can run on AWS Lambda later
When you're done, you'll have a working app on your local machine. In Part 2, you'll deploy it to AWS Amplify frontend hosting, Lambda, and API Gateway, with no Docker and no IAM CLI setup.
How the App Works
Before you write any code, it helps to see the shape of the whole system:
The flow is simple:
- You pick a category, such as Technology or History.
- The frontend sends
POST /api/topicto the backend. - The backend asks Groq to generate a speaking topic and a few talking points.
- The frontend shows the topic and starts a 60-second timer.
- If you get stuck, you reveal the hints. You can also ask the AI to explain the topic in plain language.
The nice part about this architecture is that the backend is just a FastAPI app. It runs on your laptop with uvicorn, and it runs in AWS Lambda with Mangum the same code, no changes.
Prerequisites
To follow along, you need:
- Node.js 18+ and npm for the frontend
- Python 3.11+ for the backend
- A free Groq API key from the Groq Console
- Basic familiarity with React hooks and Python
You don't need an AWS account for this part. You only need AWS when you deploy in Part 2.
Project Structure
Create a folder called minutetalk-ai with this layout:
minutetalk-ai/
├── backend/
│ ├── app/
│ │ └── main.py # FastAPI app + Groq + Mangum
│ ├── tests/
│ │ └── test_api.py
│ └── requirements.txt
├── src/
│ ├── App.jsx # React component
│ ├── main.jsx # React entry point
│ └── index.css # Tailwind styles
├── public/
├── index.html
├── package.json
├── vite.config.js
└── tailwind.config.js
Step 1: Build the Backend
The backend is a single FastAPI module. It exposes three endpoints:
-
GET /api/health— returns the service status -
POST /api/topic— generates a topic and hints for a category -
POST /api/explain— explains a topic in plain language
Start with the dependencies:
fastapi>=0.109.0
mangum>=0.17.0
uvicorn>=0.27.0
groq>=0.4.0
pydantic>=2.5.0
python-dotenv>=1.0.0
Define the API with Pydantic models
FastAPI uses Pydantic models to validate requests and responses:
from pydantic import BaseModel, Field
class TopicRequest(BaseModel):
category: str = Field(..., description="The category ID")
class TopicResponse(BaseModel):
topic: str
hints: list[str] = []
category: str
Call Groq to generate a topic
The groq package gives you a typed client. You send a system prompt and a user prompt, and you get back the model's text:
from groq import Groq
client = Groq(api_key=os.environ.get("GROQ_API_KEY"))
response = client.chat.completions.create(
model="llama-3.1-8b-instant",
messages=[
{
"role": "system",
"content": (
"You are a precise topic generator. Output ONLY a single "
"concise sentence or question, under 20 words."
),
},
{
"role": "user",
"content": "Generate a thought-provoking technology topic.",
},
],
temperature=0.8,
max_tokens=150,
)
topic = response.choices[0].message.content.strip()
Two details matter here:
-
temperature=0.8keeps the output creative but coherent. -
max_tokens=150keeps each response short, because a speaking topic should fit in one line.
You generate hints the same way, with a second call that asks for 3-4 bullet points.
Add a fallback when Groq is unavailable
Your app shouldn't break if the API key is missing or Groq is down. Keep a small list of hand-written topics per category, and use them when the AI call fails:
@app.post("/api/topic", response_model=TopicResponse)
async def generate_topic(request: TopicRequest):
category = request.category.lower()
if os.environ.get("GROQ_API_KEY"):
try:
topic, hints = await generate_topic_with_groq(category)
return TopicResponse(topic=topic, hints=hints, category=category)
except Exception as e:
logger.warning(f"Groq failed, using fallback: {e}")
topic, hints = get_fallback_topic(category)
return TopicResponse(topic=topic, hints=hints, category=category)
This graceful degradation is what makes the app testable without spending API credits.
Prepare for AWS Lambda with Mangum
The backend runs as a normal FastAPI app locally. To run the same app in AWS Lambda later, wrap it in a Mangum handler:
from mangum import Mangum
app = FastAPI()
# ... routes ...
handler = Mangum(app, lifespan="off")
That's the entire Lambda integration. In Part 2, you'll point Lambda's handler at app.main.handler, and API Gateway will proxy HTTP requests into FastAPI.
Step 2: Build the Frontend
The frontend is a single React component. It keeps the UI simple: pick a category, get a topic, watch the timer.
Set up Vite and Tailwind
Scaffold the project with Vite and add Tailwind:
npm create vite@latest . -- --template react
npm install
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Point Tailwind at your source files in tailwind.config.js:
export default {
content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}"],
theme: { extend: {} },
plugins: [],
}
Manage state with hooks
The component tracks four things: the selected category, the current topic, the time left, and whether hints are visible.
const [view, setView] = useState('select') // 'select' | 'topic'
const [topic, setTopic] = useState('')
const [hints, setHints] = useState([])
const [timeLeft, setTimeLeft] = useState(60)
const [isTimerRunning, setIsTimerRunning] = useState(false)
Fetch a topic from the backend
When you click a category, the frontend posts to the backend and stores the response:
const fetchTopic = async (category) => {
const response = await fetch(`${API_URL}/api/topic`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category }),
})
const data = await response.json()
setTopic(data.topic)
setHints(data.hints || [])
setView('topic')
}
API_URL comes from a Vite environment variable, which you set in a .env file:
VITE_API_URL=http://localhost:8000
Build the 60-second timer
The timer is a useEffect that ticks down once per second. When it reaches zero, it stops itself:
useEffect(() => {
if (isTimerRunning && timeLeft > 0) {
const timer = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
setIsTimerRunning(false)
return 0
}
return prev - 1
})
}, 1000)
return () => clearInterval(timer)
}
}, [isTimerRunning, timeLeft])
The clearInterval cleanup matters: it prevents the timer from leaking when the component re-renders or unmounts.
Style with Tailwind utility classes
You don't need a component library. Tailwind classes give the app a polished, dark-theme look:
<h1 className="text-5xl font-bold bg-gradient-to-r from-sky-400 to-purple-400 bg-clip-text text-transparent">
MinuteTalk AI
</h1>
<button className="btn-primary">🎤 Start Speaking</button>
The app uses a 2-column grid on mobile and a 4-column grid on desktop for the category cards:
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{CATEGORIES.map((category) => (
<button key={category.id} onClick={() => handleCategorySelect(category)}>
{category.icon} {category.name}
</button>
))}
</div>
Step 3: Run the App Locally
You run two servers during development: the backend API and the frontend dev server.
Start the backend
cd backend
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements-dev.txt
export GROQ_API_KEY=your_key_here # Windows: $env:GROQ_API_KEY="your_key_here"
python -m uvicorn app.main:app --reload --port 8000
Start the frontend
In a second terminal:
npm run dev
Open http://localhost:5173, pick a category, and you'll see a generated topic with the timer running. If you didn't set GROQ_API_KEY, the app still works, it just serves the built-in fallback topics.
Step 4: Run the Tests
The project ships with tests for both sides so you can refactor with confidence.
Backend tests use FastAPI's TestClient, which simulates requests without starting a server:
cd backend
PYTHONPATH=.. python -m pytest tests/ -v
Frontend tests use Jest and Testing Library. They mock fetch so no network calls happen:
npm test -- --watchAll=false
What's Next
You now have a complete, working full-stack AI app on your machine. You built:
- A FastAPI backend that talks to Groq and degrades gracefully
- A React frontend with a category picker, timer, and hints
- A Mangum handler that makes the backend Lambda-ready
In Part 2: How to Deploy a Full-Stack AI App on AWS Amplify, you'll take this exact code and deploy it: the React app to Amplify Hosting, the FastAPI backend to AWS Lambda, and an HTTP API Gateway in front of it. You'll also learn how to avoid the two most common deployment gotchas: the pydantic_core import error and the frontend "Failed to fetch" error.


Top comments (0)