A production-ready, full-stack Online Quiz Application built with Node.js/Express (backend) and React/Vite (frontend).
- Loads questions from an Excel (.xlsx) file
- Fisher-Yates shuffle for both questions and answer options
- Configurable number of questions, categories, and timer
- Real-time score tracking with answer review
- Session-based grading — correct answers never sent to client
- Restart quiz at any time
- Responsive, accessible UI with keyboard-friendly navigation
- Docker support for one-command deployment
online-quiz/
├── backend/
│ ├── src/
│ │ ├── app.js # Express application entry
│ │ ├── config/config.js # Environment configuration
│ │ ├── controllers/ # HTTP request handlers
│ │ ├── middleware/ # Error handler
│ │ ├── repositories/ # Excel data loading + caching
│ │ ├── routes/ # Route definitions
│ │ ├── services/ # Business logic + session store
│ │ └── utils/shuffle.js # Fisher-Yates algorithm
│ ├── tests/ # Jest unit + integration tests
│ ├── scripts/generateSampleData.js
│ ├── data/questions.xlsx # (generated)
│ └── package.json
│
├── frontend/
│ ├── src/
│ │ ├── components/ # Timer, ProgressBar, QuizCard, OptionButton…
│ │ ├── context/QuizContext.jsx # Global state with useReducer
│ │ ├── hooks/ # useQuiz, useTimer
│ │ ├── pages/ # HomePage, QuizPage, ResultPage
│ │ └── services/quizApi.js # Axios API layer
│ └── package.json
│
├── docker-compose.yml
└── README.md
- Node.js >= 18
- npm >= 9
cd backend
npm install
npm run generate:data # creates data/questions.xlsx with 30 sample questionsnpm run dev # http://localhost:5000cd ../frontend
npm install
npm run dev # http://localhost:5173The Vite dev server proxies /api requests to http://localhost:5000.
# From the project root:
cd backend && npm install && npm run generate:data && cd ..
docker-compose up --build- Frontend: http://localhost
- Backend: http://localhost:5000
Place your file at backend/data/questions.xlsx.
| Column | Required | Description |
|---|---|---|
Question |
Yes | Question text |
OptionA |
Yes | First option |
OptionB |
Yes | Second option |
OptionC |
No | Third option |
OptionD |
No | Fourth option |
Answer |
Yes | Correct answer key: A, B, C, or D |
Category |
No | e.g. Geography, Science (default: General) |
Difficulty |
No | Easy / Medium / Hard (default: Medium) |
Returns server health status.
Response:
{ "status": "ok", "timestamp": "2024-01-01T00:00:00.000Z", "env": "development" }Returns all unique categories from the question bank.
Response:
{
"status": "success",
"data": { "categories": ["Geography", "History", "Science", "Technology"] }
}Start a quiz session. Returns shuffled questions with shuffled options.
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
category |
string | all | Filter by category |
limit |
number | 10 | Number of questions |
timerSeconds |
number | 600 | Timer duration in seconds |
Response:
{
"status": "success",
"data": {
"sessionId": "uuid-v4",
"totalQuestions": 10,
"timerSeconds": 600,
"questions": [
{
"id": "q_1",
"question": "What is the capital of India?",
"options": [
{ "key": "A", "text": "Mumbai" },
{ "key": "B", "text": "Delhi" },
{ "key": "C", "text": "Chennai" },
{ "key": "D", "text": "Kolkata" }
],
"category": "Geography",
"difficulty": "Easy"
}
]
}
}Note:
correctAnsweris never included in this response.
Submit answers and receive score with detailed results.
Request Body:
{
"sessionId": "uuid-v4",
"answers": {
"q_1": "B",
"q_2": "C"
}
}Response:
{
"status": "success",
"data": {
"sessionId": "uuid-v4",
"score": 8,
"total": 10,
"percentage": 80,
"results": [
{
"questionId": "q_1",
"question": "What is the capital of India?",
"options": [...],
"selectedAnswer": "B",
"correctAnswer": "B",
"isCorrect": true,
"category": "Geography",
"difficulty": "Easy"
}
]
}
}Error Codes:
| Code | Reason |
|---|---|
| 400 | Missing sessionId or invalid answers |
| 404 | Session not found or expired |
| 409 | Session already submitted |
| 410 | Session expired (TTL exceeded) |
cd backend
npm test # runs Jest with coverage
npm run test:watch # watch modeTests cover:
shuffle.test.js— Fisher-Yates algorithmquizService.test.js— Business logic, session managementquizApi.test.js— HTTP integration tests via supertest
| Variable | Default | Description |
|---|---|---|
NODE_ENV |
development |
Environment mode |
PORT |
5000 |
Server port |
EXCEL_FILE_PATH |
./data/questions.xlsx |
Path to questions file |
SESSION_TTL_MINUTES |
60 |
Session expiry time |
DEFAULT_QUESTION_LIMIT |
10 |
Default questions per quiz |
DEFAULT_TIMER_SECONDS |
600 |
Default timer (10 minutes) |
CORS_ORIGIN |
http://localhost:5173 |
Allowed CORS origin |
- Correct answers are stored server-side only — never exposed to the client
- Sessions expire automatically (configurable TTL)
- Each session can only be submitted once (prevents replay attacks)
- Input validation on all endpoints
- CORS restricted to configured origin