A real-time quiz platform built with FastAPI (Python backend) and Astro + Alpine.js (frontend). Perfect for demonstrating Python's capabilities to students!
This is an interactive quiz application designed to close a 3-day Python tutorial for beginners. The goal is to give students a "glimpse" of what's possible with Python and modern web technologies.
Key Features:
- Real-time WebSocket updates
- Host presentation view + participant views
- Multiple choice & short-answer questions
- Timed questions with countdown
- Scoring with streak system
- Real-time leaderboard
- Mobile responsive
- FastAPI - Modern Python web framework
- WebSockets - Real-time communication
- Pydantic - Data validation
- uvicorn - ASGI server
- uv - Fast Python package manager
- Astro 5.x - SSR framework with Node.js adapter
- Alpine.js - Lightweight reactive framework (via @astrojs/alpinejs)
- Tailwind CSS - Styling
- TypeScript - Type safety for Alpine logic modules
python-quiz/
├── backend/ # FastAPI Python backend
│ ├── main.py # Main application & WebSocket routes
│ ├── models.py # Pydantic data models
│ ├── storage.py # In-memory storage
│ ├── scoring.py # Scoring algorithm
│ ├── validation.py # Answer validation (fuzzy matching)
│ ├── game_state.py # Room state management
│ ├── pyproject.toml # Python dependencies (uv)
│ └── .env # Environment configuration
│
└── frontend/ # Astro + Alpine.js frontend
├── src/
│ ├── content/
│ │ ├── config.ts # Content collection schema
│ │ └── test-quiz/ # Example quiz (one folder per quiz)
│ │ ├── question-1.md # Each question is a separate file
│ │ ├── question-2.md # Frontmatter: type, timeLimit, points, etc.
│ │ └── ... # (10 questions total)
│ ├── components/
│ │ ├── QuizHost.astro # Host controls (Alpine markup)
│ │ ├── QuizPlayer.astro # Participant UI (Alpine markup)
│ │ ├── Leaderboard.astro # Real-time leaderboard (Alpine markup)
│ │ └── Timer.astro # Countdown timer (Alpine markup)
│ ├── lib/
│ │ ├── alpine/
│ │ │ ├── quizHost.ts # Alpine component logic
│ │ │ ├── quizPlayer.ts # Alpine component logic
│ │ │ ├── leaderboard.ts # Alpine component logic
│ │ │ └── timer.ts # Alpine component logic
│ │ ├── alpineInit.ts # Global Alpine initialization
│ │ └── websocket.ts # WebSocket client wrapper
│ └── pages/
│ ├── index.astro # Homepage
│ └── room/[roomId]/ # Dynamic room routes
├── astro.config.mjs # Astro + Alpine.js integration
└── .env # Frontend environment variables
- Python 3.12+
- Node.js 18+
- npm 9+
- uv - Install with:
curl -LsSf https://astral.sh/uv/install.sh | sh
-
Navigate to backend directory:
cd backend -
Install dependencies with uv (automatically creates virtual environment):
uv sync
-
Configure environment (optional - defaults are fine for development):
# Edit .env file if needed nano .env -
Run the development server:
uv run uvicorn main:app --reload --host 0.0.0.0 --port 8000
The backend will be running at:
- API: http://localhost:8000
- Interactive Docs: http://localhost:8000/docs
- WebSocket: ws://localhost:8000/ws/{room_id}
-
Navigate to frontend directory:
cd frontend -
Dependencies are already installed during project setup. If needed, reinstall with:
npm install
-
Configure environment (optional - defaults work for local development):
# Edit .env file if needed nano .env -
Run the development server:
npm run dev
The frontend will be running at: http://localhost:4321
Terminal 1 - Backend:
cd backend
uv run uvicorn main:app --reloadTerminal 2 - Frontend:
cd frontend
npm run devQuizzes use Astro Content Collections with a specific structure:
Quiz Structure:
- One folder per quiz in
frontend/src/content/ - Each question is a separate
.mdfile - Frontmatter is validated by schema in
config.ts
Example: Creating "my-quiz"
-
Create folder:
frontend/src/content/my-quiz/ -
Create
question-1.md:
---
type: "multiple-choice"
timeLimit: 30
points: 1000
correctAnswer: ["4"]
options: ["3", "4", "5", "22"]
---
What is the output of `print(2 + 2)`?- Create
question-2.md:
---
type: "short-answer"
timeLimit: 45
points: 1500
correctAnswer: ["Python", "python"]
---
What programming language are you learning?- The quiz
my-quizwill be automatically available at/room/{roomId}/host?quiz=my-quiz
Key Points:
- One file per question (not all questions in one file)
- Frontmatter structure differs by question type
- Multiple-choice needs
optionsarray - Short-answer can have multiple accepted answers for fuzzy matching
- Hot reload: Uvicorn automatically reloads on file changes
- API Documentation: Visit http://localhost:8000/docs for interactive API docs
- Testing endpoints: Use the interactive docs or curl:
curl -X POST "http://localhost:8000/api/rooms/create?quiz_id=test-quiz"
- Hot reload: Astro dev server auto-refreshes on changes
- Type checking: Run
npm run checkfor TypeScript validation - Build: Run
npm run buildto create production build
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Frontend │◄───────►│ REST API │◄───────►│ Storage │
│ (Alpine.js)│ │ (FastAPI) │ │ (In-Memory) │
└─────────────┘ └──────────────┘ └─────────────┘
▲ │
│ │
└────────WebSocket───────┘
(Real-time)
The frontend uses a separation of concerns pattern:
Astro Components (.astro files):
- Server-side rendering (SSR) for initial HTML
- Alpine.js directives embedded in markup (
x-data,x-show,x-if,x-on) - Pass data from Astro → Alpine via template literals
Alpine Logic Modules (src/lib/alpine/*.ts):
- TypeScript functions that return Alpine component definitions
- Contain reactive state, methods, and lifecycle hooks
- Imported and called in Astro components via
x-data
Example Pattern:
<!-- Timer.astro (markup) -->
<div x-data={timer(timeLimit, currentQuestionIndex, totalQuestions)}>
<span x-text="timeLeft"></span>
<button @click="startTimer()">Start</button>
</div>
<script>
import { timer } from '../lib/alpine/timer.ts';
</script>// timer.ts (logic)
export function timer(limit: number, index: number, total: number) {
return {
timeLeft: limit,
isStarted: false,
startTimer() {
this.isStarted = true;
// countdown logic...
}
};
}Benefits:
- Logic is testable TypeScript (type-safe)
- Markup is readable Astro (SSR + Alpine directives)
- No client-side JavaScript framework bundle
- Alpine (~15KB) is the only JS shipped to browser
- Player submits answer → QuizPlayer component sends POST to
/api/rooms/{room_id}/answer - Backend validates → Uses
validation.pyfor fuzzy matching (85% threshold) - Backend calculates score → Uses
scoring.pywith time bonus + streak multiplier - Backend updates participant → Stores answer in
storage.pydictionaries - Backend broadcasts → WebSocket sends
leaderboard_updatedevent to all clients - All clients update → Leaderboard component fetches new rankings
This project is designed to teach Python beginners. Here are the interesting parts:
def calculate_score(max_points: int, response_time: int, time_limit: int, streak: int) -> int:
"""
Time bonus: Faster answers = more points (100% instant, 50% at timeout)
Streak multiplier: +10% per correct answer, max +50%
"""Learning: Mathematical calculations, percentage logic, max/min functions
from rapidfuzz import fuzz
similarity = fuzz.ratio(user_answer.lower().strip(), correct_answer.lower().strip())
if similarity >= 85: # Handles typos like "Pari" → "Paris"
return TrueLearning: String algorithms, external libraries, threshold-based logic, case-insensitive comparison
async def broadcast_to_room(room_id: str, message: dict):
"""Send message to all connected clients in a room"""
if room_id in connected_clients:
for ws in connected_clients[room_id]:
await ws.send_json(message)Learning: Async/await patterns, real-time communication, event broadcasting
rooms: Dict[str, Room] = {} # Room sessions
participants: Dict[str, Participant] = {} # Players
answers: Dict[str, List[Answer]] = {} # Submitted answersLearning: Dictionary-based data structures, type hints, data modeling without databases
backend/scoring.py- Pure Python math and logicbackend/main.py- FastAPI endpoints, async/await, WebSocketsbackend/validation.py- String algorithms and fuzzy matchingbackend/models.py- Pydantic data validation and type safety
CORS_ORIGINS=http://localhost:4321,http://localhost:3000
HOST=0.0.0.0
PORT=8000
DEBUG=TruePUBLIC_BACKEND_API=http://localhost:8000
PUBLIC_BACKEND_WS=ws://localhost:8000# Activate virtual environment (if needed manually)
source .venv/bin/activate # macOS/Linux
.venv\Scripts\activate # Windows
# Run server
uv run uvicorn main:app --reload
# Add new dependency
uv add <package-name>
# Run with custom port
uv run uvicorn main:app --reload --port 8080# Development server
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview
# Type check
npm run check
# Add new dependency
npm install <package-name>Check if backend is running:
curl http://localhost:8000View interactive API documentation:
open http://localhost:8000/docs # macOS
# or visit http://localhost:8000/docs in your browserTest endpoints directly:
# Create a room
curl -X POST "http://localhost:8000/api/rooms/create?quiz_id=test-quiz"
# Check room status
curl "http://localhost:8000/api/rooms/{room_id}"View server logs:
- Uvicorn prints all requests and errors to the terminal
- Look for WebSocket connection messages
- Check for CORS errors if frontend can't connect
Browser Console (DevTools):
// View Alpine.js component state
$data
// Watch state changes in real-time
$watch('timeLeft', value => console.log('Time:', value))Network Tab:
- Monitor WebSocket connection at
ws://localhost:8000/ws/... - Check API calls to
/api/...endpoints - Look for 422 errors (validation failures) or 404 (wrong URLs)
Common Console Errors:
WebSocket connection failed→ Backend not runningCORS policy→ Backend CORS_ORIGINS doesn't include frontend URLFailed to fetch→ Wrong API URL in.env
This guide walks you through deploying both backend and frontend to Railway.app for permanent, shareable URLs.
- GitHub Account - Code must be in a GitHub repository
- Railway Account - Sign up at railway.app (free tier available)
If your code isn't on GitHub yet:
# In project root
git add .
git commit -m "Prepare for Railway deployment"
# Create a new repository on GitHub, then:
git remote add origin https://github.com/YOUR_USERNAME/python-quiz.git
git push -u origin main-
Go to Railway Dashboard → Click "New Project"
-
Select "Deploy from GitHub repo"
- Authorize Railway to access your GitHub account
- Select your
python-quizrepository
-
Configure Backend Service:
- Railway will detect the Python project automatically
- Click on the service → Settings → Set Root Directory to:
backend - Railway will detect
pyproject.tomland install dependencies withuv
-
Add Environment Variables:
- Click on the service → Variables tab → Add these:
CORS_ORIGINS=http://localhost:4321 HOST=0.0.0.0 PORT=8000 DEBUG=False- Note: We'll update
CORS_ORIGINSafter deploying the frontend
-
Generate Domain:
- Click Settings → Generate Domain
- Copy the URL (e.g.,
https://python-quiz-backend-production.up.railway.app) - Save this for Step 3
-
Verify Deployment:
- Visit
https://YOUR-BACKEND-URL.railway.app/docs - You should see the FastAPI interactive documentation
- Visit
-
Back to Railway Dashboard → Click "New Project" again
-
Select same GitHub repository (
python-quiz) -
Configure Frontend Service:
- Click on the service → Settings → Set Root Directory to:
frontend - Railway will detect
package.jsonand runnpm install
- Click on the service → Settings → Set Root Directory to:
-
Add Environment Variables:
- Click Variables tab → Add these (use your backend URL from Step 2):
PUBLIC_BACKEND_API=https://YOUR-BACKEND-URL.railway.app PUBLIC_BACKEND_WS=wss://YOUR-BACKEND-URL.railway.app- Note: Use
wss://(notws://) for secure WebSocket in production
-
Configure Build Command (Important!):
- Click Settings → Build Command
- Set to:
npm run build - Start Command should be:
node dist/server/entry.mjs
-
Generate Domain:
- Click Settings → Generate Domain
- Copy the frontend URL (e.g.,
https://python-quiz-production.up.railway.app)
-
Verify Deployment:
- Visit your frontend URL
- You should see the quiz homepage
Now that you have the frontend URL, update the backend to accept requests from it:
-
Go to Backend Service in Railway Dashboard
-
Update Variables:
- Click Variables tab
- Update
CORS_ORIGINSto include your frontend URL:
CORS_ORIGINS=https://YOUR-FRONTEND-URL.railway.app,http://localhost:4321- Example:
CORS_ORIGINS=https://python-quiz-production.up.railway.app,http://localhost:4321
-
Redeploy:
- Railway will automatically redeploy when you save variables
- Wait for deployment to complete (~1-2 minutes)
- Visit your frontend URL:
https://YOUR-FRONTEND-URL.railway.app - Create a new room as host
- Join as participant (open in incognito/different browser)
- Start a question and verify real-time updates work
- Submit answers and check leaderboard updates
Share the URL with colleagues for feedback! 🎉
Backend deployment fails:
- Check Railway logs for Python errors
- Ensure
pyproject.tomlhas all dependencies - Verify
PORTenvironment variable is set to 8000
Frontend can't connect to backend:
- Check browser console for CORS errors
- Verify
PUBLIC_BACKEND_APIandPUBLIC_BACKEND_WSare correct - Ensure backend CORS includes frontend URL
- WebSocket must use
wss://(notws://) in production
WebSocket disconnects frequently:
- Railway free tier may have connection limits
- Check Railway logs for WebSocket errors
- Verify WebSocket URL uses
wss://protocol
Changes not appearing:
- Push changes to GitHub:
git push - Railway auto-deploys on git push
- Check "Deployments" tab in Railway dashboard
You can still develop locally! Just keep the .env files pointing to localhost:
Backend .env:
CORS_ORIGINS=http://localhost:4321,http://localhost:3000
HOST=0.0.0.0
PORT=8000
DEBUG=TrueFrontend .env:
PUBLIC_BACKEND_API=http://localhost:8000
PUBLIC_BACKEND_WS=ws://localhost:8000Railway uses environment variables you set in the dashboard, not your local .env files.
Backend won't start:
- Check if port 8000 is available:
lsof -i :8000 - Ensure Python 3.12+ is installed:
python --version - Try:
uv syncto reinstall dependencies
Frontend won't start:
- Check if port 4321 is available
- Clear npm cache:
npm cache clean --force - Reinstall dependencies:
rm -rf node_modules && npm install
WebSocket not connecting:
- Ensure backend is running
- Check browser console for errors
- Verify WebSocket URL in frontend
.env
CORS errors:
- Add frontend URL to backend
CORS_ORIGINSin.env - Restart backend server after changing
.env
See the Troubleshooting Deployment section above for production-specific issues.
This project is created for educational purposes.
This is a teaching project. Feel free to adapt for your own educational needs!
Built with Python and love for teaching! 🐍