A structured, exercise-driven curriculum that walks you through this codebase piece by piece. Each exercise builds on the previous one, gradually introducing new concepts until you understand the entire stack.
How to use this guide:
- Work through the exercises in order — they build on each other.
- Each exercise has a Goal, Concepts, Steps, and a Verify section.
- Try each exercise yourself before looking at hints or solutions.
- The existing code in this repo is heavily commented — read the comments as you go.
Prerequisites: This guide assumes you can open a terminal and run basic commands. No prior web development experience is required.
Goal: Get a mental map of what lives where in this monorepo.
Concepts: Monorepo architecture, separation of concerns, project layout conventions.
Steps:
-
Open a terminal in the project root directory.
-
List the top-level files and directories. Notice how the project separates
backend/(Python) andfrontend/(JavaScript/TypeScript) into distinct folders. -
Look at these key files and what they do:
File/Directory Purpose backend/Python FastAPI API server frontend/React TypeScript UI application MakefileShortcuts for common commands docker-compose.ymlMulti-container orchestration Dockerfile.backendContainer image for the backend Dockerfile.frontendContainer image for the frontend .devcontainer/VS Code Dev Container configuration .env.exampleTemplate for environment variables README.mdProject documentation TUTORIAL.mdHands-on chatbot tutorial -
Now explore the backend directory structure:
backend/ ├── app/ ← Application code │ ├── __init__.py ← Makes this directory a Python package │ ├── config.py ← Centralized settings │ ├── main.py ← FastAPI app creation and wiring │ └── routers/ ← API endpoint definitions │ ├── __init__.py │ └── health.py ← Health check endpoint ├── tests/ ← Test files │ └── test_health.py └── pyproject.toml ← Python dependencies and metadata -
And the frontend directory structure:
frontend/ ├── src/ ← Application source code │ ├── App.tsx ← Main React component │ ├── App.css ← Component styles │ ├── main.tsx ← Entry point (mounts React) │ ├── index.css ← Global styles │ └── api/ │ └── client.ts ← Typed API fetch helper ├── index.html ← HTML template ├── vite.config.ts ← Vite bundler + proxy configuration └── package.json ← Node.js dependencies
Verify: Can you answer these questions?
- Which directory contains the Python API code?
- Which file is the entry point for the FastAPI application?
- Where are the React components located?
- What file defines the shortcuts for
make dev,make test, etc.?
Answers
backend/app/backend/app/main.pyfrontend/src/Makefile
Goal: Understand what each make command does before you run anything.
Concepts: Makefiles, task automation, development workflows.
Steps:
-
Open
Makefilein your editor and read the comments. -
For each target, identify what shell command it actually runs:
Command What it does Actual shell command make devStarts both services in parallel $(MAKE) -j2 dev-backend dev-frontendmake dev-backend? ? make dev-frontend? ? make test? ? make lint? ? make clean? ? -
Fill in the table yourself before checking the answer.
Verify: Fill in all the "?" cells.
Answers
| Command | What it does | Actual shell command |
|---|---|---|
make dev-backend |
Starts FastAPI with hot reload | cd backend && uv run uvicorn app.main:app --reload --port 8000 |
make dev-frontend |
Starts Vite dev server | cd frontend && npm run dev |
make test |
Runs pytest | cd backend && uv run pytest |
make lint |
Checks Python code quality | cd backend && uv run ruff check . && uv run ruff format --check . |
make clean |
Removes caches and build artifacts | (multiple find and rm commands) |
Goal: Get the application running and see it in your browser.
Concepts: Package managers (uv, npm), virtual environments, development servers.
Steps:
-
Install backend dependencies:
cd backend && uv sync && cd ..
uv syncreadspyproject.toml, creates a virtual environment, and installs all packages. -
Install frontend dependencies:
cd frontend && npm install && cd ..
npm installreadspackage.jsonand downloads packages intonode_modules/. -
Create your environment file:
cp .env.example .env
-
Start both services:
make dev
-
Open your browser to
http://localhost:5173. You should see the React app displaying the health status from the backend. -
In a separate terminal, test the backend API directly:
curl http://localhost:8000/api/health
You should see:
{"status":"ok","version":"0.1.0"}
Verify:
- The frontend at
http://localhost:5173shows "Status: ok" and "Version: 0.1.0". - The curl command returns JSON with
status: "ok".
Goal: Understand how application settings are managed.
Concepts: Environment variables, .env files, Pydantic BaseSettings, type safety.
Steps:
-
Open
backend/app/config.pyand read all comments carefully. -
Answer these questions:
- What Python library provides the
BaseSettingsclass? - What happens if you don't set the
APP_NAMEenvironment variable? - How does Pydantic know to read from a
.envfile? - If you added a field
database_url: str = "", what environment variable would populate it?
- What Python library provides the
-
Open
.env.exampleand compare it with theSettingsclass. Notice how each variable in.env.examplemaps to a field inSettings. -
Experiment: Change the
APP_NAMEvalue in your.envfile to something like"My Awesome App". Restart the backend (make dev-backend) and visithttp://localhost:8000/docs. The Swagger UI title should reflect your change.
Verify:
Answers
pydantic_settings(thepydantic-settingspackage).- It uses the default value
"My App". - The
model_configdictionary has an"env_file"key that specifies the path. DATABASE_URL(Pydantic matches field names to env vars case-insensitively, converting underscores).
Goal: Follow the path of an HTTP request from start to finish.
Concepts: FastAPI app creation, middleware, routers, request lifecycle.
Steps:
-
Open
backend/app/main.pyand read all comments. -
Trace what happens when a browser sends
GET /api/health:Step 1: The request arrives at the FastAPI `app` instance. Step 2: __________ middleware processes the request first. Step 3: FastAPI matches the URL /api/health to a registered __________. Step 4: The __________ function in health.py handles the request. Step 5: The function returns a Python dict, which FastAPI converts to __________. Step 6: The __________ middleware adds headers to the response. Step 7: The response is sent back to the browser. -
Open
backend/app/routers/health.pyand read the comments. -
Notice how the router has no
/apiprefix — that's added inmain.pywhen registering:app.include_router(health.router, prefix="/api")
Verify: Fill in all the blanks.
Answers
- The request arrives at the FastAPI
appinstance. - CORS middleware processes the request first.
- FastAPI matches the URL /api/health to a registered router.
- The health_check function in health.py handles the request.
- The function returns a Python dict, which FastAPI converts to JSON.
- The CORS middleware adds headers to the response.
- The response is sent back to the browser.
Goal: Learn how FastAPI handles startup and shutdown events.
Concepts: Async context managers, yield, application lifecycle.
Steps:
-
Look at the
lifespanfunction inbackend/app/main.py:@asynccontextmanager async def lifespan(app: FastAPI): # Startup logic goes here yield # Shutdown logic goes here
-
This is an async context manager. The key idea:
- Code before
yieldruns when the server starts. - Code after
yieldruns when the server shuts down. yieldis the point where the server is "running" and handling requests.
- Code before
-
Thought exercise: If you needed to connect to a database at startup and disconnect at shutdown, where would each line go? Write pseudocode:
@asynccontextmanager async def lifespan(app: FastAPI): # Where does db = await connect_to_database() go? yield # Where does await db.disconnect() go?
Verify: You understand that yield separates startup from shutdown, and that this pattern ensures cleanup always runs, even if the server crashes.
Goal: Learn why CORS exists and how it's configured.
Concepts: Cross-Origin Resource Sharing, browser security model, middleware.
Steps:
-
Read the CORS middleware configuration in
backend/app/main.py. -
Research the concept: When your frontend (
localhost:5173) makes a request to the backend (localhost:8000), the browser considers this a "cross-origin" request because the ports are different. -
Without CORS headers, the browser blocks the response (even though the server sent it successfully). CORS middleware adds headers like
Access-Control-Allow-Originthat tell the browser "this is allowed." -
Look at the
cors_originssetting inconfig.py. It defaults to["http://localhost:5173"]. This means only the Vite dev server is allowed to make cross-origin requests. -
Thought exercise: In production, what would you set
cors_originsto? Think about what URL your frontend would be deployed at.
Verify: You can explain in your own words why CORS exists and why the backend needs to explicitly allow the frontend's origin.
Goal: Understand how the existing test works, then write a new one.
Concepts: pytest, async testing, ASGI transport, assertions.
Steps:
-
Open
backend/tests/test_health.pyand read all comments. -
Run the existing test:
make testYou should see
1 passed. -
Understand the testing pattern:
ASGITransport(app=app)connects the test client directly to your FastAPI app (no real HTTP server needed).AsyncClientsends requests through that transport.assertstatements verify the response.
-
Your turn: Add a second test to
test_health.pythat verifies the health endpoint returns the correctContent-Typeheader. Here's a skeleton:@pytest.mark.asyncio async def test_health_returns_json_content_type(): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get("/api/health") # TODO: Assert that the content-type header contains "application/json" # Hint: response.headers["content-type"] gives you the header value
-
Run the tests again. Both should pass.
Verify:
Solution
@pytest.mark.asyncio
async def test_health_returns_json_content_type():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/api/health")
assert "application/json" in response.headers["content-type"]Goal: Build a new endpoint from scratch, following the existing patterns.
Concepts: APIRouter, route decorators, JSON responses, router registration.
Steps:
-
Create a new file:
backend/app/routers/info.py -
Following the pattern in
health.py, create:- An
APIRouterinstance. - A
GET /infoendpoint that returns:{ "project": "React + FastAPI Starter", "python_version": "3.12", "framework": "FastAPI" }
- An
-
Register the router in
main.py:- Import the
infomodule. - Add
app.include_router(info.router, prefix="/api").
- Import the
-
Test it with curl:
curl http://localhost:8000/api/info
-
Bonus: Write a test for your new endpoint in
backend/tests/test_info.py.
Verify:
Solution — info.py
from fastapi import APIRouter
router = APIRouter()
@router.get("/info")
async def get_info():
return {
"project": "React + FastAPI Starter",
"python_version": "3.12",
"framework": "FastAPI",
}Solution — main.py changes
Add to imports:
from app.routers import health, infoAdd after the health router registration:
app.include_router(info.router, prefix="/api")Solution — test_info.py
import pytest
from httpx import ASGITransport, AsyncClient
from app.main import app
@pytest.mark.asyncio
async def test_get_info():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/api/info")
assert response.status_code == 200
data = response.json()
assert data["framework"] == "FastAPI"Goal: Understand the chain from HTML to React rendering.
Concepts: DOM, React root, entry points, StrictMode.
Steps:
-
Open
frontend/index.html. Find the<div id="root"></div>element — this is the empty container where React will render the entire application. -
Open
frontend/src/main.tsxand read the comments. This is the first JavaScript that runs. It:- Finds the
#rootdiv in the HTML. - Creates a React "root" inside it.
- Tells React to render the
<App />component.
- Finds the
-
Trace the rendering chain:
Browser loads index.html → index.html has <script> pointing to main.tsx → main.tsx creates a React root in <div id="root"> → main.tsx renders <App /> inside that root → App.tsx defines what the user actually sees -
Thought exercise: What is
StrictModeand why is it useful? (Hint: read the comments inmain.tsx.)
Verify: You can explain the journey from HTML file to visible UI.
Goal: Learn how React manages data that changes over time.
Concepts: useState hook, state variables, re-rendering, TypeScript generics.
Steps:
-
Open
frontend/src/App.tsxand read the comments. -
Find the two
useStatecalls:const [health, setHealth] = useState<HealthResponse | null>(null); const [error, setError] = useState<string | null>(null);
-
For each, identify:
- The state variable (the current value).
- The setter function (used to update the value).
- The initial value (passed to
useState). - The TypeScript type (in the angle brackets).
-
Key concept: When you call a setter (like
setHealth(data)), React:- Stores the new value.
- Re-renders the component with the new value.
- The UI updates automatically.
-
Fill in this table:
State Variable Setter Initial Value Type Health data ? ? ? ? Error message ? ? ? ?
Verify:
Answers
| State Variable | Setter | Initial Value | Type | |
|---|---|---|---|---|
| Health data | health |
setHealth |
null |
HealthResponse | null |
| Error message | error |
setError |
null |
string | null |
Goal: Learn how React loads data when a component first appears.
Concepts: useEffect hook, side effects, dependency arrays, Promise chains.
Steps:
-
Look at the
useEffectcall inApp.tsx:useEffect(() => { apiFetch<HealthResponse>('/health') .then(setHealth) .catch((err) => setError(err.message)); }, []);
-
Break it down:
useEffect(fn, deps)— runsfnafter the component renders.- The empty array
[]means "run this only once, on first mount." apiFetch<HealthResponse>('/health')calls the backend API..then(setHealth)stores the response in state on success..catch(...)stores the error message in state on failure.
-
Experiment: Open your browser's DevTools (F12) → Network tab. Reload the page. You should see a request to
/api/health. Click it to inspect the request and response. -
Thought exercise: What would happen if the dependency array was
[health]instead of[]?
Verify:
Answer
With [health] as the dependency, the effect would re-run every time health changes. Since the effect itself updates health, this would create an infinite loop: fetch → update state → re-render → fetch → update state → ...
The empty array [] means "run once on mount" which is the correct behavior for initial data loading.
Goal: Learn how the typed fetch wrapper works.
Concepts: TypeScript generics, async/await, fetch API, error handling.
Steps:
-
Open
frontend/src/api/client.tsand read all comments. -
The function signature uses a TypeScript generic
<T>:export async function apiFetch<T>(path: string, options?: RequestInit): Promise<T>
This means: "The caller tells me what type
Tis, and I promise to return that type." -
Trace what happens when
apiFetch<HealthResponse>('/health')is called:path="/health"- The URL becomes
"/api/health"(prepended with/api). Content-Type: application/jsonheader is set.- The browser sends a GET request.
- Vite's proxy forwards it to
http://localhost:8000/api/health. - If the response status is 2xx, parse JSON and return as
HealthResponse. - If not, throw an error.
-
Thought exercise: Why can't we use
apiFetchfor streaming responses? (Hint: read the NOTE in the module comment.)
Verify: You can explain why the helper prepends /api and how the Vite proxy makes it work.
Goal: Learn how React shows different UI based on state.
Concepts: JSX, conditional rendering, short-circuit evaluation.
Steps:
-
Look at the return statement in
App.tsx. There are three conditional blocks:{error && <p className="error">Error: {error}</p>} {!health && !error && <p className="loading">Loading...</p>} {health && (<div className="health">...</div>)}
-
For each state combination, determine what renders:
healtherrorWhat shows? nullnull? null"Network error"? {status: "ok", ...}null? -
The
&&operator in JSX is called short-circuit evaluation:true && <Component />renders the component.false && <Component />renders nothing.- This is React's primary pattern for conditional rendering.
Verify:
Answers
health |
error |
What shows? |
|---|---|---|
null |
null |
"Loading..." |
null |
"Network error" |
"Error: Network error" |
{status: "ok", ...} |
null |
The health status card |
Goal: Create a new React component that fetches from your custom endpoint.
Concepts: Component creation, state management, API integration, TypeScript interfaces.
Steps:
-
If you completed Exercise 2.6, you have a
/api/infoendpoint. Now display its data in the frontend. -
In
frontend/src/App.tsx, add:- A TypeScript interface for the info response.
- A new
useStatefor the info data. - A
useEffectthat fetches/infousingapiFetch. - JSX to display the project name and framework.
-
Try it yourself before looking at the hint.
Verify: The page shows both the health status and the project info.
Hint
interface InfoResponse {
project: string;
python_version: string;
framework: string;
}
// Inside the App function, add:
const [info, setInfo] = useState<InfoResponse | null>(null);
// Inside the useEffect (or add a second useEffect):
apiFetch<InfoResponse>('/info').then(setInfo);
// In the JSX return, add:
{info && (
<div className="health">
<p>Project: <span className="status">{info.project}</span></p>
<p>Framework: <span className="version">{info.framework}</span></p>
</div>
)}Goal: Learn how the frontend talks to the backend during development.
Concepts: Development proxy, cross-origin requests, Vite configuration.
Steps:
-
Open
frontend/vite.config.tsand read the comments. -
The key configuration is:
proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true, }, }
-
This means: any request to a URL starting with
/apithat arrives at the Vite dev server (port 5173) gets forwarded to the backend (port 8000). -
Trace the full path:
Browser: fetch("/api/health") → Request goes to localhost:5173 (Vite dev server) → Vite sees the path starts with "/api" → Vite forwards the request to localhost:8000/api/health → FastAPI processes and responds → Vite passes the response back to the browser -
Experiment: Stop the backend (
Ctrl+Conmake dev-backend). Refresh the frontend. You should see an error because the proxy has nowhere to forward requests. -
Thought exercise: In production, you wouldn't use a Vite proxy. How would you handle the frontend-to-backend communication instead?
Verify:
Answer
In production, you'd typically:
- Serve both from the same domain using a reverse proxy like nginx.
- Configure nginx to route
/api/*to the backend and everything else to the static frontend files. - Or use separate domains with CORS headers properly configured.
Goal: Understand how the backend is containerized.
Concepts: Docker images, layers, caching, multi-stage copies.
Steps:
-
Open
Dockerfile.backendand read all comments. -
Identify the purpose of each instruction:
Instruction Purpose FROM python:3.12-slim? COPY --from=ghcr.io/astral-sh/uv:latest ...? WORKDIR /app? COPY backend/pyproject.toml backend/uv.lock ./? RUN uv sync --frozen --no-dev? COPY backend/ .? CMD [...]? -
Key concept — Layer caching: Docker caches each instruction. If a layer hasn't changed, Docker reuses the cached version. This is why we copy dependency files before source code — dependencies change rarely, source code changes often.
Verify:
Answers
| Instruction | Purpose |
|---|---|
FROM python:3.12-slim |
Use a minimal Python base image |
COPY --from=... |
Copy the uv binary from its official image |
WORKDIR /app |
Set the working directory for subsequent commands |
COPY ... pyproject.toml ... uv.lock |
Copy dependency files first (for caching) |
RUN uv sync --frozen --no-dev |
Install production dependencies |
COPY backend/ . |
Copy application source code |
CMD [...] |
Define the default startup command |
Goal: Learn how multiple containers work together.
Concepts: Docker Compose services, port mapping, volumes, depends_on.
Steps:
-
Open
docker-compose.ymland read all comments. -
For each service, identify:
- What Dockerfile it uses.
- What port it exposes.
- What volumes are mounted.
- What the startup command is.
-
Key concepts:
ports: "8000:8000"means "map port 8000 on your machine to port 8000 in the container."volumes: ./backend:/appmounts your local code into the container so changes take effect immediately.depends_on: backendensures the backend starts before the frontend.
-
Experiment: If you have Docker installed:
make docker-up # Build and start all services make docker-down # Stop all services
Verify: You can explain the difference between running services with make dev (local) vs make docker-up (containerized).
Goal: Understand how the development environment is fully containerized.
Concepts: Dev Containers, VS Code integration, reproducible environments.
Steps:
-
Read these three files in order:
.devcontainer/Dockerfile— Builds the development image..devcontainer/devcontainer.json— Configures VS Code integration..devcontainer/post-create.sh— Installs dependencies on first setup.
-
Understand the key differences from production Dockerfiles:
- Dev Container includes development tools (git, curl, make).
- Creates a non-root user for security.
- Has VS Code extensions pre-configured.
- Installs ALL dependencies (including dev dependencies).
-
In
devcontainer.json, identify:- Which ports are forwarded.
- What VS Code extensions are installed.
- What settings are configured for Python and TypeScript.
- When
post-create.shruns.
Verify: You understand that a Dev Container provides a complete, reproducible development environment that works the same on any machine.
Goal: Learn how secrets and configuration are managed.
Concepts: Environment variables, .env files, .gitignore, security.
Steps:
-
Open
.env.exampleand read the comments. -
Open
.gitignoreand confirm that.envis listed (never committed to git). -
Understand the flow:
.env.example (template, committed to git) → Developer copies to .env (personal config, NOT committed) → Pydantic Settings reads .env at startup → Values available as settings.app_name, settings.debug, etc. -
Why this matters: API keys and passwords should NEVER be in git. The
.envpattern keeps them local and out of version control, while.env.exampledocuments what variables are needed. -
Thought exercise: What would happen if someone accidentally committed their
.envfile with a real API key?
Verify: You can explain why .env is in .gitignore and how the .env.example template helps new developers.
Goal: Learn how async tests work with FastAPI.
Concepts: pytest, pytest-asyncio, ASGITransport, test isolation.
Steps:
-
Open
backend/tests/test_health.pyand read all comments. -
The test infrastructure has three key pieces:
- pytest — discovers and runs tests.
- pytest-asyncio — adds support for
asynctest functions. - httpx + ASGITransport — lets you test FastAPI without a real server.
-
Understand why this approach is better than starting a real server:
- Speed: No server startup time.
- Isolation: Each test gets a fresh connection.
- Reliability: No port conflicts or network issues.
-
Your turn: Write a test that verifies a non-existent endpoint returns a 404:
@pytest.mark.asyncio async def test_nonexistent_endpoint_returns_404(): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get("/api/nonexistent") # What should you assert here?
Verify:
Solution
assert response.status_code == 404Run make test — all tests should pass, including your new one.
Goal: Learn how to test endpoints that accept request bodies.
Concepts: POST requests, JSON request bodies, test coverage.
Steps:
-
This exercise prepares you for the chatbot tutorial. Create a simple echo endpoint first.
-
Create
backend/app/routers/echo.py:from fastapi import APIRouter from pydantic import BaseModel router = APIRouter() class EchoRequest(BaseModel): message: str @router.post("/echo") async def echo(request: EchoRequest): return {"echo": request.message}
-
Register it in
main.py(same pattern as health router). -
Your turn: Write a test for this endpoint. You'll need:
client.post("/api/echo", json={"message": "hello"})to send a POST with JSON.- Assertions on the status code and response body.
-
Run the tests.
Verify:
Solution — test_echo.py
import pytest
from httpx import ASGITransport, AsyncClient
from app.main import app
@pytest.mark.asyncio
async def test_echo():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post("/api/echo", json={"message": "hello"})
assert response.status_code == 200
assert response.json()["echo"] == "hello"
@pytest.mark.asyncio
async def test_echo_missing_message():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post("/api/echo", json={})
# FastAPI returns 422 Unprocessable Entity for validation errors
assert response.status_code == 422Goal: Understand the full journey of data from user action to screen update.
Concepts: Full-stack request lifecycle, client-server architecture.
Steps:
-
Map the complete lifecycle when the page loads:
┌─────────────────────────────────────────────────────────────┐ │ BROWSER │ │ │ │ 1. Browser loads index.html │ │ 2. index.html loads main.tsx │ │ 3. main.tsx creates React root, renders <App /> │ │ 4. App renders with health=null (shows "Loading...") │ │ 5. useEffect fires → apiFetch("/health") │ │ 6. fetch("/api/health") sent to Vite dev server │ │ │ └────────────────────────┬────────────────────────────────────┘ │ HTTP GET /api/health ▼ ┌─────────────────────────────────────────────────────────────┐ │ VITE DEV SERVER (5173) │ │ │ │ 7. Vite proxy matches /api prefix │ │ 8. Forwards request to localhost:8000 │ │ │ └────────────────────────┬────────────────────────────────────┘ │ HTTP GET /api/health ▼ ┌─────────────────────────────────────────────────────────────┐ │ FASTAPI BACKEND (8000) │ │ │ │ 9. CORS middleware processes the request │ │ 10. FastAPI matches /api/health → health.router │ │ 11. health_check() runs → returns {"status": "ok", ...} │ │ 12. FastAPI serializes dict to JSON response │ │ 13. CORS middleware adds response headers │ │ │ └────────────────────────┬────────────────────────────────────┘ │ JSON response ▼ ┌─────────────────────────────────────────────────────────────┐ │ BROWSER │ │ │ │ 14. apiFetch receives response, parses JSON │ │ 15. .then(setHealth) stores data in React state │ │ 16. React re-renders App with new health value │ │ 17. Conditional rendering shows the health status card │ │ 18. User sees "Status: ok" and "Version: 0.1.0" │ │ │ └─────────────────────────────────────────────────────────────┘ -
For each step, identify which file in the codebase is responsible.
Verify: You can explain every step of this diagram from memory.
Goal: Learn how code quality tools work.
Concepts: Linting, code formatting, ruff, code standards.
Steps:
-
Run the linter:
make lint
This runs two ruff commands:
ruff check .— looks for code issues (unused imports, style violations, etc.).ruff format --check .— checks if code formatting matches the standard.
-
Experiment: Introduce a linting error. Open
backend/app/routers/health.pyand add an unused import:import os # This is unused
-
Run
make lintagain. Ruff should flag the unused import. -
Fix it by removing the import. Run
make lintagain to confirm it passes. -
Auto-formatting: You can also auto-fix formatting issues:
cd backend && uv run ruff format .
Verify: You can run the linter, understand its output, and fix issues it reports.
Goal: Apply everything you've learned to build a real feature.
Concepts: All previous concepts + API integration, streaming, real-time UI.
Steps:
Now that you understand every piece of this codebase, you're ready for the main event. Open TUTORIAL.md and follow it step by step to build a streaming LLM chatbot.
The tutorial will have you:
- Add the Anthropic SDK to the backend.
- Create a streaming chat endpoint.
- Build a React chat UI that handles streaming responses.
- Connect everything together.
You'll use every concept from this learning guide:
- Configuration management (Exercise 2.1) — for the API key.
- Router creation (Exercise 2.6) — for the chat endpoint.
- API communication (Exercise 3.4) — for calling the backend.
- React state (Exercise 3.2) — for managing the message list.
- useEffect patterns (Exercise 3.3) — for auto-scrolling.
- Testing (Exercise 8.2) — for verifying your endpoint.
Verify: You complete the TUTORIAL.md and have a working chatbot.
| Module | Key Concepts |
|---|---|
| 1. Environment | Project structure, Makefiles, setup |
| 2. Backend | FastAPI, routers, middleware, CORS, lifespan |
| 3. Frontend | React components, useState, useEffect, TypeScript |
| 4. Proxy | Vite proxy, frontend-backend communication |
| 5. Docker | Dockerfiles, layers, caching, images |
| 6. Dev Container | Reproducible development environments |
| 7. Configuration | Environment variables, .env files, security |
| 8. Testing | pytest, async tests, ASGI transport |
| 9. Full Stack | Complete request lifecycle |
| 10. Code Quality | Linting, formatting, ruff |
| 11. Chatbot | API integration, streaming, real-time UI |
Use this table to find which file teaches which concept:
| File | Concepts |
|---|---|
backend/app/config.py |
Pydantic Settings, environment variables, type safety |
backend/app/main.py |
FastAPI creation, CORS middleware, router registration, lifespan |
backend/app/routers/health.py |
APIRouter, async endpoints, JSON responses |
backend/tests/test_health.py |
pytest, async testing, ASGI transport, assertions |
frontend/src/main.tsx |
React bootstrapping, DOM mounting, StrictMode |
frontend/src/App.tsx |
React components, useState, useEffect, conditional rendering |
frontend/src/api/client.ts |
TypeScript generics, fetch API, error handling |
frontend/vite.config.ts |
Development proxy, Vite plugins |
Makefile |
Task automation, shell commands |
Dockerfile.backend |
Docker layers, caching, multi-stage copies |
Dockerfile.frontend |
Node.js containers, npm ci |
docker-compose.yml |
Multi-container orchestration, volumes, ports |
.devcontainer/ |
Reproducible dev environments, VS Code integration |
backend/pyproject.toml |
Python dependency management, project metadata |
.env.example |
Configuration templates, secrets management |