A Node.js backend service for an online coding judge platform built with Fastify, MongoDB, and JDoodle/Judge0 code execution engines.
- Project Overview
- Folder Structure
- Installation
- Environment Setup
- Running the Application
- API Documentation
- Database Models
- Services
- Contributing
The Online Coding Judge backend provides REST APIs for:
- Admin user authentication and problem/test management
- Candidate invitation and test access via unique tokens
- Code submission execution using JDoodle or Judge0 services
- Submission tracking and progress monitoring
- Real-time statistics dashboard for admins
Technology Stack:
- Runtime: Node.js
- Web Framework: Fastify
- Database: MongoDB with Mongoose ODM
- Authentication: JWT (JSON Web Tokens)
- Code Execution: JDoodle API
- Email: Nodemailer SMTP
backend/
├── app/
│ ├── config/
│ │ └── index.js # Environment configuration
│ ├── controllers/
│ │ ├── admin.controller.js # Admin operations (registration, login, invitations, problems, stats)
│ │ ├── invitation.controller.js # Invitation acceptance
│ │ ├── problems.controller.js # Problem retrieval
│ │ ├── progress.controller.js # Candidate progress and statistics
│ │ ├── submission.controller.js # Submission testing
│ │ ├── test.controller.js # Test retrieval and code execution
│ │ └── index.js # Controller exports
│ ├── middlewares/
│ │ ├── auth.middleware.js # JWT admin authentication
│ │ └── logger.middleware.js # Request logging
│ ├── models/
│ │ ├── invitation.model.js # Invitation schema (email, token, test, expiry)
│ │ ├── problem.model.js # Problem schema (title, description, test cases, languages)
│ │ ├── submission.model.js # Submission schema (code, status, score, execution results)
│ │ ├── test.model.js # Test schema (assessment with multiple problems)
│ │ ├── user.model.js # User schema (admin/candidate roles)
│ │ └── user.test.model.js # User test progress tracking
│ ├── routes/
│ │ ├── admin.routes.js # Admin API routes
│ │ ├── candidate.route.js # Candidate routes (deprecated/legacy)
│ │ ├── invitation.routes.js # Invitation routes
│ │ ├── problem.route.js # Problem routes
│ │ ├── submission.routes.js # Submission routes
│ │ ├── test.routes.js # Test routes
│ │ └── index.js # Route registration
│ ├── services/
│ │ ├── email.service.js # Email sending via Nodemailer
│ │ ├── jdoodle.service.js # JDoodle code execution
│ │ └── judge0.service.js # Judge0 code execution
│ ├── startup/
│ │ ├── db.startup.js # MongoDB connection setup
│ │ └── server.startup.js # Fastify server initialization
│ ├── utils/
│ │ ├── languageMap.js # Language ID mapping for JDoodle
│ │ └── logger.js # Logging utility
│ └── middleware/
│ └── auth.middleware.js # Authentication middleware
├── logs/ # Log files directory
├── server.js # Application entry point
├── package.json # Project dependencies
├── .env.example # Environment variables template
└── README.md # This file
- Node.js v16+
- MongoDB v4.0+
- npm or yarn
- Clone the repository:
git clone <repository-url>
cd backend- Install dependencies:
npm install- Set up environment variables (see Environment Setup below):
cp .env.example .envCreate a .env file in the project root with the following variables:
# Server Configuration
PORT=3000
NODE_ENV=development
# Database
MONGODB_URI=mongodb://localhost:27017/online-coding-judge
# JWT
JWT_SECRET=your-super-secret-key-min-32-chars
JWT_EXPIRES_IN=1d
# Frontend URL
FRONTEND_URL=http://localhost:5173
# JDoodle Configuration
JDOODLE_CLIENTID=your-jdoodle-client-id
JDOODLE_CLIENTSECRET=your-jdoodle-client-secret
JDOODLE_BASE_URL=https://api.jdoodle.com/v1
# By default the code-runner will prefer Judge0 if it's available. If you
# only want to use JDoodle (or you don't have Judge0 running) set this to
# false and every test‑case evaluation will be performed via JDoodle instead of
# Judge0.
USE_JUDGE0=false
# Judge0 Configuration (Alternative to JDoodle)
JUDGE0_BASE_URL=http://127.0.0.1:2358
JUDGE0_AUTH_TOKEN=your-judge0-auth-token
# SMTP Email Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
SMTP_FROM=your-email@gmail.comnpm run devThe server will start on http://localhost:3000 with automatic restart on file changes.
npm startAll requests should be made to: http://localhost:3000/api
Protected endpoints require an Authorization header with a JWT token:
Authorization: Bearer <your-jwt-token>
POST /admin/register
Create a new admin account.
Request Body:
{
"name": "John Admin",
"email": "admin@example.com",
"password": "securepassword123"
}Response:
{
"message": "Admin registered successfully",
"user": {
"_id": "63f...",
"name": "John Admin",
"email": "admin@example.com",
"role": "admin"
}
}POST /admin/login
Authenticate an admin and receive JWT token.
Request Body:
{
"email": "admin@example.com",
"password": "securepassword123"
}Response:
{
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"_id": "63f...",
"name": "John Admin",
"email": "admin@example.com",
"role": "admin"
}
}POST /admin/problems (Requires Admin Auth)
Create a new coding problem with test cases.
Request Body:
{
"title": "Two Sum",
"slug": "two-sum",
"description": "Given an array of integers nums and an integer target...",
"difficulty": "easy",
"languages": ["python", "java", "cpp", "javascript"],
"timeLimitMs": 2000,
"memoryLimitMb": 256,
"publicTestCases": [
{
"input": "2 7 11 15\n9",
"expectedOutput": "0 1"
}
],
"hiddenTestCases": [
{
"input": "3 3",
"expectedOutput": "0 1"
}
]
}Response:
{
"message": "Problem created successfully",
"problem": {
"_id": "63f...",
"title": "Two Sum",
"slug": "two-sum",
...
}
}POST /admin/invite (Requires Admin Auth)
Send test invitations to candidates.
Request Body:
{
"email": "candidate@example.com",
"problemIds": ["63f...problem1", "63f...problem2"],
"durationMinutes": 90
}Response:
{
"message": "Invitation sent successfully",
"email": "candidate@example.com",
"expiresAt": "2026-03-04T10:30:00.000Z",
"testId": "63f..."
}GET /admin/submissions (Requires Admin Auth)
Retrieve submissions with optional filters.
Query Parameters:
testId(optional): Filter by test IDemail(optional): Filter by candidate emailproblemId(optional): Filter by problem ID
Example Request:
GET /admin/submissions?testId=63f...&email=candidate@example.com
Response:
{
"submissions": [
{
"_id": "63f...",
"user": {
"_id": "63f...",
"name": "John Candidate",
"email": "candidate@example.com"
},
"problem": {
"_id": "63f...",
"title": "Two Sum"
},
"language": "python",
"status": "accepted",
"score": 100,
"executionTime": 0.05,
"memory": 7424,
"createdAt": "2026-02-25T10:30:00.000Z"
}
]
}GET /admin/stats (Requires Admin Auth)
Get overview statistics for all candidates and submissions.
Query Parameters:
testId(optional): Scope statistics to a specific test
Example Request:
GET /admin/stats
GET /admin/stats?testId=63f...
Response:
{
"totalCandidates": 25,
"activeSessions": 8,
"totalSubmissions": 156,
"usersAttemptedCount": 18
}GET /admin/candidates (Requires Admin Auth)
List all invited candidates with their participation status and submission counts.
Query Parameters:
status(optional): Filter by status (pending, accepted, expired)testId(optional): Filter by test IDemail(optional): Filter by candidate email
Example Request:
GET /admin/candidates
GET /admin/candidates?status=pending
Response:
{
"candidates": [
{
"id": "63f...",
"email": "candidate@example.com",
"status": "pending",
"token": "a1b2c3d4e5f6...",
"test": {
"id": "63f...",
"name": "Assessment for candidate@example.com",
"durationMinutes": 90
},
"createdBy": {
"id": "63f...",
"name": "John Admin",
"email": "admin@example.com"
},
"attempts": 3,
"lastAttempt": {
"id": "63f...",
"status": "accepted",
"score": 85,
"passed": true,
"createdAt": "2026-02-25T10:30:00.000Z"
},
"expiresAt": "2026-03-04T10:30:00.000Z",
"createdAt": "2026-02-25T09:30:00.000Z"
}
]
}GET /test?token=<invitation-token> OR POST /test
Retrieve test details and problems for a candidate using invitation token.
Request (GET):
GET /test?token=a1b2c3d4e5f6...
Request (POST):
{
"token": "a1b2c3d4e5f6..."
}Response:
{
"testId": "63f...",
"email": "candidate@example.com",
"durationMinutes": 90,
"problems": [
{
"_id": "63f...",
"title": "Two Sum",
"description": "Given an array of integers...",
"difficulty": "easy",
"languages": ["cpp", "java", "javascript", "python"],
"publicTestCases": [
{
"input": "2 7 11 15 9",
"output": "0 1"
}
],
"timeLimitMs": 2000,
"memoryLimitMb": 256
}
]
}POST /test/run
Execute candidate's code and save submission.
Request Body:
{
"sourceCode": "print('Hello World')",
"stdin": "",
"languageKey": "python",
"token": "a1b2c3d4e5f6...",
"problemId": "63f..."
}Response:
{
"output": "Hello World",
"memory": "7424",
"cpuTime": "0.05",
"statusCode": 200
}GET /test/progress?token=<invitation-token>
Retrieve candidate's progress across all problems in a test.
Request:
GET /test/progress?token=a1b2c3d4e5f6...
Response:
{
"testId": "63f...",
"email": "candidate@example.com",
"attempted": true,
"problems": [
{
"problemId": "63f...",
"title": "Two Sum",
"attempted": true,
"attempts": 2,
"lastAttempt": {
"id": "63f...",
"status": "accepted",
"score": 100,
"passed": true,
"output": "0 1",
"createdAt": "2026-02-25T10:30:00.000Z"
}
},
{
"problemId": "63f...",
"title": "Reverse String",
"attempted": false,
"attempts": 0,
"lastAttempt": null
}
]
}GET /invitation?token=<invitation-token>
Mark an invitation as accepted when candidate opens the test.
Request:
GET /invitation?token=a1b2c3d4e5f6...
Response:
{
"success": true,
"problems": [...]
}GET /problems
Retrieve all active problems (public endpoint).
Response:
{
"problems": [
{
"_id": "63f...",
"title": "Two Sum",
"slug": "two-sum",
"difficulty": "easy",
"languages": ["python", "java"],
...
}
]
}Represents admin and candidate users.
{
_id: ObjectId,
name: String,
email: String (unique),
role: String (enum: "admin", "candidate"),
passwordHash: String,
isActive: Boolean,
createdAt: Date,
updatedAt: Date
}Represents test invitations sent to candidates.
{
_id: ObjectId,
email: String,
test: ObjectId (ref: Test),
token: String (unique),
createdBy: ObjectId (ref: User),
expiresAt: Date,
status: String (enum: "pending", "accepted", "expired"),
createdAt: Date,
updatedAt: Date
}Represents an assessment with multiple problems.
{
_id: ObjectId,
name: String,
description: String,
problems: [ObjectId] (ref: Problem),
durationMinutes: Number,
startTime: Date,
endTime: Date,
inviteCode: String,
createdBy: ObjectId (ref: User),
isActive: Boolean,
createdAt: Date,
updatedAt: Date
}Represents a coding problem.
{
_id: ObjectId,
title: String,
slug: String (unique),
description: String,
difficulty: String (enum: "easy", "medium", "hard"),
tags: [String],
languages: [String],
timeLimitMs: Number,
memoryLimitMb: Number,
publicTestCases: [{
input: String,
expectedOutput: String,
score: Number
}],
hiddenTestCases: [{
input: String,
expectedOutput: String,
score: Number
}],
createdBy: ObjectId (ref: User),
isActive: Boolean,
createdAt: Date,
updatedAt: Date
}Represents a code submission by a candidate.
{
_id: ObjectId,
user: ObjectId (ref: User),
test: ObjectId (ref: Test),
problem: ObjectId (ref: Problem),
language: String,
sourceCode: String,
status: String (enum: "pending", "running", "accepted", "wrong_answer", "compile_error", "runtime_error", "time_limit_exceeded", "failed"),
score: Number,
executionTime: Number,
memory: Number,
judge0SubmissionId: String,
rawJudge0Response: Object,
createdAt: Date,
updatedAt: Date
}Sends invitation emails to candidates via SMTP.
Location: app/services/email.service.js
Functions:
sendInvitationEmail(to: string, inviteLink: string)- Sends HTML email with invitation link
Executes code using JDoodle API.
Location: app/services/jdoodle.service.js
Functions:
executeCode(params: object)- Executes code and returns output/memory/time
Parameters:
script: Source codestdin: Standard inputlanguage: Language identifier (python3, nodejs, cpp17, java)versionIndex: Language version
Executes code using Judge0 API (alternative to JDoodle).
Location: app/services/judge0.service.js
Functions:
createSubmission(params: object)- Create and optionally wait for submissiongetSubmission(token: string)- Get submission result by tokenrunTestCase(params: object)- Run single test case synchronously
Standard error response format:
{
"message": "Error description"
}HTTP Status Codes:
- 200: Success
- 201: Created
- 400: Bad Request
- 401: Unauthorized
- 403: Forbidden
- 404: Not Found
- 500: Internal Server Error
- Create a feature branch:
git checkout -b feature/your-feature - Make changes and test thoroughly
- Commit:
git commit -am "Add your feature" - Push:
git push origin feature/your-feature - Submit a pull request
This project is private and proprietary. Unauthorized copying is prohibited.
Questions? Contact the development team or refer to the API documentation above.