Spaces:
Sleeping
Question Bank And Exam Generator Frontend API Documentation
Date: 2026-05-04
Backend root: D:\Graduation\backend\last_backend\EduVerse_Backend
Audience: frontend developers implementing instructor question bank and exam generator screens.
1. Current Scope
The current backend scope is instructor authoring only.
Allowed role:
INSTRUCTOR
Blocked roles for these features:
STUDENTTAADMIN
Every endpoint below requires:
Authorization: Bearer <instructor_jwt>
Content-Type: application/json
Multipart upload endpoints use:
Authorization: Bearer <instructor_jwt>
Content-Type: multipart/form-data
The backend also checks course ownership. An instructor can only manage courses assigned through:
course_instructors -> course_sections -> course_id
2. Base URL And Common Conventions
Examples use:
{{baseUrl}}/api
IDs are numeric.
Dates are ISO strings when returned or sent as query filters.
Pagination response shape for exam list APIs:
{
"data": [],
"meta": {
"total": 0,
"page": 1,
"limit": 20,
"totalPages": 0
}
}
Question bank list response shape:
{
"data": [],
"total": 0
}
Common NestJS error shape:
{
"statusCode": 400,
"message": "Validation or business error message",
"error": "Bad Request"
}
For exam-generation shortages, message can be an object:
{
"statusCode": 400,
"message": {
"message": "Insufficient question pool for one or more buckets",
"shortages": [
{
"section": "Part A",
"chapterId": 2,
"required": 5,
"available": 3,
"questionType": "mcq",
"difficulty": "easy",
"bloomLevel": null
}
]
},
"error": "Bad Request"
}
3. Enums
Question types:
type QuestionBankType =
| 'written'
| 'mcq'
| 'true_false'
| 'fill_blanks'
| 'essay';
Question difficulty:
type QuestionBankDifficulty = 'easy' | 'medium' | 'hard';
Bloom levels:
type BloomLevel =
| 'remembering'
| 'understanding'
| 'applying'
| 'analyzing'
| 'evaluating'
| 'creating';
Question status:
type QuestionBankStatus = 'draft' | 'approved' | 'archived';
Attachment type:
type QuestionAttachmentType = 'image' | 'document' | 'audio' | 'video';
Question group type:
type QuestionGroupType =
| 'passage'
| 'case_study'
| 'image_set'
| 'multipart'
| 'other';
Exam draft status:
type ExamDraftStatus =
| 'open'
| 'finalized'
| 'expired'
| 'cancelled'
| 'failed';
Exam status:
type ExamStatus = 'draft' | 'published' | 'archived';
Exam mark distribution mode:
type ExamMarkDistributionMode =
| 'manual'
| 'weight_normalized'
| 'equal';
Exam rounding policy:
type ExamRoundingPolicy =
| 'none'
| 'nearest_0_25'
| 'nearest_0_5'
| 'nearest_1';
Exam section answer policy:
type ExamSectionAnswerPolicy = 'answer_all' | 'answer_any';
Exam group selection mode:
type ExamGroupSelectionMode =
| 'independent'
| 'keep_group_together'
| 'exclude_grouped';
Important: only independent is implemented now. Sending keep_group_together or exclude_grouped returns 400.
Exam export format:
type ExamExportFormat = 'html_doc' | 'docx' | 'pdf';
Important: current export implementation returns HTML content with Microsoft Word MIME for html_doc. docx and pdf are reserved by the enum, but frontend should treat html_doc as the supported format unless backend is extended.
4. Shared Response Shapes
4.1 Course Chapter
type CourseChapter = {
id: number;
courseId: number;
name: string;
chapterOrder: number;
isActive: number;
createdAt: string;
updatedAt: string;
};
4.2 Question Private Response
This response is for instructors only and includes answer keys.
type QuestionBankPrivateResponse = {
id: number;
questionId: number;
courseId: number;
chapterId: number;
questionType: QuestionBankType;
difficulty: QuestionBankDifficulty;
bloomLevel: BloomLevel;
status: QuestionBankStatus;
questionText: string | null;
expectedAnswerText: string | null;
hints: string | null;
options: QuestionOptionResponse[];
fillBlanks: FillBlankResponse[];
attachments: QuestionAttachmentResponse[];
groups: QuestionGroupSummary[];
};
type QuestionOptionResponse = {
optionId: number;
optionText: string;
isCorrect: boolean;
optionOrder: number;
};
type FillBlankResponse = {
blankId: number;
blankKey: string;
acceptableAnswer: string;
isCaseSensitive: boolean;
};
type QuestionAttachmentResponse = {
attachmentId: number;
fileId: number;
attachmentType: QuestionAttachmentType;
caption: string | null;
altText: string | null;
displayOrder: number;
isPrimary: boolean;
storagePath: string | null;
imageUrl?: string | null;
};
Nested question responses normalize attachment fields for frontend use. Direct attachment endpoints return the attachment entity shape below.
type QuestionAttachmentEntityResponse = {
id: number;
questionId: number;
fileId: number;
attachmentType: QuestionAttachmentType;
caption: string | null;
altText: string | null;
displayOrder: number;
isPrimary: number; // 0 or 1 in direct attachment endpoint responses
storagePath: string | null;
createdBy: number;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
};
type QuestionGroupSummary = {
groupItemId: number;
groupId: number;
itemOrder: number;
courseId: number;
chapterId: number;
title: string | null;
sharedPrompt: string | null;
sharedFileId: number | null;
groupType: QuestionGroupType;
};
4.3 Question Group Response
Group endpoints return TypeORM-shaped group objects.
type QuestionGroup = {
id: number;
courseId: number;
chapterId: number;
title: string | null;
sharedPrompt: string | null;
sharedFileId: number | null;
groupType: QuestionGroupType;
createdBy: number;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
items?: QuestionGroupItem[];
};
type QuestionGroupItem = {
id: number;
groupId: number;
questionId: number;
itemOrder: number;
createdAt: string;
};
4.4 File Upload Response
type FileResponse = {
fileId: number;
fileName: string;
originalFileName: string;
fileSize: number;
mimeType: string;
folderId?: number;
uploadedBy: number;
uploaderName?: string;
createdAt: string;
versionCount?: number;
imageUrl?: string | null;
};
4.5 Exam Response
List/get/save/publish/unpublish/archive exam endpoints return a compact response:
type ExamResponse = {
id: number;
courseId: number;
title: string;
totalMarks?: number | null;
status: ExamStatus;
publishedAt?: string | null;
archivedAt?: string | null;
itemCount?: number;
sectionCount?: number;
};
4.6 Exam Draft Response
Draft endpoints return TypeORM-shaped draft objects.
type ExamDraft = {
id: number;
courseId: number;
title: string;
generationRequestJson: Record<string, unknown>;
generatedBy: number;
seed: string;
totalMarks: number | null;
markDistributionMode: ExamMarkDistributionMode;
roundingPolicy: ExamRoundingPolicy;
status: ExamDraftStatus;
finalizedExamId: number | null;
finalizedBy: number | null;
finalizedAt: string | null;
failureReason: string | null;
expiresAt: string;
createdAt: string;
updatedAt: string;
sections?: ExamDraftSection[];
items?: ExamDraftItem[];
};
type ExamDraftSection = {
id: number;
draftId: number;
title: string;
instructions: string | null;
sectionOrder: number;
totalMarks: number | null;
answerPolicy: ExamSectionAnswerPolicy;
requiredAnswerCount: number | null;
createdAt: string;
updatedAt: string;
};
type ExamDraftItem = {
id: number;
draftId: number;
questionId: number;
draftSectionId: number | null;
chapterId: number;
questionType: QuestionBankType;
difficulty: QuestionBankDifficulty;
bloomLevel: BloomLevel;
weight: number;
weightUnits: number | null;
marks: number | null;
itemOrder: number;
overrideReason: string | null;
question?: QuestionBankPrivateResponse | Record<string, unknown>;
};
5. Question Bank Feature
5.1 What The Feature Supports
The question bank currently supports:
- Instructor-owned course chapters.
- Single question creation.
- Plain bulk question creation up to 50 questions per request.
- Related question groups.
- Batch creation of questions inside a group.
- Question image upload.
- Multiple ordered question attachments with captions and alt text.
- Question list filters.
- Question answer data for instructor authoring.
- Question review/status workflow: draft, approved, archived, restored.
- Immutable question versions internally for audit/snapshot support.
Questions must belong to an instructor-owned course and a chapter in that course.
5.2 Question Validation Rules
All question types require:
courseIdchapterIdquestionTypedifficultybloomLevel- At least one of
questionTextorquestionFileId
questionFileId must reference an image file accessible to the instructor. Supported MIME types:
image/jpegimage/pngimage/webpimage/gif
MCQ:
- Requires
optionswith at least 2 options. - Requires at least one option with
isCorrect: true. - Must not include
fillBlanks.
True/False:
- Requires exactly 2 options.
- Requires exactly one option with
isCorrect: true. - Must not include
fillBlanks.
Fill blanks:
- Requires
fillBlankswith at least one item. blankKeyvalues must be unique case-insensitively.- Must not include
options.
Written and essay:
- Require
expectedAnswerText. - Must not include
options. - Must not include
fillBlanks.
Update behavior:
- Omitting
optionsorfillBlankspreserves existing children when type still needs them. - Sending
optionsorfillBlanksreplaces that child collection. - Changing
questionTyperemoves incompatible child rows.
6. Question Bank Endpoints
6.1 Create Chapter
POST /api/courses/:courseId/chapters
Required path params:
courseId
Request body:
{
"name": "Chapter 1",
"chapterOrder": 1
}
Required fields:
name: string, max 200chapterOrder: integer >= 1
Response:
CourseChapter
6.2 List Chapters
GET /api/courses/:courseId/chapters
Response:
CourseChapter[]
6.3 Update Chapter
PATCH /api/courses/:courseId/chapters/:chapterId
Request body:
{
"name": "Chapter 2",
"chapterOrder": 2,
"isActive": 1
}
All body fields are optional:
name: string, max 200chapterOrder: integer >= 1isActive: integer, usually1or0
Response:
CourseChapter
6.4 Delete Chapter
DELETE /api/courses/:courseId/chapters/:chapterId
Response:
{
"message": "Chapter deleted successfully"
}
6.5 Upload Question Image
POST /api/question-bank/questions/upload-image
Content-Type: multipart/form-data
Form fields:
image: required binary file.
Supported file types:
- JPEG
- PNG
- WebP
- GIF
Response:
FileResponse
Use returned fileId as:
questionFileIdfor image-based questions.fileIdfor question attachments.
6.6 Create Question
POST /api/question-bank/questions
Base request body:
{
"courseId": 34,
"chapterId": 2,
"questionType": "mcq",
"difficulty": "medium",
"bloomLevel": "understanding",
"questionText": "Which HTTP method is idempotent?",
"questionFileId": 88,
"expectedAnswerText": null,
"hints": "Think about safe retries.",
"status": "draft",
"options": [
{ "optionText": "GET", "isCorrect": true },
{ "optionText": "POST", "isCorrect": false }
],
"fillBlanks": []
}
Required:
courseIdchapterIdquestionTypedifficultybloomLevelquestionTextorquestionFileId
Optional:
questionTextquestionFileIdexpectedAnswerTexthintsstatus, defaults todraftoptions, depending on typefillBlanks, depending on type
Response:
QuestionBankPrivateResponse
6.7 Create MCQ Example
{
"courseId": 34,
"chapterId": 2,
"questionType": "mcq",
"difficulty": "easy",
"bloomLevel": "remembering",
"questionText": "What does HTML stand for?",
"options": [
{ "optionText": "HyperText Markup Language", "isCorrect": true },
{ "optionText": "High Transfer Machine Language", "isCorrect": false },
{ "optionText": "Hyper Tool Multi Language", "isCorrect": false }
]
}
6.8 Create True/False Example
{
"courseId": 34,
"chapterId": 2,
"questionType": "true_false",
"difficulty": "easy",
"bloomLevel": "remembering",
"questionText": "CSS is used for styling web pages.",
"options": [
{ "optionText": "True", "isCorrect": true },
{ "optionText": "False", "isCorrect": false }
]
}
6.9 Create Fill-Blanks Example
{
"courseId": 34,
"chapterId": 2,
"questionType": "fill_blanks",
"difficulty": "medium",
"bloomLevel": "applying",
"questionText": "The HTTP status code for not found is {{code}}.",
"fillBlanks": [
{
"blankKey": "code",
"acceptableAnswer": "404",
"isCaseSensitive": false
}
]
}
6.10 Create Written/Essay Example
{
"courseId": 34,
"chapterId": 2,
"questionType": "essay",
"difficulty": "hard",
"bloomLevel": "evaluating",
"questionText": "Explain tradeoffs between server-side rendering and client-side rendering.",
"expectedAnswerText": "A complete answer should mention SEO, first paint, interactivity, caching, server cost, and complexity.",
"hints": "Compare performance and operational concerns."
}
6.11 Bulk Create Questions
POST /api/question-bank/questions/batch
Request body:
{
"courseId": 34,
"defaultChapterId": 2,
"questions": [
{
"courseId": 34,
"chapterId": 2,
"questionType": "mcq",
"difficulty": "easy",
"bloomLevel": "remembering",
"questionText": "Question 1",
"options": [
{ "optionText": "A", "isCorrect": true },
{ "optionText": "B", "isCorrect": false }
]
}
]
}
Required:
courseIdquestions: array, minimum 1, maximum 50
Optional:
defaultChapterId: used for items that do not includechapterId.
Rules:
- All questions are created transactionally.
- If any question is invalid, the whole batch fails.
- Every item must belong to the same course.
Response:
{
"count": 1,
"created": [
{
"id": 101,
"questionId": 101,
"courseId": 34,
"chapterId": 2,
"questionType": "mcq",
"difficulty": "easy",
"bloomLevel": "remembering",
"status": "draft",
"questionText": "Question 1",
"expectedAnswerText": null,
"hints": null,
"options": [],
"fillBlanks": [],
"attachments": [],
"groups": []
}
]
}
6.12 List Questions
GET /api/question-bank/questions
Query params:
| Param | Type | Required | Notes |
|---|---|---|---|
courseId |
number | no | Restricts to owned course. |
chapterId |
number | no | Filter by chapter. |
questionType |
enum | no | mcq, essay, etc. |
difficulty |
enum | no | easy, medium, hard. |
bloomLevel |
enum | no | Bloom enum. |
status |
enum | no | draft, approved, archived. |
search |
string | no | Case-insensitive text search. |
hasAttachments |
boolean | no | true or false. |
groupId |
number | no | Questions in group. |
createdBy |
number | no | Creator filter. |
page |
number | no | Default 1. |
limit |
number | no | Default 20, max 100. |
Response:
{
"total": 1,
"data": [
{
"id": 101,
"questionId": 101,
"courseId": 34,
"chapterId": 2,
"questionType": "mcq",
"difficulty": "easy",
"bloomLevel": "remembering",
"status": "approved",
"questionText": "What does HTML stand for?",
"expectedAnswerText": null,
"hints": null,
"options": [
{
"optionId": 1,
"optionText": "HyperText Markup Language",
"isCorrect": true,
"optionOrder": 0
}
],
"fillBlanks": [],
"attachments": [],
"groups": []
}
]
}
6.13 Get Question
GET /api/question-bank/questions/:id
Response:
QuestionBankPrivateResponse
6.14 Update Question
PATCH /api/question-bank/questions/:id
All body fields are optional:
{
"chapterId": 3,
"questionType": "mcq",
"difficulty": "medium",
"bloomLevel": "applying",
"questionText": "Updated text",
"questionFileId": null,
"expectedAnswerText": null,
"hints": "Updated hint",
"status": "draft",
"options": [
{ "optionText": "A", "isCorrect": true },
{ "optionText": "B", "isCorrect": false }
],
"fillBlanks": []
}
Response:
QuestionBankPrivateResponse
6.15 Delete Question
DELETE /api/question-bank/questions/:id
This archives/soft-deletes the question.
Response:
{
"message": "Question archived successfully"
}
6.16 Add Existing File As Attachment
POST /api/question-bank/questions/:id/attachments
Request body:
{
"fileId": 88,
"attachmentType": "image",
"caption": "Architecture diagram",
"altText": "Diagram showing request flow",
"displayOrder": 0,
"isPrimary": true
}
Required:
fileId
Optional:
attachmentType, defaults toimagecaption, max 500altText, max 500displayOrder, integer >= 0isPrimary
Rules:
- File must exist and be owned/shared/public for instructor access.
- If
attachmentTypeisimage, file MIME must be supported image type. isPrimary: trueunsets primary on other attachments for the same question.
Response:
QuestionAttachmentEntityResponse
6.17 Upload Attachment Image
POST /api/question-bank/questions/:id/attachments/upload-image
Content-Type: multipart/form-data
Form fields:
| Field | Type | Required | Notes |
|---|---|---|---|
image |
file | yes | JPEG, PNG, WebP, GIF. |
caption |
string | no | Max 500. |
altText |
string | no | Max 500. |
displayOrder |
number | no | >= 0. |
isPrimary |
boolean | no | Marks primary image. |
Response:
QuestionAttachmentEntityResponse
6.18 Reorder Attachments
PATCH /api/question-bank/questions/:id/attachments/reorder
Request body:
{
"items": [
{ "attachmentId": 10, "displayOrder": 0 },
{ "attachmentId": 11, "displayOrder": 1 }
]
}
Required:
items: non-empty arrayitems[].attachmentIditems[].displayOrder
Response:
QuestionAttachmentEntityResponse[]
6.19 Update Attachment Metadata
PATCH /api/question-bank/questions/:id/attachments/:attachmentId
Request body:
{
"caption": "Updated caption",
"altText": "Updated alt text",
"displayOrder": 2,
"isPrimary": false
}
All body fields are optional.
Response:
QuestionAttachmentEntityResponse
6.20 Delete Attachment
DELETE /api/question-bank/questions/:id/attachments/:attachmentId
Response:
{
"message": "Attachment removed successfully"
}
6.21 Create Question Group
POST /api/question-bank/groups
Request body:
{
"courseId": 34,
"chapterId": 2,
"title": "Read the passage and answer",
"sharedPrompt": "Read the following text, then answer questions 1-3.",
"sharedFileId": 90,
"groupType": "passage"
}
Required:
courseIdchapterId
Optional:
title, max 255sharedPromptsharedFileIdgroupType, defaults toother
Response:
QuestionGroup
6.22 List Question Groups
GET /api/question-bank/groups
Query params:
| Param | Type | Required | Notes |
|---|---|---|---|
courseId |
number | no | Restricts to owned course. |
chapterId |
number | no | Filter by chapter. |
page |
number | no | Default backend behavior uses 1. |
limit |
number | no | Service clamps to 100. |
Response:
{
"total": 1,
"data": [
{
"id": 20,
"courseId": 34,
"chapterId": 2,
"title": "Read the passage and answer",
"sharedPrompt": "Read the following text...",
"sharedFileId": 90,
"groupType": "passage",
"createdBy": 5,
"createdAt": "2026-05-04T00:00:00.000Z",
"updatedAt": "2026-05-04T00:00:00.000Z",
"deletedAt": null
}
]
}
6.23 Get Question Group
GET /api/question-bank/groups/:groupId
Response:
QuestionGroup
6.24 Update Question Group
PATCH /api/question-bank/groups/:groupId
Request body:
{
"title": "Updated group title",
"sharedPrompt": "Updated prompt",
"sharedFileId": null,
"groupType": "case_study"
}
All body fields are optional.
Response:
QuestionGroup
6.25 Delete Question Group
DELETE /api/question-bank/groups/:groupId
Deletes group metadata. It does not delete the questions.
Response:
{
"message": "Question group deleted successfully"
}
6.26 Batch Create Questions Inside Group
POST /api/question-bank/groups/:groupId/questions/batch
Request body:
{
"questions": [
{
"courseId": 34,
"chapterId": 2,
"questionType": "mcq",
"difficulty": "medium",
"bloomLevel": "understanding",
"questionText": "What is the main idea of the passage?",
"options": [
{ "optionText": "Idea A", "isCorrect": true },
{ "optionText": "Idea B", "isCorrect": false }
]
}
]
}
Required:
questions: non-empty array
Rules:
- Group course ownership is checked.
- Questions are created transactionally.
questionFileIdis checked for ownership/shared access and image MIME.- If any question fails, the whole batch fails.
Response:
{
"group": {
"id": 20,
"courseId": 34,
"chapterId": 2,
"title": "Read the passage and answer",
"sharedPrompt": "Read the following text...",
"sharedFileId": 90,
"groupType": "passage"
},
"questions": [
{
"id": 101,
"questionId": 101,
"courseId": 34,
"chapterId": 2,
"questionType": "mcq",
"difficulty": "medium",
"bloomLevel": "understanding",
"status": "draft",
"questionText": "What is the main idea of the passage?",
"expectedAnswerText": null,
"hints": null,
"options": [],
"fillBlanks": [],
"attachments": [],
"groups": [
{
"groupItemId": 1,
"groupId": 20,
"itemOrder": 0,
"courseId": 34,
"chapterId": 2,
"title": "Read the passage and answer",
"sharedPrompt": "Read the following text...",
"sharedFileId": 90,
"groupType": "passage"
}
]
}
]
}
6.27 Reorder Questions Inside Group
PATCH /api/question-bank/groups/:groupId/questions/reorder
Request body:
{
"items": [
{ "questionId": 101, "itemOrder": 0 },
{ "questionId": 102, "itemOrder": 1 }
]
}
Response:
QuestionGroupItem[]
6.28 Question Review And Status Endpoints
Submit for review:
POST /api/question-bank/questions/:id/submit-for-review
Response:
QuestionBankPrivateResponse
Approve:
POST /api/question-bank/questions/:id/approve
Request body:
{
"comment": "Approved for exam generation"
}
Response:
QuestionBankPrivateResponse
Reject:
POST /api/question-bank/questions/:id/reject
Request body:
{
"comment": "Needs clearer wording"
}
Response:
QuestionBankPrivateResponse
Archive:
POST /api/question-bank/questions/:id/archive
Request body:
{
"comment": "Outdated"
}
Response:
QuestionBankPrivateResponse
Restore:
POST /api/question-bank/questions/:id/restore
Request body:
{
"comment": "Restored after review"
}
Response:
QuestionBankPrivateResponse
Frontend note: only approved questions are eligible for exam generation.
7. Exam Generator Feature
7.1 What The Feature Supports
The exam generator currently supports:
- Generating drafts from approved question bank questions.
- Flat rule-based generation.
- Sectioned generation.
- Total marks and per-question marks.
- Mark distribution by weights, equal distribution, or manual values.
- Rounding policies.
- Draft expiration and lifecycle.
- Manual draft section create/update/delete/reorder.
- Manual draft item add/update/remove/reorder.
- Override reasons when adding/replacing questions outside the original generation rules.
- Saving a draft idempotently to a final exam.
- Publishing, unpublishing, and archiving exams.
- Exporting an exam as Word-compatible HTML document content.
- Immutable exam item snapshots for saved exams.
7.2 Exam Generation Rules
Generation selects only questions where:
courseIdmatches the exam course.- Question status is
approved. - Chapter matches the rule.
- Optional question type matches.
- Optional difficulty matches.
- Optional Bloom level matches.
- Question is not already selected in the same draft generation.
The backend returns a shortage error if it cannot satisfy any bucket.
Drafts expire after 24 hours. Expired drafts cannot be edited or saved.
8. Exam Endpoints
8.1 List Exams
GET /api/exams
GET /api/exams/list
Both routes call the same backend logic.
Query params:
| Param | Type | Required | Notes |
|---|---|---|---|
page |
number | no | Default 1. |
limit |
number | no | Default 20. |
courseId |
number | no | Restricts to owned course. |
status |
ExamStatus | no | draft, published, archived. |
dateFrom |
ISO date string | no | Inclusive created-at lower bound. |
dateTo |
ISO date string | no | Inclusive created-at upper bound. |
Response:
{
data: ExamResponse[];
meta: {
total: number;
page: number;
limit: number;
totalPages: number;
};
}
8.2 List Drafts
GET /api/exams/drafts
GET /api/exams/drafts/list
GET /api/exam-drafts
GET /api/exam-drafts/list
All routes call the same backend logic. Prefer /api/exams/drafts for new frontend work.
Query params:
| Param | Type | Required | Notes |
|---|---|---|---|
page |
number | no | Default 1. |
limit |
number | no | Default 20. |
courseId |
number | no | Restricts to owned course. |
status |
ExamDraftStatus | no | open, finalized, expired, cancelled, failed. |
dateFrom |
ISO date string | no | Inclusive created-at lower bound. |
dateTo |
ISO date string | no | Inclusive created-at upper bound. |
Response:
{
data: ExamDraft[];
meta: {
total: number;
page: number;
limit: number;
totalPages: number;
};
}
8.3 Get Draft
GET /api/exams/drafts/:draftId
GET /api/exam-drafts/:draftId
Prefer /api/exams/drafts/:draftId.
Response:
ExamDraft
8.4 Generate Exam Preview / Draft
POST /api/exams/generate-preview
This endpoint creates an editable draft and returns the selected draft data.
Flat request body:
{
"courseId": 34,
"title": "Web Development Midterm",
"rules": [
{
"chapterId": 2,
"count": 5,
"weightPerQuestion": 1,
"questionType": "mcq",
"difficulty": "easy",
"bloomLevel": "remembering"
}
],
"totalMarks": 50,
"markDistributionMode": "weight_normalized",
"roundingPolicy": "nearest_0_5",
"groupSelectionMode": "independent",
"seed": "optional-seed"
}
Sectioned request body:
{
"courseId": 34,
"title": "Web Development Final",
"sections": [
{
"title": "Part A - MCQ",
"instructions": "Answer all questions.",
"totalMarks": 20,
"answerPolicy": "answer_all",
"rules": [
{
"chapterId": 2,
"count": 10,
"weightPerQuestion": 1,
"questionType": "mcq"
}
]
},
{
"title": "Part B - Essay",
"instructions": "Answer any one question.",
"totalMarks": 30,
"answerPolicy": "answer_any",
"requiredAnswerCount": 1,
"rules": [
{
"chapterId": 3,
"count": 2,
"weightPerQuestion": 5,
"questionType": "essay",
"difficulty": "hard"
}
]
}
],
"markDistributionMode": "weight_normalized",
"roundingPolicy": "nearest_0_5",
"groupSelectionMode": "independent"
}
Required:
courseIdtitle- At least one of
rulesorsections
Flat rule required fields:
chapterIdcountweightPerQuestion
Flat rule optional fields:
questionTypedifficultybloomLevel
Section required fields:
titletotalMarksrules
Section optional fields:
instructionsanswerPolicy, defaults to backend/entity defaultanswer_allrequiredAnswerCount
Top-level optional fields:
totalMarksmarkDistributionMode, defaultweight_normalizedroundingPolicy, defaultnonegroupSelectionMode, onlyindependentsupportedseed
Response:
{
"draftId": 12,
"seed": "optional-seed-or-generated-uuid",
"totalQuestions": 10,
"totalWeight": 10,
"totalMarks": 50,
"sections": [
{
"id": 1,
"draftId": 12,
"title": "Part A - MCQ",
"instructions": "Answer all questions.",
"sectionOrder": 0,
"totalMarks": 20,
"answerPolicy": "answer_all",
"requiredAnswerCount": null
}
],
"items": [
{
"id": 1,
"draftId": 12,
"questionId": 101,
"draftSectionId": 1,
"chapterId": 2,
"questionType": "mcq",
"difficulty": "easy",
"bloomLevel": "remembering",
"weight": 1,
"weightUnits": 1,
"marks": 2,
"itemOrder": 0,
"overrideReason": null
}
]
}
8.5 Create Draft Section
POST /api/exams/drafts/:draftId/sections
Request body:
{
"title": "Part C",
"instructions": "Answer any two.",
"totalMarks": 20,
"answerPolicy": "answer_any",
"requiredAnswerCount": 2
}
Required:
title
Optional:
instructionstotalMarksanswerPolicyrequiredAnswerCount
Response:
ExamDraftSection
8.6 Reorder Draft Sections
PATCH /api/exams/drafts/:draftId/sections/reorder
Request body:
{
"items": [
{ "sectionId": 1, "sectionOrder": 0 },
{ "sectionId": 2, "sectionOrder": 1 }
]
}
Response:
ExamDraftSection[]
8.7 Update Draft Section
PATCH /api/exams/drafts/:draftId/sections/:sectionId
Request body:
{
"title": "Updated Part A",
"instructions": "Updated instructions.",
"totalMarks": 25,
"answerPolicy": "answer_all",
"requiredAnswerCount": null
}
All body fields are optional.
Response:
ExamDraftSection
8.8 Delete Draft Section
DELETE /api/exams/drafts/:draftId/sections/:sectionId
Response:
{
"message": "Draft section deleted successfully"
}
8.9 Add Draft Item
POST /api/exams/drafts/:draftId/items
Request body:
{
"questionId": 101,
"draftSectionId": 1,
"weightUnits": 1,
"marks": 2,
"overrideReason": "Needed to replace a missing question from the same chapter."
}
Required:
questionId
Optional:
draftSectionIdweightUnitsmarksoverrideReason
Rules:
- Draft must be open and not expired.
- Question must be approved.
- Question must belong to the same course as the draft.
- If the question does not match original generation constraints,
overrideReasonis required.
Response:
ExamDraftItem
8.10 Reorder Draft Items
PATCH /api/exams/drafts/:draftId/items/reorder
Request body:
{
"items": [
{ "itemId": 1, "itemOrder": 0 },
{ "itemId": 2, "itemOrder": 1 }
]
}
Required:
items: non-empty array- The payload must include every item in the draft exactly once.
Response:
ExamDraftItem[]
8.11 Update Draft Item
PATCH /api/exams/drafts/:draftId/items/:itemId
Request body:
{
"replacementQuestionId": 102,
"weight": 2,
"weightUnits": 2,
"marks": 5,
"draftSectionId": 1,
"itemOrder": 0,
"overrideReason": "Instructor intentionally selected a harder equivalent question."
}
All body fields are optional.
Rules:
- Draft must be open and not expired.
- Replacement question must be approved and same-course.
- If replacement does not match original generation constraints,
overrideReasonis required.
Response:
ExamDraftItem
8.12 Remove Draft Item
DELETE /api/exams/drafts/:draftId/items/:itemId
Rules:
- Draft must be open and not expired.
- Cannot remove the last item from an open draft.
Response:
{
"message": "Draft item removed successfully"
}
8.13 Save Draft To Exam
POST /api/exams/drafts/:draftId/save
No request body.
Rules:
- Draft must be open and not expired.
- Draft must contain at least one item.
- Save is idempotent. If already finalized, backend returns the existing exam.
- Saved exam gets immutable item snapshots.
Response:
ExamResponse
8.14 Get Exam
GET /api/exams/:id
Response:
ExamResponse
8.15 Publish Exam
POST /api/exams/:id/publish
Request body:
{
"reason": "Ready for review"
}
Optional:
reason, max 1000
Response:
ExamResponse
8.16 Unpublish Exam
POST /api/exams/:id/unpublish
Request body:
{
"reason": "Need edits before publishing"
}
Response:
ExamResponse
8.17 Archive Exam
POST /api/exams/:id/archive
Request body:
{
"reason": "Old version"
}
Response:
ExamResponse
8.18 Export Exam Word
POST /api/exams/:id/export-word
Request body:
{
"format": "html_doc",
"includeAnswerKey": false
}
Optional:
format, default/current practical value:html_docincludeAnswerKey, default false
Response:
{
"fileName": "exam-1.doc",
"mimeType": "application/msword",
"content": "<html><head><meta charset=\"utf-8\"></head><body>...</body></html>"
}
Frontend usage:
- Create a
Blobfromcontent. - Use
mimeType. - Download with
fileName.
Example:
const blob = new Blob([response.content], { type: response.mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = response.fileName;
a.click();
URL.revokeObjectURL(url);
9. Recommended Frontend Workflows
9.1 Question Authoring Workflow
- Load chapters:
GET /api/courses/:courseId/chapters. - Optionally upload an image:
POST /api/question-bank/questions/upload-image. - Create question:
POST /api/question-bank/questions. - Optionally add more attachments:
- existing file:
POST /api/question-bank/questions/:id/attachments - upload image:
POST /api/question-bank/questions/:id/attachments/upload-image
- existing file:
- Preview the returned
QuestionBankPrivateResponse. - Approve when ready:
POST /api/question-bank/questions/:id/approve.
Frontend should block or warn on invalid question-type combinations before calling the API.
9.2 Bulk Question Authoring Workflow
- Build an array of up to 50 questions.
- Validate each question locally using the rules in Section 5.2.
- Submit once:
POST /api/question-bank/questions/batch. - If the request fails, show the backend message and let the instructor fix the batch.
9.3 Related/Grouped Question Workflow
- Create group:
POST /api/question-bank/groups. - Add grouped questions:
POST /api/question-bank/groups/:groupId/questions/batch. - Reorder if needed:
PATCH /api/question-bank/groups/:groupId/questions/reorder. - Render group prompt/file at top and questions below using
groupsin question response.
9.4 Exam Generation Workflow
- Ensure enough approved questions exist. List questions by
status=approved. - Build generation rules or sections.
- Generate draft:
POST /api/exams/generate-preview. - Show returned sections/items for instructor review.
- Let instructor add, replace, remove, or reorder items.
- Save:
POST /api/exams/drafts/:draftId/save. - Optionally publish:
POST /api/exams/:id/publish. - Export:
POST /api/exams/:id/export-word.
9.5 Draft Editing UX Rules
Disable editing when:
status !== 'open'expiresAtis in the past
Show warnings when:
- User tries to remove the last draft item.
- User replaces/adds a question outside the original generation rules and no
overrideReasonis entered. - Reorder payload does not include all items.
10. Frontend Field Reference
10.1 Required Question Form Fields
Always show:
- Course selector
- Chapter selector
- Question type
- Difficulty
- Bloom level
- Question text and/or image upload
Show for MCQ:
- Options editor
- At least two options
- Correct option checkbox on each option
Show for true/false:
- Two fixed options: True and False
- Exactly one correct option
Show for fill blanks:
- Blank key
- Acceptable answer
- Case-sensitive toggle
Show for written/essay:
- Expected answer text
- Optional hints
10.2 Attachment Form Fields
- File/image
- Caption
- Alt text
- Display order
- Primary image toggle
10.3 Exam Generator Form Fields
Flat generator:
- Course
- Title
- Total marks
- Mark distribution mode
- Rounding policy
- Rules: chapter, count, weight per question, optional type/difficulty/Bloom
Sectioned generator:
- Course
- Title
- Sections
- Per-section title
- Per-section instructions
- Per-section total marks
- Per-section answer policy
- Per-section required answer count when answer policy is
answer_any - Per-section rules
11. Important Backend Constraints For Frontend
- Only instructor JWTs can use these APIs.
- Course ownership is enforced server-side.
- Only approved questions are used by exam generation.
- Drafts expire after 24 hours.
- Draft save creates immutable snapshots, so later question edits do not change saved exams.
- Student exam delivery is not implemented in this feature scope.
groupSelectionModemust currently be omitted or set toindependent.- Export currently returns document content directly, not a stored file URL.
- Question list/get responses expose correct answers because they are instructor-only.
12. Minimal TypeScript Client Types
export type ApiList<T> = {
data: T[];
total: number;
};
export type ApiPage<T> = {
data: T[];
meta: {
total: number;
page: number;
limit: number;
totalPages: number;
};
};
export type ApiMessage = {
message: string;
};
Recommended frontend route modules:
questionBankApi.tsquestionGroupsApi.tsexamGeneratorApi.tsexamDraftsApi.tsexamExportApi.ts
13. Quick Endpoint Index
Question bank chapters:
POST /api/courses/:courseId/chapters
GET /api/courses/:courseId/chapters
PATCH /api/courses/:courseId/chapters/:chapterId
DELETE /api/courses/:courseId/chapters/:chapterId
Question bank questions:
POST /api/question-bank/questions
POST /api/question-bank/questions/batch
POST /api/question-bank/questions/upload-image
GET /api/question-bank/questions
GET /api/question-bank/questions/:id
PATCH /api/question-bank/questions/:id
DELETE /api/question-bank/questions/:id
Question attachments:
POST /api/question-bank/questions/:id/attachments
POST /api/question-bank/questions/:id/attachments/upload-image
PATCH /api/question-bank/questions/:id/attachments/reorder
PATCH /api/question-bank/questions/:id/attachments/:attachmentId
DELETE /api/question-bank/questions/:id/attachments/:attachmentId
Question groups:
POST /api/question-bank/groups
GET /api/question-bank/groups
GET /api/question-bank/groups/:groupId
PATCH /api/question-bank/groups/:groupId
DELETE /api/question-bank/groups/:groupId
POST /api/question-bank/groups/:groupId/questions/batch
PATCH /api/question-bank/groups/:groupId/questions/reorder
Question review:
POST /api/question-bank/questions/:id/submit-for-review
POST /api/question-bank/questions/:id/approve
POST /api/question-bank/questions/:id/reject
POST /api/question-bank/questions/:id/archive
POST /api/question-bank/questions/:id/restore
Exams and drafts:
GET /api/exams
GET /api/exams/list
GET /api/exams/drafts
GET /api/exams/drafts/list
GET /api/exams/drafts/:draftId
GET /api/exam-drafts
GET /api/exam-drafts/list
GET /api/exam-drafts/:draftId
POST /api/exams/generate-preview
Draft sections:
POST /api/exams/drafts/:draftId/sections
PATCH /api/exams/drafts/:draftId/sections/reorder
PATCH /api/exams/drafts/:draftId/sections/:sectionId
DELETE /api/exams/drafts/:draftId/sections/:sectionId
Draft items:
POST /api/exams/drafts/:draftId/items
PATCH /api/exams/drafts/:draftId/items/reorder
PATCH /api/exams/drafts/:draftId/items/:itemId
DELETE /api/exams/drafts/:draftId/items/:itemId
Saved exams:
POST /api/exams/drafts/:draftId/save
GET /api/exams/:id
POST /api/exams/:id/publish
POST /api/exams/:id/unpublish
POST /api/exams/:id/archive
POST /api/exams/:id/export-word