A minimal REST API for quizzes with JWT authentication. Built with FastAPI and Tortoise ORM. This project includes:
- Category management (create/list/update/delete)
- Question management with optional per-question time limits
- Quiz attempts with optional total quiz time limits
- Result tracking, user statistics and a leaderboard
- JWT authentication and pytest-based tests
- Run:
uvicorn main:app --reload - Docs:
http://127.0.0.1:8000/docs
- FastAPI for the web framework
- Tortoise ORM (lightweight async ORM)
- JWT auth via
python-jose - passlib[bcrypt] for password hashing
- User signup and token-based authentication (OAuth2 password flow)
- Category CRUD for organizing questions
- Question CRUD (create, read, update, delete) with optional per-question time limits
- Answer CRUD (create, read, update, delete) - manage answers independently
- Support for multiple correct answers per question
- Create questions with answers in a single request
- Start quiz attempts with an optional overall
total_time_limit(seconds) - Completing attempts computes score, records
time_spent, and markstimed_outwhen limits are exceeded - Per-user aggregated statistics and a global leaderboard
- Comprehensive tests that cover auth, questions, categories, answers, attempts and time-limit behavior
auth.py # Auth router + dependencies
quiz.py # Quiz router: categories & questions
quiz_results.py # Quiz attempts, completion, statistics, leaderboard
models.py # Tortoise models
schemas.py # Pydantic request/response models
config.py # Config (DATABASE_URL, JWT settings)
main.py # App entry + Tortoise registration
tests/ # pytest tests
requirements.txt
Set configuration via environment variables or a .env file. Key vars used by the app:
DATABASE_URL(example:sqlite://:memory:for tests orpostgres://user:pass@host:port/db)SECRET_KEY(set a long random secret for production)ALGORITHM(e.g.HS256)ACCESS_TOKEN_EXPIRE_MINUTES(default:30)
Example .env:
DATABASE_URL=postgres://postgres:password@localhost:5432/quiz_db
SECRET_KEY=your-very-secret-key
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=60During development the project uses Tortoise's generate_schemas=True to create tables automatically.
For production you should adopt a proper migration strategy.
User— username, email, hashed_password, is_activeCategory— name, descriptionQuestion— text, category (FK), difficulty, optionaltime_limit_secondsAnswer— question (FK), text, is_correctUserAnswer— user, question, answer, answered_atQuizAttempt— user, category, started_at, completed_at,time_spent, optionaltotal_time_limitQuizResult— attempt, user, total_questions, correct_answers, score,timed_outUserStatistics— aggregated per-user stats and averages
- Create & activate virtualenv:
python -m venv venv
venv\Scripts\activate- Install dependencies:
pip install -r requirements.txt- Start the app:
uvicorn main:app --reloadInteractive docs will be available at http://127.0.0.1:8000/docs.
Use the OAuth2 password flow to obtain a JWT:
POST /auth/signup— register (JSON body:username,email,password)POST /auth/token— get token (form data:username,password)
The token response looks like:
{"access_token": "<JWT>", "token_type": "bearer"}Send the token in requests using the Authorization: Bearer <JWT> header.
Prefix: /quiz
Categories
POST /quiz/categories/— Create a categoryGET /quiz/categories/— List all categoriesGET /quiz/categories/{id}— Get a categoryPUT /quiz/categories/{id}— UpdateDELETE /quiz/categories/{id}— Delete
Questions
POST /quiz/questions/— Create a question- body example:
{ "text": "What is 2+2?", "category_id": 1, "difficulty": "easy", "time_limit_seconds": 10, "answers": [{"text": "4", "is_correct": true}, {"text": "5", "is_correct": false}] } - Supports creating question with multiple answers (including multiple correct answers)
- body example:
GET /quiz/questions/— List questions; optional querycategory_id,skip,limitGET /quiz/questions/{id}— Get a single question with all its answersPUT /quiz/questions/{id}— Update a question (partial update supported)DELETE /quiz/questions/{id}— Delete a question
Answers
POST /quiz/questions/{question_id}/answers/— Create an answer for a question- body example:
{ "text": "4", "is_correct": true }
- body example:
GET /quiz/questions/{question_id}/answers/— Get all answers for a questionGET /quiz/answers/{id}— Get a single answerPUT /quiz/answers/{id}— Update an answer (partial update supported)DELETE /quiz/answers/{id}— Delete an answer
Quiz attempts & results
POST /quiz/attempts/— Start a quiz attempt (optional{ "category_id": 1, "total_time_limit": 300 })POST /quiz/attempts/{id}/complete— Complete an attempt; server computes score, recordstime_spent, and setstimed_outwhen limits exceeded
Statistics & leaderboard
GET /quiz/statistics/me— Get current user's aggregated statisticsGET /quiz/leaderboard— Get top users ordered by average score (query paramlimitoptional)
- Per-question time limit:
Question.time_limit_secondsis stored for a question and returned in the question response. Frontend should enforce per-question timers when presenting questions to users. - Total quiz time limit: set via
QuizAttempt.total_time_limitwhen starting an attempt. When completing an attempt the server calculates actual elapsed time and:- sets
timed_outtotrueif elapsed time >=total_time_limit(inclusive), - caps
time_spenttototal_time_limitwhen the limit is exceeded.
- sets
Note: this implementation enforces total-time limits at attempt completion time (server-side). For stricter, real-time enforcement you can use client-side timers or websockets to auto-submit.
Create question with per-question limit and multiple answers (including multiple correct answers):
POST /quiz/questions/
Authorization: Bearer <JWT>
Content-Type: application/json
{
"text": "Which numbers are even?",
"category_id": 1,
"difficulty": "easy",
"time_limit_seconds": 15,
"answers": [
{"text": "2", "is_correct": true},
{"text": "3", "is_correct": false},
{"text": "4", "is_correct": true},
{"text": "5", "is_correct": false}
]
}Update a question:
PUT /quiz/questions/{question_id}
Authorization: Bearer <JWT>
Content-Type: application/json
{
"text": "Updated question text",
"difficulty": "hard"
}Create an answer for a question:
POST /quiz/questions/{question_id}/answers/
Authorization: Bearer <JWT>
Content-Type: application/json
{
"text": "New answer option",
"is_correct": false
}Update an answer:
PUT /quiz/answers/{answer_id}
Authorization: Bearer <JWT>
Content-Type: application/json
{
"text": "Updated answer text",
"is_correct": true
}Start an attempt with a 5-minute total limit:
POST /quiz/attempts/
Authorization: Bearer <JWT>
Content-Type: application/json
{
"category_id": 1,
"total_time_limit": 300
}Complete attempt:
POST /quiz/attempts/{attempt_id}/complete
Authorization: Bearer <JWT>Response includes timed_out and time_spent fields.
Run tests locally (the project includes pytest tests that run against an in-memory or temporary DB configured in tests/conftest.py):
pytest -qAll tests should pass; new tests include coverage for category CRUD, attempts/results, and time-limit enforcement.
- If you see warnings about
python_multipart, installpython-multipartin your venv. - For bcrypt/passlib issues, pin
bcryptif needed:
pip install 'bcrypt~=4.1.3'- For production use, switch from auto-generating schemas to a proper migration workflow and ensure
SECRET_KEYis secure. - Consider adding realtime enforcement (websocket) if you need server-initiated auto-submit when time expires.
MIT