- Getting Started
- Project Structure
- Development Setup
- Module Architecture
- API Documentation
- Database Models
- Authentication & Authorization
- Error Handling
- Code Execution
- AI Integration
- Testing
- Deployment
- Node.js: 18+ (recommended 20+)
- PostgreSQL: Database server (Supabase recommended)
- npm or yarn: Package manager
# 1. Install dependencies
npm install
# 2. Setup environment variables
npm run setup:env
# 3. Run database migrations
npx prisma migrate deploy
npx prisma generate
# 4. Create admin user
npm run setup:admin
# 5. Start development server
npm run devThe server will start on http://localhost:4000
backend/
├── src/
│ ├── config/ # Configuration files
│ │ ├── cors.ts # CORS configuration
│ │ └── env.ts # Environment variables validation
│ │
│ ├── middleware/ # Express middleware
│ │ ├── auth.ts # JWT authentication & authorization
│ │ ├── error.ts # Global error handler
│ │ └── validate.ts # Zod request validation
│ │
│ ├── modules/ # Feature modules (MVC pattern)
│ │ ├── auth/ # Authentication module
│ │ │ ├── auth.controller.ts
│ │ │ ├── auth.service.ts
│ │ │ ├── auth.repo.ts
│ │ │ ├── auth.routes.ts
│ │ │ ├── auth.zod.ts
│ │ │ └── token.service.ts
│ │ │
│ │ ├── exams/ # Exam management
│ │ ├── questions/ # Question management
│ │ ├── attempts/ # Exam attempts
│ │ ├── grading/ # Code execution & grading
│ │ ├── evaluations/ # Manual evaluations
│ │ ├── rubrics/ # Grading rubrics
│ │ ├── sections/ # Exam sections
│ │ ├── assets/ # Media assets
│ │ └── ai/ # AI question generation
│ │
│ ├── lib/ # Shared utilities
│ │ ├── prisma.ts # Prisma client singleton
│ │ ├── cookie-utils.ts # Cookie helpers
│ │ ├── judge0.ts # Judge0 API integration
│ │ ├── local-executor.ts # Local code execution
│ │ ├── execution-queue.ts # Code execution queue
│ │ ├── gemini.ts # Google Gemini AI
│ │ ├── db-health.ts # Database health checks
│ │ ├── socket.ts # Socket.IO setup
│ │ └── utils.ts # General utilities
│ │
│ ├── types/ # TypeScript types
│ │ └── express/ # Express type extensions
│ │
│ ├── app.ts # Express app configuration
│ └── index.ts # Server entry point
│
├── prisma/
│ ├── schema.prisma # Database schema
│ └── migrations/ # Database migrations
│
├── scripts/ # Utility scripts
│ ├── setup-env.js
│ ├── create-admin-user.js
│ ├── add-students.js
│ └── ...
│
├── uploads/ # Uploaded files
├── package.json
├── tsconfig.json
└── README.md
Create a .env file in the backend directory:
# Database
DATABASE_URL="postgresql://user:password@host:5432/database"
DIRECT_URL="postgresql://user:password@host:5432/database"
# Server
PORT=4000
NODE_ENV=development
# JWT
JWT_SECRET="your-secret-key-here"
# External Services
GOOGLE_API_KEY="your-google-api-key"
JUDGE0_API_KEY="your-judge0-api-key" # Optional
# CORS
ALLOW_ALL_ORIGINS=true # Development onlyUse npm run setup:env for interactive setup.
- Create a Supabase project
- Get connection strings from Supabase dashboard
- Set
DATABASE_URLandDIRECT_URLin.env
- Install PostgreSQL locally
- Create a database
- Set
DATABASE_URLin.env DIRECT_URLis optional for local PostgreSQL
# Generate Prisma client
npm run prisma:generate
# Run migrations
npm run prisma:migrate
# Open Prisma Studio (database GUI)
npm run prisma:studio# Interactive admin creation
npm run setup:admin
# Or use script directly
node scripts/create-admin-user.jsEach module follows the MVC (Model-View-Controller) pattern:
Controller (Route Handler)
↓
Service (Business Logic)
↓
Repository (Data Access)
↓
Database (Prisma ORM)
// Example: auth module
// 1. Controller (auth.controller.ts)
export async function loginController(req: Request, res: Response) {
try {
const data = validate(loginSchema, req.body);
const result = await authService.login(data);
res.json(result);
} catch (error) {
next(error); // Error middleware handles it
}
}
// 2. Service (auth.service.ts)
export async function login(data: LoginInput) {
// Business logic
const user = await authRepo.findByEmail(data.email);
if (!user || !await bcrypt.compare(data.password, user.password!)) {
throw { status: 401, message: 'Invalid credentials' };
}
const tokens = await tokenService.generateTokens(user);
return tokens;
}
// 3. Repository (auth.repo.ts)
export async function findByEmail(email: string) {
return prisma.user.findUnique({
where: { email },
select: { /* ... */ }
});
}
// 4. Routes (auth.routes.ts)
export function registerAuthRoutes(app: Express) {
const router = express.Router();
router.post('/login', validate(loginSchema), loginController);
app.use('/api/auth', router);
}
// 5. Validation Schema (auth.zod.ts)
export const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(1),
});- Create module directory:
src/modules/your-module/ - Create files:
your-module.controller.ts- Route handlersyour-module.service.ts- Business logicyour-module.repo.ts- Data accessyour-module.routes.ts- Route definitionsyour-module.zod.ts- Validation schemas
- Register routes in
src/app.ts:import { registerYourModuleRoutes } from './modules/your-module/your-module.routes'; registerYourModuleRoutes(app);
{
data: T,
meta?: {
total: number,
page: number,
pageSize: number
}
}{
error: {
code: string,
message: string,
details?: any
}
}See backend/README.md for complete API documentation.
All authenticated endpoints require:
- Header:
Authorization: Bearer <access_token> - Cookie:
refreshToken(for refresh endpoint)
All POST/PUT requests are validated using Zod schemas:
// In controller
const data = validate(loginSchema, req.body);
// Middleware automatically validates
router.post('/login', validate(loginSchema), loginController);- User: Students and Staff accounts
- Exam: Exam definitions
- Question: Questions of various types
- Section: Exam sections
- Attempt: Student exam attempts
- Response: Student answers
- Evaluation: Manual grading
- Rubric: Grading rubrics
- Asset: Media files
See prisma/schema.prisma for complete schema.
import { prisma } from '../lib/prisma';
// Create
const user = await prisma.user.create({
data: { email, password: hashed }
});
// Read
const user = await prisma.user.findUnique({
where: { id: userId }
});
// Update
const user = await prisma.user.update({
where: { id: userId },
data: { name: 'New Name' }
});
// Delete
await prisma.user.delete({
where: { id: userId }
});
// Transactions
await prisma.$transaction([
prisma.user.create({ data: user1 }),
prisma.user.create({ data: user2 })
]);# Create migration
npx prisma migrate dev --name your_migration_name
# Apply migrations
npx prisma migrate deploy
# Reset database (CAUTION: deletes all data)
npx prisma migrate reset- Access Token: Short-lived (15 minutes), sent in Authorization header
- Refresh Token: Long-lived (7 days), stored in HTTP-only cookie
// 1. Login
POST /api/auth/login
Body: { email, password }
Response: { accessToken, user }
// 2. Access protected endpoint
GET /api/student/exams
Header: Authorization: Bearer <access_token>
// 3. Refresh access token (if expired)
POST /api/auth/refresh
Cookie: refreshToken=<token>
Response: { accessToken }// Check authentication
router.get('/protected', verifyAccess, handler);
// Check role
router.get('/admin-only', verifyAccess, requireRole('STAFF'), handler);
// Check ownership
router.get('/my-resource/:id', verifyAccess, requireOwnership, handler);// In service
export async function getAttempt(attemptId: string, userId: string) {
const attempt = await attemptRepo.findById(attemptId);
// Check ownership
if (attempt.studentId !== userId) {
throw { status: 403, message: 'Forbidden' };
}
return attempt;
}All errors are handled by the global error middleware (src/middleware/error.ts):
// Throw structured error
throw {
status: 400,
code: 'VALIDATION_ERROR',
message: 'Invalid input',
details: { field: 'email', error: 'Invalid email format' }
};
// Error middleware converts to JSON response
{
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid input',
details: { field: 'email', error: 'Invalid email format' }
}
}// Validation errors (400)
throw { status: 400, code: 'VALIDATION_ERROR', message: '...' };
// Authentication errors (401)
throw { status: 401, code: 'UNAUTHORIZED', message: '...' };
// Authorization errors (403)
throw { status: 403, code: 'FORBIDDEN', message: '...' };
// Not found errors (404)
throw { status: 404, code: 'NOT_FOUND', message: '...' };
// Server errors (500)
throw { status: 500, code: 'INTERNAL_ERROR', message: '...' };try {
await prisma.user.create({ data });
} catch (error) {
if (error.code === 'P2002') {
// Unique constraint violation
throw { status: 409, code: 'CONFLICT', message: 'Email already exists' };
}
throw error;
}import { executeCode } from '../lib/judge0';
const result = await executeCode({
code: 'print("Hello, World!")',
language: 'python3',
input: 'test input'
});import { executeCodeLocally } from '../lib/local-executor';
const result = await executeCodeLocally({
code: 'print("Hello")',
language: 'python',
input: ''
});For rate limiting and better performance:
import { executionQueue } from '../lib/execution-queue';
const result = await executionQueue.enqueue({
code,
language,
input
});import { generateJsonFromAi } from '../lib/gemini';
const questions = await generateJsonFromAi(
'Generate 5 MCQ questions about JavaScript'
);// src/modules/ai/ai.service.ts
export async function generateQuestions(
examId: string,
prompt: string,
count: number
) {
const promptText = `Generate ${count} questions: ${prompt}`;
const questions = await generateJsonFromAi(promptText);
// Process and save questions
return await questionService.createMany(examId, questions);
}// Example test structure
describe('AuthService', () => {
it('should login with valid credentials', async () => {
const result = await authService.login({
email: '[email protected]',
password: 'password'
});
expect(result.accessToken).toBeDefined();
});
});// Test API endpoints
describe('POST /api/auth/login', () => {
it('should return access token', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({ email: '[email protected]', password: 'password' });
expect(response.status).toBe(200);
expect(response.body.accessToken).toBeDefined();
});
});# Build TypeScript
npm run build
# Run migrations
npm run prisma:migrate
# Start server
npm startSet production environment variables:
NODE_ENV=productionDATABASE_URL(production database)JWT_SECRET(strong secret)ALLOW_ALL_ORIGINS=false- Set specific
CORS_ORIGINS
- Environment variables configured
- Database migrations applied
- HTTPS enabled
- CORS configured properly
- Rate limiting enabled (future)
- Error logging configured (future)
- Monitoring setup (future)
- Use TypeScript strict mode
- Follow MVC pattern
- Use async/await for async operations
- Handle errors properly
- Validate all inputs with Zod
- Use meaningful variable names
- Never commit
.envfiles - Hash passwords with bcrypt
- Use HTTP-only cookies for refresh tokens
- Validate all inputs
- Use parameterized queries (Prisma handles this)
- Set secure CORS headers
- Use database indexes (e.g.,
@@index([startedAt])and@@index([score])onAttemptfor dashboard analytics) - Implement pagination
- Use transactions for related operations
- Cache frequently accessed data (future)
- Optimize database queries
-
Database Connection Error
- Check
DATABASE_URLin.env - Verify database is running
- Check network connectivity
- Check
-
Migration Errors
- Run
npx prisma migrate reset(CAUTION: deletes data) - Or manually fix migration conflicts
- Run
-
JWT Errors
- Verify
JWT_SECRETis set - Check token expiration
- Verify token format
- Verify
-
CORS Errors
- Check
CORS_ORIGINSin.env - Verify frontend URL is allowed
- Check CORS middleware configuration
- Check
Last Updated: November 2024 Version: 1.0.0