A full-featured certification exam simulator built with React and Express. Designed to replicate the experience of Microsoft (and other vendor) certification exams with realistic question types, domain-weighted scoring, timed sessions, AI-powered analysis, and comprehensive review capabilities.
- Features
- Architecture Overview
- Tech Stack
- Getting Started
- Project Structure
- Question Bank Format
- Exam Modes
- Domain-Based Exam Structure
- API Reference
- Testing Collections
- Internationalization
- Keyboard Shortcuts
- Configuration
- License
- Single Choice — Select exactly one correct answer from a list of options.
- Multiple Choice — Select one or more correct answers (with visual count hint).
- Drag & Order — Drag items into the correct sequence (powered by
@dnd-kit). - Drag & Match — Drag options to match them with their corresponding targets.
- Hotspot — Dropdown-based questions where each box has its own set of choices (simulates image-hotspot style questions from real exams).
| Mode | Description |
|---|---|
| Exam | Simulates a real certification exam. Answers are revealed only after finishing. |
| Practice | Check correctness of each question on-demand with the "Solve" button. |
| Review All | Browse all questions with answers visible. Ideal for study sessions. |
- Official Domain Weights — Questions are distributed according to official Microsoft exam domain percentages (e.g. AZ-204: Compute 25–30%, Storage 15–20%, Security 20–25%, Monitor 15–20%, Integration 15–20%).
- Realistic Mode Toggle — One click to generate an exam that mirrors the real certification structure.
- Domain-Grouped Topic Filter — Topics are organized under their parent domain in a collapsible tree, with weight badges and question counts.
- Weighted Scoring — Final score (0–1000) uses domain weights so weaknesses in high-weight domains are penalized proportionally.
- Timer — Configurable countdown timer with low-time warnings (visual + animation).
- Pagination — Adjustable questions per page (1, 5, 10, 20, 50).
- Question Flagging — Flag questions for review before finishing.
- Per-Question Notes — Write personal notes on any question during the exam.
- Session Persistence — Active sessions are saved to SQLite and survive page reloads.
- Auto-save — Answers are debounced and persisted every 500ms.
- Weighted Score Calculation — Scaled to 0–1000 using official domain weights (pass threshold: 700).
- Domain Breakdown — Per-domain accuracy with weight badges and color-coded progress bars.
- Topic Breakdown — Per-topic accuracy for granular analysis.
- Time Analytics — Total time and average time per question.
- Exam History — Full history with filter by passed/failed and sort by date.
- Review Mode — Post-exam review with filters for correct, incorrect, and flagged questions.
- Gemini AI Explanation — Get AI-generated explanations for any question via Google Gemini.
- Question Analysis — AI-powered analysis and validation of question quality and correctness.
- Technology Tag Management — View and edit technology tag content with AI assistance.
- JSON Import/Export — Import question banks from JSON files; export for backup or sharing.
- Inline Editing — Edit question text, explanations, and correct answers directly in the UI.
- Search & Filter — Search questions by text and filter by topic/domain within any bank.
- Technology Tags — 24 technology tags with content descriptions, linked via a relation table.
- Edit Audit Trail — All question edits are tracked in a separate
question_editstable.
- Responsive Design — Sidebar navigation on desktop, hamburger menu on mobile.
- Microsoft Fluent Design — Tailwind CSS theme mimicking Microsoft's design language (Segoe UI, Fluent colors).
- Internationalization — Full i18n support with English and Spanish locales.
- Keyboard Shortcuts — Navigate pages, flag questions, and more without a mouse.
- Dark Mode Ready — Tailwind
darkMode: 'class'is configured (theme tokens defined).
graph TB
subgraph Browser["Browser (SPA)"]
Pages["Pages<br/>Dashboard · ExamSetup · ExamSession<br/>ExamResults · ExamReview · History<br/>QuestionBank · Settings"]
Hooks["Custom Hooks<br/>useExamSession · useTimer"]
API["API Client — src/api.ts"]
Pages --> Hooks --> API
end
subgraph Server["Express 5 Server (:3150)"]
Entry["server/index.ts — CORS, JSON parser, dotenv"]
Routes["server/routes.ts — REST API"]
DB["server/db.ts — SQLite + auto-migration"]
Gemini["server/gemini-prompt.ts — AI integration"]
Entry --> Routes --> DB
Routes --> Gemini
end
API -- "HTTP/JSON via Vite proxy<br/>/api/* → localhost:3150" --> Entry
DB --> SQLite["exam-data.db (auto-generated)"]
The frontend is a Vite-powered React SPA. During development, Vite proxies /api requests to the Express backend on port 3150. The backend uses better-sqlite3 with WAL mode for fast, concurrent-safe reads. The database is created automatically (empty, with schema) on first server startup.
See app-doc/architecture.md for detailed architecture documentation and app-doc/database.md for the full SQL schema.
| Layer | Technology | Version |
|---|---|---|
| Frontend Framework | React | 19 |
| Routing | React Router | 7 |
| Styling | Tailwind CSS | 3.4 |
| Drag & Drop | @dnd-kit/core + sortable | 6.3 / 10.0 |
| Icons | Lucide React | 0.468 |
| Build Tool | Vite | 6 |
| Language | TypeScript | 5.7 |
| Backend Framework | Express | 5 |
| Database | SQLite (better-sqlite3) | 12.9 |
| AI Integration | Google Gemini API | — |
| Server Runtime | tsx | 4.21 |
| Testing | Vitest | — |
| License | GPL-3.0 | — |
- Node.js ≥ 18
- npm ≥ 9
git clone https://github.com/BorjaUmpierrezMayor/Exam-Simulator.git
cd Exam-Simulator
npm installCopy the example environment file and fill in your values:
cp .env.example .envRequired variables:
GEMINI_API_KEY— Google Gemini API key (for AI explanations)JWT_SECRET— Secret for JWT authentication tokens
Start both the API server and the Vite dev server:
# Terminal 1 — API Server (port 3150)
npm run server
# Terminal 2 — Vite Dev Server (port 5173, proxies /api to 3150)
npm run devOr use the combined command (Windows):
npm run dev:allThen open http://localhost:5173.
npm run build
npm run previewThe production build outputs to dist/. The Express server must still run separately for the API.
- Open the app and navigate to Question Banks.
- Click Import JSON to load a question bank file (see Question Bank Format).
- Go to New Exam, configure your session, and start practicing.
The SQLite database starts empty on first run. The repository includes sample banks under
public/exams/that you can import from the UI.
Exam-Simulator/
├── index.html # HTML entry point
├── vite.config.ts # Vite config with API proxy
├── tailwind.config.js # Tailwind theme (Microsoft Fluent palette)
├── tsconfig.json # TypeScript config (frontend)
├── tsconfig.server.json # TypeScript config (server → dist-server/)
├── package.json # Dependencies and scripts
├── .env.example # Environment variable template
│
├── server/ # Express API backend (TypeScript source)
│ ├── index.ts # Server entry, CORS, middleware, dotenv
│ ├── routes.ts # REST API routes (~1700+ lines)
│ ├── db.ts # SQLite connection, schema migration
│ ├── auth.ts # JWT authentication
│ ├── gemini-prompt.ts # Gemini AI prompt construction
│ ├── question-schema.ts # Question validation & normalization
│ └── tsconfig.json # TypeScript config (server)
│
├── dist-server/ # Compiled server output (from tsconfig.server.json)
│
├── src/ # React frontend
│ ├── main.tsx # App entry, renders root
│ ├── App.tsx # Router + page layout
│ ├── api.ts # API client (fetch wrapper)
│ ├── store.ts # LocalStorage persistence (legacy)
│ ├── types.ts # TypeScript type definitions
│ ├── utils.ts # Exam logic (grading, selection, domain weights)
│ ├── theme.ts # Theme utilities
│ ├── index.css # Tailwind directives + custom styles
│ │
│ ├── auth/ # Authentication context & guards
│ │
│ ├── components/
│ │ ├── Layout.tsx # App shell with sidebar navigation
│ │ ├── QuestionNav.tsx # Question grid navigator
│ │ ├── TimerDisplay.tsx # Timer with low-time warnings
│ │ ├── AiExplainDialog.tsx # Gemini AI explanation dialog
│ │ ├── GeminiAnalyzer.tsx # AI-powered question analysis
│ │ ├── TechnologyTagDialog.tsx # Tag content viewer/editor
│ │ └── questions/
│ │ ├── QuestionRenderer.tsx # Question type dispatcher
│ │ ├── SingleChoice.tsx # Radio button question
│ │ ├── MultipleChoice.tsx # Checkbox question
│ │ ├── DragOrder.tsx # Sortable drag question
│ │ ├── DragMatch.tsx # Drag-to-target question
│ │ └── HotspotQuestion.tsx # Dropdown-based hotspot
│ │
│ ├── hooks/
│ │ ├── useExamSession.ts # Exam session state + API sync
│ │ └── useTimer.ts # Countdown/countup timer hook
│ │
│ ├── i18n/
│ │ ├── index.tsx # I18n context provider
│ │ ├── en.ts # English translations (~260 keys)
│ │ └── es.ts # Spanish translations (~260 keys)
│ │
│ ├── pages/
│ │ ├── Dashboard.tsx # Home with stats and quick actions
│ │ ├── ExamSetup.tsx # Exam configuration with domain tree
│ │ ├── ExamSession.tsx # Active exam-taking view
│ │ ├── ExamResults.tsx # Score, domain & topic breakdown
│ │ ├── ExamReview.tsx # Question-by-question review
│ │ ├── History.tsx # Exam history list
│ │ ├── QuestionBank.tsx # Bank management UI
│ │ └── Settings.tsx # App preferences
│ │
│ ├── test/ # Vitest unit tests
│ └── utils/ # Additional utility modules
│
├── app-doc/ # Architecture and data model docs
│ ├── architecture.md
│ ├── data-model.md
│ ├── database.md
│ ├── technologies-overview.md
│ └── user-flows.md
│
├── scripts/ # Data management & migration scripts
│ ├── deploy.ps1 # Deployment script
│ ├── migrate_id_to_int.cjs # ID format migration
│ ├── populate_database.py # Database population tool
│ ├── data-fix/ # Data correction scripts
│ ├── patch-data/ # Batch question patches
│ ├── pdf-extraction/ # PDF→question extraction tools
│ └── analysis/ # Data analysis scripts
│
├── public/exams/ # Bundled exam banks and assets
│ └── az-204/ # AZ-204 exam bank + images
│
├── data/ # Runtime SQLite directory (git-ignored)
└── test/ # Postman API collections
Question banks are JSON files following this structure:
{
"id": "my-exam",
"title": "My Certification Exam",
"description": "Practice questions for certification preparation",
"passingScore": 700,
"maxScore": 1000,
"version": "1.0",
"topics": ["Topic A", "Topic B"],
"timeLimit": 90,
"questions": [
{
"id": 1,
"type": "single",
"topic": "Topic A",
"domain": "Develop Azure compute solutions (25–30%)",
"tags": ["Azure Functions"],
"text": "Which option is correct?",
"code": "// optional code block",
"options": [
{ "id": "a", "text": "Option A" },
{ "id": "b", "text": "Option B" }
],
"correctAnswers": ["b"],
"explanation": "Option B is correct because...",
"references": ["https://docs.example.com/..."]
}
]
}| Type | correctAnswers Format |
Extra Fields |
|---|---|---|
single |
["optionId"] |
— |
multiple |
["optionId1", "optionId2"] |
— |
drag-order |
["id1", "id2", "id3"] (ordered) |
— |
drag-match |
["optionId:targetId", ...] |
targets: [{id, text}] |
hotspot |
["value1", "value2", ...] (parallel to targets) |
targets: [{id, label, choices}] |
Simulates a real proctored exam. No answers are shown during the session. When finished, the exam is graded and a full results page is displayed with score, pass/fail status, domain breakdown, and topic breakdown.
Identical interface to Exam Mode, but each question has a Solve button that reveals the correct answer, explanation, and references immediately. Ideal for active learning.
All questions from the selected bank (or filtered topics) are loaded. Works like Practice Mode but defaults to the full question pool. Best for end-to-end study sessions.
Question Order — Review All offers two ordering options:
- In Order (default) — Questions appear in ascending ID order (1, 2, 3…). Ideal for systematic study.
- Random — Questions are shuffled. Useful for mixed review or spaced repetition.
Question Range — Optionally narrow the study session to a specific slice (e.g. questions 51–100) using the quick preset buttons or custom From/To inputs.
The simulator supports official certification exam domain structures. For AZ-204:
| Domain | Weight | Topics |
|---|---|---|
| Develop Azure compute solutions | 25–30% | Azure Functions, App Service (Web Apps, Containers), Container Apps & ACI, Docker & ACR, AKS & IaC |
| Develop for Azure storage | 15–20% | Azure Blob Storage, Azure Cosmos DB & Data Services |
| Implement Azure security | 20–25% | Microsoft Entra ID, App Service Security, Managed Identity, Key Vault & App Configuration |
| Monitor, troubleshoot, and optimize | 15–20% | Application Insights & Azure Monitor, App Service Monitoring, Azure Cache for Redis |
| Connect to and consume Azure services | 15–20% | Azure API Management, Azure Event Grid & Hubs, Service Bus & Queues, Logic Apps |
When Realistic Mode is enabled in the exam setup:
- Questions are distributed proportionally across domains by weight
- A visual breakdown shows how many questions each domain will contribute
- Scoring uses weighted domain averages (a 70% in Compute weighs more than 70% in Integration)
The topic filter displays topics grouped under their parent domain with:
- Collapsible domain sections
- Domain-level select/deselect all
- Weight percentage badges
- Question counts per topic and domain
- Search filter across all topics
All endpoints are prefixed with /api.
| Method | Endpoint | Description |
|---|---|---|
GET |
/banks |
List all question banks (with questions) |
GET |
/banks/:id |
Get a single bank with questions |
POST |
/banks |
Create or update a bank (upsert) |
DELETE |
/banks/:id |
Delete a bank and its questions |
| Method | Endpoint | Description |
|---|---|---|
GET |
/banks/:bankId/questions |
List questions (supports topic, domain, limit, offset, fromId, toId params) |
GET |
/banks/:bankId/questions/:questionId |
Get one question by id |
POST |
/banks/:bankId/questions |
Create/upsert a question |
POST |
/banks/:bankId/questions/:questionId |
Create/upsert a question by explicit id |
PATCH |
/banks/:bankId/questions/:questionId |
Partially update fields including domain, topic, tags |
| Method | Endpoint | Description |
|---|---|---|
GET |
/banks/:bankId/domains |
List domains with question counts |
GET |
/banks/:bankId/topics |
List topics with domain and question counts |
| Method | Endpoint | Description |
|---|---|---|
GET |
/sessions |
List active (in-progress) sessions |
GET |
/sessions/:id |
Get session with full question data |
POST |
/sessions |
Create a new session |
PUT |
/sessions/:id |
Update session (answers, page, status) |
DELETE |
/sessions/:id |
Delete a session |
| Method | Endpoint | Description |
|---|---|---|
GET |
/results |
List all exam results |
GET |
/results/:id |
Get result with full question data |
POST |
/results |
Save a graded exam result |
DELETE |
/results/:id |
Delete a single result |
DELETE |
/results |
Clear all results |
| Method | Endpoint | Description |
|---|---|---|
GET |
/tags |
List all technology tags |
GET |
/tags/:id |
Get tag details with content |
PATCH |
/tags/:id |
Update tag content |
| Method | Endpoint | Description |
|---|---|---|
GET |
/edits?bankId= |
List edits (optionally filtered by bank) |
POST |
/edits |
Save an edit (also applies it to the question) |
| Method | Endpoint | Description |
|---|---|---|
GET |
/settings |
Get all settings as key-value pairs |
PUT |
/settings |
Update settings (upsert) |
Postman collections are available in test/:
test/questions-crud.collection.json—GET(all, batch by ID range, batch by limit/offset),POST, andPATCHrequests for questions.test/questions-patch-scenarios.collection.json— focused partial-update scenarios, including explanation-only updates.
Both collections use these variables:
baseUrl(defaulthttp://localhost:3150/api)bankId(defaultaz-204)questionId(default1)
The app supports multiple locales via a React context-based i18n system with fully typed translation keys.
| Locale | File | Keys | Status |
|---|---|---|---|
| English | src/i18n/en.ts |
~260 | Complete |
| Spanish | src/i18n/es.ts |
~260 | Complete |
The locale is persisted in localStorage under the key app-locale. To add a new language:
- Create
src/i18n/{code}.tsmirroring the structure ofen.ts. - Import it in
src/i18n/index.tsxand add it to thedictionariesandLOCALE_LABELSobjects. - The
TranslationKeytype is auto-derived fromen.ts— no manual type updates needed.
| Key | Action |
|---|---|
→ or N |
Next page |
← or P |
Previous page |
F |
Flag/unflag first question on current page |
Shortcuts are disabled when the focus is in an input or textarea field.
| Setting | Default | Description |
|---|---|---|
| Questions per page | 10 | Default pagination size |
| Questions per exam | 50 | Default number of questions |
| Default time limit | None | Optional countdown timer |
| Keyboard shortcuts | Shown | Toggle shortcut display |
| Language | English | UI language |
| Variable | Default | Description |
|---|---|---|
PORT |
3150 |
Express server port |
GEMINI_API_KEY |
— | Google Gemini API key for AI features |
GEMINI_MODEL |
gemini-2.0-flash |
Gemini model to use |
JWT_SECRET |
— | Secret for JWT token signing |
DB_PATH |
./data/exam-data.db |
SQLite database file path |
| Variable | Default | Description |
|---|---|---|
| Vite Port | 5173 |
Dev server port (Vite default) |
| API Proxy | /api → :3150 |
Configured in vite.config.ts |
Key tables:
- banks — Exam bank metadata (title, passing score, version)
- questions — All questions with
type,topic,domain,tags(JSON),text(Markdown),options,correct_answers - question_tags — Many-to-many relation linking questions to technology tags
- tags — Technology tag definitions with
name,slug,content_md - sessions — Active exam sessions with answers and progress
- results — Completed exam results with scores
- question_edits — Audit trail for all question modifications
- settings — Key-value app settings
See app-doc/database.md for the full SQL schema.
This project is licensed under the GNU General Public License v3.0.