A production-ready, full-stack assessment platform built with Express.js + PostgreSQL (backend) and Flutter (mobile & web frontend). Designed for recruiters and educators to create, publish, and manage online tests with real-time candidate feedback.
- Overview
- Tech Stack
- Project Structure
- Prerequisites
- Installation & Setup
- API Documentation
- Features
- Architecture & Design
- Development Guide
- Environment Variables
- Troubleshooting
TestGorilla is an enterprise-grade assessment platform that enables organizations to:
- Create & Manage Tests: Build assessments with multiple question types (MCQ, coding, essays)
- Live Testing: Candidates take tests in real-time with timer, anti-cheating detection, and response validation
- Dual-Role Support: Admin panels for test creation/management, candidate interfaces for test taking
- Real-time Results: Instant feedback for MCQ, manual review workflows for subjective questions
- Cross-Platform: Works on iOS, Android, Web, and Desktop (built with Flutter)
Current Status: Production-ready backend with full Flutter frontend support
| Component | Technology | Version |
|---|---|---|
| Runtime | Node.js | 16+ LTS |
| Framework | Express.js | 4.22.1 |
| Database | PostgreSQL | 12+ |
| Authentication | JWT (jsonwebtoken) | 9.0.3 |
| Security | bcryptjs | 2.4.3 |
| CORS | cors | 2.8.6 |
| Development | Nodemon | 3.1.14 |
| Component | Technology | Version |
|---|---|---|
| Framework | Flutter | 3.9.2+ |
| UI Framework | Material 3 | Latest |
| State Management | Provider | 6.0.0 |
| HTTP Client | http | 1.1.0 |
| Storage | shared_preferences | 2.0.0 |
| Audio | audioplayers | 5.2.1 |
| Ringtone | flutter_ringtone_player | 4.0.0+4 |
| Icons | cupertino_icons | 1.0.8 |
test-gorilla/
βββ backend/ # Node.js/Express backend
β βββ src/
β β βββ app.js # Express app setup & middleware
β β βββ server.js # Entry point & server startup
β β βββ config/
β β β βββ db.js # PostgreSQL pool configuration
β β βββ routes/
β β β βββ auth.js # Authentication endpoints
β β β βββ tests.js # Test CRUD & management
β β β βββ questions.js # Question management
β β β βββ attempts.js # Test attempt & response tracking
β β β βββ results.js # Test results & evaluation
β β β βββ health.js # Health check endpoint
β β βββ controllers/ # Business logic handlers
β β β βββ authController.js
β β β βββ testController.js
β β β βββ questionController.js
β β β βββ attemptController.js
β β β βββ resultController.js
β β βββ services/ # Database & business services
β β β βββ authService.js
β β β βββ testService.js
β β β βββ questionService.js
β β β βββ attemptService.js
β β β βββ evaluationService.js
β β β βββ tokenService.js
β β βββ middleware/
β β β βββ authMiddleware.js # JWT validation & role checks
β β β βββ attemptMiddleware.js # Attempt-specific checks
β β β βββ cors.js # CORS configuration
β β β βββ errorHandler.js # Global error handling
β β β βββ requestLogger.js # Request logging
β β βββ utils/
β β βββ logger.js # Structured logging
β β βββ jwt.js # JWT utilities
β β βββ constants.js # App constants & enums
β βββ migrations/ # Database migrations (SQL)
β β βββ 001_create_users_table.sql
β β βββ 002_create_tests_table.sql
β β βββ 003_create_questions.sql
β β βββ 004_create_attempts.sql
β β βββ 005_create_results.sql
β β βββ 006_add_manual_review_support.sql
β β βββ 007_create_verification_tokens.sql
β βββ package.json # npm dependencies & scripts
β βββ .env.example # Environment variables template
β βββ seed_users.js # Database seeding script
β
βββ frontend/ # Flutter cross-platform app
β βββ test_gorilla/
β βββ lib/
β β βββ main.dart # App entry point & providers
β β βββ core/
β β β βββ api/
β β β β βββ api_client.dart # HTTP client & API wrapper
β β β βββ config/
β β β β βββ app_config.dart # App configuration
β β β βββ theme/
β β β β βββ app_theme.dart # Material 3 theme
β β β βββ utils/
β β β β βββ jwt_storage.dart # JWT token storage
β β β β βββ tenseconds.mp3 # Timer audio
β β β β βββ constants.dart
β β β βββ services/
β β βββ features/
β β β βββ auth/
β β β β βββ auth_provider.dart # Auth state management
β β β β βββ login_screen.dart
β β β β βββ register_screen.dart
β β β βββ admin/
β β β β βββ dashboard_screen.dart # Admin panel
β β β β βββ test_creation/
β β β β βββ test_management/
β β β β βββ evaluation/ # Manual evaluation UI
β β β βββ candidate/
β β β β βββ candidate_dashboard_shell.dart
β β β β βββ available_tests_screen.dart
β β β β βββ test_taking/
β β β β β βββ test_screen.dart
β β β β β βββ timer_widget.dart
β β β β β βββ question_widgets/
β β β β βββ results_screen.dart
β β β βββ navigation/
β β β β βββ app_router.dart # Route management
β β β βββ shared/
β β β βββ widgets/
β β β βββ dialogs/
β β βββ test/
β β β βββ widget_test.dart
β βββ android/ # Android-specific config
β βββ ios/ # iOS-specific config
β βββ web/ # Web-specific config
β βββ pubspec.yaml # Flutter dependencies
β βββ analysis_options.yaml # Lint rules
β
βββ README.md # This file
- Node.js: v16 LTS or higher (download)
- PostgreSQL: v12+ (download)
- Flutter: 3.9.2+ (install)
- Dart: 3.9.2+ (included with Flutter)
- npm/yarn: v7+ (comes with Node.js)
# Check Node.js version
node --version # Should be v16+
# Check npm version
npm --version # Should be v7+
# Check PostgreSQL
psql --version # Should be 12+
# Check Flutter
flutter --version # Should be 3.9.2+
# Check Dart
dart --version # Should be 3.9.2+git clone <repository-url>
cd test-gorilla/backendnpm installThis installs all required packages:
- express - Web framework
- pg - PostgreSQL client
- jsonwebtoken - JWT authentication
- bcryptjs - Password hashing
- cors - Cross-origin requests
- dotenv - Environment variables
- nodemon - Development hot reload
# Copy the example file
cp .env.example .env
# Edit .env with your settings (see Environment Variables section below)Quick Start Config:
Security note: Never commit real credentials or secrets to GitHub. Keep actual values only in local
.envfiles and use placeholders in documentation.
# .env
NODE_ENV=development
PORT=5000
# PostgreSQL
DB_HOST=<db-host>
DB_PORT=5432
DB_NAME=<db-name>
DB_USER=<your_db_user>
DB_PASSWORD=<your_db_password>
DB_SSL=false
DB_POOL_MIN=2
DB_POOL_MAX=10
# JWT
JWT_SECRET=<generate-a-long-random-secret>
JWT_EXPIRY=24h
# CORS
CORS_ORIGIN=<allowed-origin-url>
# Logging
LOG_LEVEL=debug# Open PostgreSQL terminal
psql -U postgres
# Inside psql:
CREATE DATABASE testgorilla;
\qOr use a single command:
createdb -U postgres testgorillaMigrations create the necessary tables and schema:
# Manually run migrations (in order)
psql -U postgres -d testgorilla -f migrations/001_create_users_table.sql
psql -U postgres -d testgorilla -f migrations/002_create_tests_table.sql
psql -U postgres -d testgorilla -f migrations/003_create_questions.sql
psql -U postgres -d testgorilla -f migrations/004_create_attempts.sql
psql -U postgres -d testgorilla -f migrations/005_create_results.sql
psql -U postgres -d testgorilla -f migrations/006_add_manual_review_support.sql
psql -U postgres -d testgorilla -f migrations/007_create_verification_tokens.sqlOr create a migration script and automate this process.
# Add sample users for development
node seed_users.jsDevelopment (with hot reload):
npm run devServer will be available at: http://localhost:5000
Production:
npm startExpected Output:
Server started successfully
environment: development
port: 5000
url: http://localhost:5000
cd test-gorilla/frontend/test_gorillaflutter pub getThis installs all packages from pubspec.yaml.
Edit the API base URL in your app configuration:
File: lib/core/config/app_config.dart
class AppConfig {
// Development
static const String DEV_API_BASE_URL = 'http://localhost:5000/api/v1';
// Production
static const String PROD_API_BASE_URL = 'https://api.testgorilla.com/api/v1';
// Use based on environment
static const String API_BASE_URL = DEV_API_BASE_URL;
}For Android Emulator:
- Use
http://10.0.2.2:5000/api/v1(special IP for emulator to reach host)
For iOS Simulator:
- Use
http://localhost:5000/api/v1or use ngrok for tunneling
For Web:
- Use
http://localhost:5000/api/v1
Run on Android Emulator:
flutter emulators --launch emulator-5554 # Launch emulator first
flutter run -d emulator-5554Run on iOS Simulator:
flutter run -d iPhoneRun on Web Browser:
flutter run -d chromeOr for a specific browser:
flutter run -d edge
flutter run -d firefoxRun on Physical Device:
# List connected devices
flutter devices
# Run on specific device
flutter run -d <device-id>Android APK:
flutter build apk --release
# Output: build/app/outputs/flutter-apk/app-release.apkiOS IPA:
flutter build ios --release
# Output: build/ios/ipa/Web Release:
flutter build web --release
# Output: build/web/http://localhost:5000/api/v1
All endpoints except /auth/register and /auth/login require JWT token in header.
POST /auth/register
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com",
"password": "SecurePassword123",
"role": "candidate" // or "admin"
}Response:
{
"success": true,
"message": "User registered successfully",
"user": {
"id": "uuid-123",
"name": "John Doe",
"email": "john@example.com",
"role": "candidate"
},
"accessToken": "eyJhbGc...",
"refreshToken": "eyJhbGc..."
}POST /auth/login
Content-Type: application/json
{
"email": "john@example.com",
"password": "SecurePassword123"
}Response:
{
"success": true,
"message": "Login successful",
"user": {
"id": "uuid-123",
"name": "John Doe",
"email": "john@example.com",
"role": "candidate"
},
"accessToken": "eyJhbGc...",
"refreshToken": "eyJhbGc..."
}GET /auth/me
Authorization: Bearer <accessToken>Response:
{
"success": true,
"user": {
"id": "uuid-123",
"name": "John Doe",
"email": "john@example.com",
"role": "candidate",
"createdAt": "2024-04-20T10:30:00Z"
}
}POST /tests
Authorization: Bearer <adminToken>
Content-Type: application/json
{
"title": "JavaScript Assessment",
"description": "Test your JavaScript skills",
"duration_minutes": 60,
"pass_percentage": 75
}GET /tests
Authorization: Bearer <token>Admin sees: their own created tests Candidate sees: published tests only
GET /tests/{testId}
Authorization: Bearer <token>PUT /tests/{testId}
Authorization: Bearer <adminToken>
Content-Type: application/json
{
"title": "Updated Title",
"description": "Updated description",
"duration_minutes": 90
}PATCH /tests/{testId}/publish
Authorization: Bearer <adminToken>PATCH /tests/{testId}/archive
Authorization: Bearer <adminToken>DELETE /tests/{testId}
Authorization: Bearer <adminToken>POST /tests/{testId}/questions
Authorization: Bearer <adminToken>
Content-Type: application/json
{
"question_text": "What is the output of this code?",
"question_type": "mcq", // mcq, coding, or essay
"options": [
{ "text": "Option A", "is_correct": true },
{ "text": "Option B", "is_correct": false },
{ "text": "Option C", "is_correct": false }
],
"order_index": 1
}GET /tests/{testId}/questions
Authorization: Bearer <token>PUT /tests/{testId}/questions/{questionId}
Authorization: Bearer <adminToken>
Content-Type: application/jsonDELETE /tests/{testId}/questions/{questionId}
Authorization: Bearer <adminToken>POST /tests/{testId}/attempts
Authorization: Bearer <candidateToken>
Content-Type: application/json
{
"candidate_id": "uuid-456"
}Response:
{
"success": true,
"attempt": {
"id": "attempt-uuid",
"test_id": "test-uuid",
"candidate_id": "candidate-uuid",
"status": "in_progress",
"started_at": "2024-04-20T10:30:00Z",
"time_limit_seconds": 3600,
"questions": [
{
"id": "q-1",
"text": "Question text...",
"type": "mcq",
"options": [...]
}
]
}
}GET /attempts/{attemptId}
Authorization: Bearer <token>POST /attempts/{attemptId}/responses
Authorization: Bearer <candidateToken>
Content-Type: application/json
{
"question_id": "q-1",
"selected_option_id": "option-a", // For MCQ
"answer_text": "...", // For essay
"code_answer": "..." // For coding
}POST /attempts/{attemptId}/submit
Authorization: Bearer <candidateToken>Response:
{
"success": true,
"message": "Test submitted successfully",
"result": {
"id": "result-uuid",
"attempt_id": "attempt-uuid",
"total_questions": 10,
"correct_answers": 8,
"score": 80,
"passing_score": 75,
"status": "passed" // or "pending_review" if has essays/coding
}
}GET /results?test_id={testId}
Authorization: Bearer <adminToken>GET /candidates/{candidateId}/results
Authorization: Bearer <token>GET /attempts/pending-evaluations
Authorization: Bearer <adminToken>Returns all pending essays and coding question evaluations.
POST /attempts/{attemptId}/evaluate
Authorization: Bearer <adminToken>
Content-Type: application/json
{
"question_id": "q-1",
"marks_obtained": 8,
"marks_total": 10,
"feedback": "Good solution with proper error handling"
}GET /healthResponse:
{
"success": true,
"status": "API and database are operational",
"database": "connected",
"timestamp": "2024-04-20T10:30:00Z"
}-
β Test Management
- Create, edit, publish, archive tests
- Set duration, passing criteria
- Manage test visibility
-
β Question Management
- Add MCQ, Coding, Essay questions
- Set correct answers for MCQ
- Define evaluation rubrics
-
β Candidate Management
- View all test takers
- Track candidate progress
- Download results in CSV/PDF
-
β Evaluation & Scoring
- Auto-score MCQ questions
- Manual review interface for essays/coding
- Provide feedback to candidates
-
β Analytics Dashboard
- Pass/fail statistics
- Time analysis
- Performance trends
-
β Test Discovery
- Browse available tests
- View test details (duration, questions count)
- Check previous attempts
-
β Test Taking Experience
- Real-time timer with audio alerts
- Question navigation (previous/next)
- Progress indicator
- MCQ response selection
- Essay/Coding text input
-
β Anti-Cheating Measures
- Tab switch detection
- Full-screen enforcement
- Window blur detection
- Suspicious activity logging
-
β Immediate Feedback
- Instant MCQ results
- Overall score display
- Detailed question review
-
β Results History
- View all attempt history
- Compare scores
- Download certificates (if passed)
1. User Registration/Login
ββ> POST /auth/register or /auth/login
ββ> Generate JWT tokens (Access + Refresh)
ββ> Return tokens to client
2. Authenticated Requests
ββ> Client includes: Authorization: Bearer {accessToken}
ββ> Middleware validates JWT signature
ββ> Extract user ID & role
ββ> Proceed with request or reject
3. Token Refresh (Future Implementation)
ββ> Access Token expires β Use Refresh Token
ββ> GET /auth/refresh
ββ> Issue new Access Token pair
JWT Payload:
{
"id": "user-uuid",
"email": "user@example.com",
"role": "admin|candidate|recruiter",
"iat": 1713607800,
"exp": 1713694200
}βββββββββββββββ
β User β
β (Role) β
ββββββββ¬βββββββ
β
βββ [ADMIN]
β ββ Create/Edit Tests
β ββ Manage Questions
β ββ View All Results
β ββ Evaluate Essays/Coding
β ββ Access Analytics
β
βββ [CANDIDATE]
β ββ View Published Tests
β ββ Take Tests
β ββ View Own Results
β ββ View Own Attempts
β
βββ [RECRUITER]
ββ Create Tests
ββ View Results (own tests)
ββ Evaluate Submissions
Users Table:
id(UUID Primary Key)name,email,password_hashrole(admin/candidate/recruiter)created_at,updated_at
Tests Table:
id,title,descriptionduration_minutes,pass_percentagecreated_by(Reference to Users)status(draft/published/archived)created_at,updated_at
Questions Table:
id,test_id,question_text,question_typeoptions(JSON array for MCQ)correct_answer(for MCQ/coding)order_index,marks
Attempts Table:
id,test_id,candidate_idstatus(in_progress/submitted/evaluated)started_at,submitted_attime_spent_seconds
Results Table:
id,attempt_id,score,statustotal_questions,correct_answersevaluated_at,created_at
Standard Success Response:
{
"success": true,
"message": "Operation successful",
"data": { ... },
"timestamp": "2024-04-20T10:30:00Z"
}Standard Error Response:
{
"success": false,
"error": "Error message",
"code": "ERROR_CODE",
"details": { ... },
"timestamp": "2024-04-20T10:30:00Z"
}npm test- Controllers: Handle HTTP request/response
- Services: Core business logic & database queries
- Middleware: Request interceptors (auth, logging, error)
- Routes: Endpoint definitions & routing
- Utils: Helpers (JWT, logger, constants)
- Define route in
routes/folder - Create controller method in
controllers/folder - Add business logic in
services/folder - Register route in
app.js - Add middleware (auth, validation)
// Using connection pool from config/db.js
const db = require('../config/db');
// Query with parameters (prevent SQL injection)
const result = await db.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
// Insert
await db.query(
'INSERT INTO users (name, email) VALUES ($1, $2)',
[name, email]
);flutter run -v # Verbose output// Define provider
class TestProvider with ChangeNotifier {
List<Test> _tests = [];
Future<void> fetchTests() async {
_tests = await apiClient.getTests();
notifyListeners();
}
}
// Use in widget
Consumer<TestProvider>(
builder: (context, testProvider, child) {
return ListView(
children: testProvider.tests.map((test) => ...).toList(),
);
},
)// lib/core/api/api_client.dart
final apiClient = ApiClient();
// GET request
final tests = await apiClient.get('/tests');
// POST request
final result = await apiClient.post('/tests', {
'title': 'New Test',
'duration_minutes': 60
});// Save token
await JwtStorage.saveToken(token);
// Retrieve token
final token = await JwtStorage.getToken();
// Remove token (logout)
await JwtStorage.deleteToken();# Server
NODE_ENV=development|production
PORT=5000
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=testgorilla
DB_USER=<your_db_user>
DB_PASSWORD=<secure-password>
DB_POOL_MIN=2
DB_POOL_MAX=10
# JWT Authentication
JWT_SECRET=<generate-a-long-random-secret-min-32-chars>
JWT_EXPIRY=24h
REFRESH_TOKEN_EXPIRY=7d
BCRYPT_SALT_ROUNDS=10
# CORS
CORS_ORIGIN=<allowed-origin-1>,<allowed-origin-2>
# Logging
LOG_LEVEL=debug|info|warn|errorclass AppConfig {
// API Configuration
static const String DEV_API_BASE_URL = 'http://localhost:5000/api/v1';
static const String PROD_API_BASE_URL = 'https://api.testgorilla.com/api/v1';
static const String API_BASE_URL = DEV_API_BASE_URL;
// JWT Storage Keys
static const String JWT_TOKEN_KEY = 'auth_token';
static const String REFRESH_TOKEN_KEY = 'refresh_token';
}# Kill process on port 5000
lsof -ti:5000 | xargs kill -9 # macOS/Linux
netstat -ano | findstr :5000 # Windows# Verify PostgreSQL is running
psql -U postgres -c "SELECT version();"
# Check .env variables
cat .env | grep DB_
# Reset database
dropdb testgorilla
createdb testgorilla
# Re-run migrations- Ensure
JWT_SECRETis set in.env - Check token expiry:
JWT_EXPIRY=24h - Clear browser localStorage and re-login
Update CORS_ORIGIN in .env:
# Allow multiple origins
CORS_ORIGIN=http://localhost:3000,http://localhost:8080,https://example.comUse the special IP for emulator:
static const String DEV_API_BASE_URL = 'http://10.0.2.2:5000/api/v1';flutter clean
flutter pub get
flutter pub upgradecd ios
pod deintegrate
pod install
cd ..
flutter runUpdate API client to allow self-signed certificates (dev only):
HttpClient httpClient = new HttpClient()
..badCertificateCallback = ((certificate, host, port) => true);MIT License - See LICENSE file for details.
For issues, feature requests, or contributions:
- Check existing issues
- Create detailed bug reports
- Submit pull requests with clear descriptions
- Follow existing code style and patterns
For questions or support, reach out to the development team or check the project wiki for additional documentation.
Last Updated: April 2024
Version: 1.0.0
Status: Production Ready