eduverse-backend / questionBank_Exam_features_docs.md
amirhamdi's picture
feat(assessment): implement Question Bank and Exam Generator backend
ffd0171
|
Raw
History Blame Contribute Delete
42.4 kB
# 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:
1. `INSTRUCTOR`
Blocked roles for these features:
1. `STUDENT`
2. `TA`
3. `ADMIN`
Every endpoint below requires:
```http
Authorization: Bearer <instructor_jwt>
Content-Type: application/json
```
Multipart upload endpoints use:
```http
Authorization: Bearer <instructor_jwt>
Content-Type: multipart/form-data
```
The backend also checks course ownership. An instructor can only manage courses assigned through:
```text
course_instructors -> course_sections -> course_id
```
## 2. Base URL And Common Conventions
Examples use:
```text
{{baseUrl}}/api
```
IDs are numeric.
Dates are ISO strings when returned or sent as query filters.
Pagination response shape for exam list APIs:
```json
{
"data": [],
"meta": {
"total": 0,
"page": 1,
"limit": 20,
"totalPages": 0
}
}
```
Question bank list response shape:
```json
{
"data": [],
"total": 0
}
```
Common NestJS error shape:
```json
{
"statusCode": 400,
"message": "Validation or business error message",
"error": "Bad Request"
}
```
For exam-generation shortages, `message` can be an object:
```json
{
"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:
```ts
type QuestionBankType =
| 'written'
| 'mcq'
| 'true_false'
| 'fill_blanks'
| 'essay';
```
Question difficulty:
```ts
type QuestionBankDifficulty = 'easy' | 'medium' | 'hard';
```
Bloom levels:
```ts
type BloomLevel =
| 'remembering'
| 'understanding'
| 'applying'
| 'analyzing'
| 'evaluating'
| 'creating';
```
Question status:
```ts
type QuestionBankStatus = 'draft' | 'approved' | 'archived';
```
Attachment type:
```ts
type QuestionAttachmentType = 'image' | 'document' | 'audio' | 'video';
```
Question group type:
```ts
type QuestionGroupType =
| 'passage'
| 'case_study'
| 'image_set'
| 'multipart'
| 'other';
```
Exam draft status:
```ts
type ExamDraftStatus =
| 'open'
| 'finalized'
| 'expired'
| 'cancelled'
| 'failed';
```
Exam status:
```ts
type ExamStatus = 'draft' | 'published' | 'archived';
```
Exam mark distribution mode:
```ts
type ExamMarkDistributionMode =
| 'manual'
| 'weight_normalized'
| 'equal';
```
Exam rounding policy:
```ts
type ExamRoundingPolicy =
| 'none'
| 'nearest_0_25'
| 'nearest_0_5'
| 'nearest_1';
```
Exam section answer policy:
```ts
type ExamSectionAnswerPolicy = 'answer_all' | 'answer_any';
```
Exam group selection mode:
```ts
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:
```ts
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
```ts
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.
```ts
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[];
};
```
```ts
type QuestionOptionResponse = {
optionId: number;
optionText: string;
isCorrect: boolean;
optionOrder: number;
};
```
```ts
type FillBlankResponse = {
blankId: number;
blankKey: string;
acceptableAnswer: string;
isCaseSensitive: boolean;
};
```
```ts
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.
```ts
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;
};
```
```ts
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.
```ts
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[];
};
```
```ts
type QuestionGroupItem = {
id: number;
groupId: number;
questionId: number;
itemOrder: number;
createdAt: string;
};
```
### 4.4 File Upload Response
```ts
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:
```ts
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.
```ts
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[];
};
```
```ts
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;
};
```
```ts
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:
1. Instructor-owned course chapters.
2. Single question creation.
3. Plain bulk question creation up to 50 questions per request.
4. Related question groups.
5. Batch creation of questions inside a group.
6. Question image upload.
7. Multiple ordered question attachments with captions and alt text.
8. Question list filters.
9. Question answer data for instructor authoring.
10. Question review/status workflow: draft, approved, archived, restored.
11. 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:
1. `courseId`
2. `chapterId`
3. `questionType`
4. `difficulty`
5. `bloomLevel`
6. At least one of `questionText` or `questionFileId`
`questionFileId` must reference an image file accessible to the instructor. Supported MIME types:
1. `image/jpeg`
2. `image/png`
3. `image/webp`
4. `image/gif`
MCQ:
1. Requires `options` with at least 2 options.
2. Requires at least one option with `isCorrect: true`.
3. Must not include `fillBlanks`.
True/False:
1. Requires exactly 2 options.
2. Requires exactly one option with `isCorrect: true`.
3. Must not include `fillBlanks`.
Fill blanks:
1. Requires `fillBlanks` with at least one item.
2. `blankKey` values must be unique case-insensitively.
3. Must not include `options`.
Written and essay:
1. Require `expectedAnswerText`.
2. Must not include `options`.
3. Must not include `fillBlanks`.
Update behavior:
1. Omitting `options` or `fillBlanks` preserves existing children when type still needs them.
2. Sending `options` or `fillBlanks` replaces that child collection.
3. Changing `questionType` removes incompatible child rows.
## 6. Question Bank Endpoints
### 6.1 Create Chapter
```http
POST /api/courses/:courseId/chapters
```
Required path params:
1. `courseId`
Request body:
```json
{
"name": "Chapter 1",
"chapterOrder": 1
}
```
Required fields:
1. `name`: string, max 200
2. `chapterOrder`: integer >= 1
Response:
```ts
CourseChapter
```
### 6.2 List Chapters
```http
GET /api/courses/:courseId/chapters
```
Response:
```ts
CourseChapter[]
```
### 6.3 Update Chapter
```http
PATCH /api/courses/:courseId/chapters/:chapterId
```
Request body:
```json
{
"name": "Chapter 2",
"chapterOrder": 2,
"isActive": 1
}
```
All body fields are optional:
1. `name`: string, max 200
2. `chapterOrder`: integer >= 1
3. `isActive`: integer, usually `1` or `0`
Response:
```ts
CourseChapter
```
### 6.4 Delete Chapter
```http
DELETE /api/courses/:courseId/chapters/:chapterId
```
Response:
```json
{
"message": "Chapter deleted successfully"
}
```
### 6.5 Upload Question Image
```http
POST /api/question-bank/questions/upload-image
Content-Type: multipart/form-data
```
Form fields:
1. `image`: required binary file.
Supported file types:
1. JPEG
2. PNG
3. WebP
4. GIF
Response:
```ts
FileResponse
```
Use returned `fileId` as:
1. `questionFileId` for image-based questions.
2. `fileId` for question attachments.
### 6.6 Create Question
```http
POST /api/question-bank/questions
```
Base request body:
```json
{
"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:
1. `courseId`
2. `chapterId`
3. `questionType`
4. `difficulty`
5. `bloomLevel`
6. `questionText` or `questionFileId`
Optional:
1. `questionText`
2. `questionFileId`
3. `expectedAnswerText`
4. `hints`
5. `status`, defaults to `draft`
6. `options`, depending on type
7. `fillBlanks`, depending on type
Response:
```ts
QuestionBankPrivateResponse
```
### 6.7 Create MCQ Example
```json
{
"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
```json
{
"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
```json
{
"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
```json
{
"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
```http
POST /api/question-bank/questions/batch
```
Request body:
```json
{
"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:
1. `courseId`
2. `questions`: array, minimum 1, maximum 50
Optional:
1. `defaultChapterId`: used for items that do not include `chapterId`.
Rules:
1. All questions are created transactionally.
2. If any question is invalid, the whole batch fails.
3. Every item must belong to the same course.
Response:
```json
{
"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
```http
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:
```json
{
"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
```http
GET /api/question-bank/questions/:id
```
Response:
```ts
QuestionBankPrivateResponse
```
### 6.14 Update Question
```http
PATCH /api/question-bank/questions/:id
```
All body fields are optional:
```json
{
"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:
```ts
QuestionBankPrivateResponse
```
### 6.15 Delete Question
```http
DELETE /api/question-bank/questions/:id
```
This archives/soft-deletes the question.
Response:
```json
{
"message": "Question archived successfully"
}
```
### 6.16 Add Existing File As Attachment
```http
POST /api/question-bank/questions/:id/attachments
```
Request body:
```json
{
"fileId": 88,
"attachmentType": "image",
"caption": "Architecture diagram",
"altText": "Diagram showing request flow",
"displayOrder": 0,
"isPrimary": true
}
```
Required:
1. `fileId`
Optional:
1. `attachmentType`, defaults to `image`
2. `caption`, max 500
3. `altText`, max 500
4. `displayOrder`, integer >= 0
5. `isPrimary`
Rules:
1. File must exist and be owned/shared/public for instructor access.
2. If `attachmentType` is `image`, file MIME must be supported image type.
3. `isPrimary: true` unsets primary on other attachments for the same question.
Response:
```ts
QuestionAttachmentEntityResponse
```
### 6.17 Upload Attachment Image
```http
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:
```ts
QuestionAttachmentEntityResponse
```
### 6.18 Reorder Attachments
```http
PATCH /api/question-bank/questions/:id/attachments/reorder
```
Request body:
```json
{
"items": [
{ "attachmentId": 10, "displayOrder": 0 },
{ "attachmentId": 11, "displayOrder": 1 }
]
}
```
Required:
1. `items`: non-empty array
2. `items[].attachmentId`
3. `items[].displayOrder`
Response:
```ts
QuestionAttachmentEntityResponse[]
```
### 6.19 Update Attachment Metadata
```http
PATCH /api/question-bank/questions/:id/attachments/:attachmentId
```
Request body:
```json
{
"caption": "Updated caption",
"altText": "Updated alt text",
"displayOrder": 2,
"isPrimary": false
}
```
All body fields are optional.
Response:
```ts
QuestionAttachmentEntityResponse
```
### 6.20 Delete Attachment
```http
DELETE /api/question-bank/questions/:id/attachments/:attachmentId
```
Response:
```json
{
"message": "Attachment removed successfully"
}
```
### 6.21 Create Question Group
```http
POST /api/question-bank/groups
```
Request body:
```json
{
"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:
1. `courseId`
2. `chapterId`
Optional:
1. `title`, max 255
2. `sharedPrompt`
3. `sharedFileId`
4. `groupType`, defaults to `other`
Response:
```ts
QuestionGroup
```
### 6.22 List Question Groups
```http
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:
```json
{
"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
```http
GET /api/question-bank/groups/:groupId
```
Response:
```ts
QuestionGroup
```
### 6.24 Update Question Group
```http
PATCH /api/question-bank/groups/:groupId
```
Request body:
```json
{
"title": "Updated group title",
"sharedPrompt": "Updated prompt",
"sharedFileId": null,
"groupType": "case_study"
}
```
All body fields are optional.
Response:
```ts
QuestionGroup
```
### 6.25 Delete Question Group
```http
DELETE /api/question-bank/groups/:groupId
```
Deletes group metadata. It does not delete the questions.
Response:
```json
{
"message": "Question group deleted successfully"
}
```
### 6.26 Batch Create Questions Inside Group
```http
POST /api/question-bank/groups/:groupId/questions/batch
```
Request body:
```json
{
"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:
1. `questions`: non-empty array
Rules:
1. Group course ownership is checked.
2. Questions are created transactionally.
3. `questionFileId` is checked for ownership/shared access and image MIME.
4. If any question fails, the whole batch fails.
Response:
```json
{
"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
```http
PATCH /api/question-bank/groups/:groupId/questions/reorder
```
Request body:
```json
{
"items": [
{ "questionId": 101, "itemOrder": 0 },
{ "questionId": 102, "itemOrder": 1 }
]
}
```
Response:
```ts
QuestionGroupItem[]
```
### 6.28 Question Review And Status Endpoints
Submit for review:
```http
POST /api/question-bank/questions/:id/submit-for-review
```
Response:
```ts
QuestionBankPrivateResponse
```
Approve:
```http
POST /api/question-bank/questions/:id/approve
```
Request body:
```json
{
"comment": "Approved for exam generation"
}
```
Response:
```ts
QuestionBankPrivateResponse
```
Reject:
```http
POST /api/question-bank/questions/:id/reject
```
Request body:
```json
{
"comment": "Needs clearer wording"
}
```
Response:
```ts
QuestionBankPrivateResponse
```
Archive:
```http
POST /api/question-bank/questions/:id/archive
```
Request body:
```json
{
"comment": "Outdated"
}
```
Response:
```ts
QuestionBankPrivateResponse
```
Restore:
```http
POST /api/question-bank/questions/:id/restore
```
Request body:
```json
{
"comment": "Restored after review"
}
```
Response:
```ts
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:
1. Generating drafts from approved question bank questions.
2. Flat rule-based generation.
3. Sectioned generation.
4. Total marks and per-question marks.
5. Mark distribution by weights, equal distribution, or manual values.
6. Rounding policies.
7. Draft expiration and lifecycle.
8. Manual draft section create/update/delete/reorder.
9. Manual draft item add/update/remove/reorder.
10. Override reasons when adding/replacing questions outside the original generation rules.
11. Saving a draft idempotently to a final exam.
12. Publishing, unpublishing, and archiving exams.
13. Exporting an exam as Word-compatible HTML document content.
14. Immutable exam item snapshots for saved exams.
### 7.2 Exam Generation Rules
Generation selects only questions where:
1. `courseId` matches the exam course.
2. Question status is `approved`.
3. Chapter matches the rule.
4. Optional question type matches.
5. Optional difficulty matches.
6. Optional Bloom level matches.
7. 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
```http
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:
```ts
{
data: ExamResponse[];
meta: {
total: number;
page: number;
limit: number;
totalPages: number;
};
}
```
### 8.2 List Drafts
```http
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:
```ts
{
data: ExamDraft[];
meta: {
total: number;
page: number;
limit: number;
totalPages: number;
};
}
```
### 8.3 Get Draft
```http
GET /api/exams/drafts/:draftId
GET /api/exam-drafts/:draftId
```
Prefer `/api/exams/drafts/:draftId`.
Response:
```ts
ExamDraft
```
### 8.4 Generate Exam Preview / Draft
```http
POST /api/exams/generate-preview
```
This endpoint creates an editable draft and returns the selected draft data.
Flat request body:
```json
{
"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:
```json
{
"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:
1. `courseId`
2. `title`
3. At least one of `rules` or `sections`
Flat rule required fields:
1. `chapterId`
2. `count`
3. `weightPerQuestion`
Flat rule optional fields:
1. `questionType`
2. `difficulty`
3. `bloomLevel`
Section required fields:
1. `title`
2. `totalMarks`
3. `rules`
Section optional fields:
1. `instructions`
2. `answerPolicy`, defaults to backend/entity default `answer_all`
3. `requiredAnswerCount`
Top-level optional fields:
1. `totalMarks`
2. `markDistributionMode`, default `weight_normalized`
3. `roundingPolicy`, default `none`
4. `groupSelectionMode`, only `independent` supported
5. `seed`
Response:
```json
{
"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
```http
POST /api/exams/drafts/:draftId/sections
```
Request body:
```json
{
"title": "Part C",
"instructions": "Answer any two.",
"totalMarks": 20,
"answerPolicy": "answer_any",
"requiredAnswerCount": 2
}
```
Required:
1. `title`
Optional:
1. `instructions`
2. `totalMarks`
3. `answerPolicy`
4. `requiredAnswerCount`
Response:
```ts
ExamDraftSection
```
### 8.6 Reorder Draft Sections
```http
PATCH /api/exams/drafts/:draftId/sections/reorder
```
Request body:
```json
{
"items": [
{ "sectionId": 1, "sectionOrder": 0 },
{ "sectionId": 2, "sectionOrder": 1 }
]
}
```
Response:
```ts
ExamDraftSection[]
```
### 8.7 Update Draft Section
```http
PATCH /api/exams/drafts/:draftId/sections/:sectionId
```
Request body:
```json
{
"title": "Updated Part A",
"instructions": "Updated instructions.",
"totalMarks": 25,
"answerPolicy": "answer_all",
"requiredAnswerCount": null
}
```
All body fields are optional.
Response:
```ts
ExamDraftSection
```
### 8.8 Delete Draft Section
```http
DELETE /api/exams/drafts/:draftId/sections/:sectionId
```
Response:
```json
{
"message": "Draft section deleted successfully"
}
```
### 8.9 Add Draft Item
```http
POST /api/exams/drafts/:draftId/items
```
Request body:
```json
{
"questionId": 101,
"draftSectionId": 1,
"weightUnits": 1,
"marks": 2,
"overrideReason": "Needed to replace a missing question from the same chapter."
}
```
Required:
1. `questionId`
Optional:
1. `draftSectionId`
2. `weightUnits`
3. `marks`
4. `overrideReason`
Rules:
1. Draft must be open and not expired.
2. Question must be approved.
3. Question must belong to the same course as the draft.
4. If the question does not match original generation constraints, `overrideReason` is required.
Response:
```ts
ExamDraftItem
```
### 8.10 Reorder Draft Items
```http
PATCH /api/exams/drafts/:draftId/items/reorder
```
Request body:
```json
{
"items": [
{ "itemId": 1, "itemOrder": 0 },
{ "itemId": 2, "itemOrder": 1 }
]
}
```
Required:
1. `items`: non-empty array
2. The payload must include every item in the draft exactly once.
Response:
```ts
ExamDraftItem[]
```
### 8.11 Update Draft Item
```http
PATCH /api/exams/drafts/:draftId/items/:itemId
```
Request body:
```json
{
"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:
1. Draft must be open and not expired.
2. Replacement question must be approved and same-course.
3. If replacement does not match original generation constraints, `overrideReason` is required.
Response:
```ts
ExamDraftItem
```
### 8.12 Remove Draft Item
```http
DELETE /api/exams/drafts/:draftId/items/:itemId
```
Rules:
1. Draft must be open and not expired.
2. Cannot remove the last item from an open draft.
Response:
```json
{
"message": "Draft item removed successfully"
}
```
### 8.13 Save Draft To Exam
```http
POST /api/exams/drafts/:draftId/save
```
No request body.
Rules:
1. Draft must be open and not expired.
2. Draft must contain at least one item.
3. Save is idempotent. If already finalized, backend returns the existing exam.
4. Saved exam gets immutable item snapshots.
Response:
```ts
ExamResponse
```
### 8.14 Get Exam
```http
GET /api/exams/:id
```
Response:
```ts
ExamResponse
```
### 8.15 Publish Exam
```http
POST /api/exams/:id/publish
```
Request body:
```json
{
"reason": "Ready for review"
}
```
Optional:
1. `reason`, max 1000
Response:
```ts
ExamResponse
```
### 8.16 Unpublish Exam
```http
POST /api/exams/:id/unpublish
```
Request body:
```json
{
"reason": "Need edits before publishing"
}
```
Response:
```ts
ExamResponse
```
### 8.17 Archive Exam
```http
POST /api/exams/:id/archive
```
Request body:
```json
{
"reason": "Old version"
}
```
Response:
```ts
ExamResponse
```
### 8.18 Export Exam Word
```http
POST /api/exams/:id/export-word
```
Request body:
```json
{
"format": "html_doc",
"includeAnswerKey": false
}
```
Optional:
1. `format`, default/current practical value: `html_doc`
2. `includeAnswerKey`, default false
Response:
```json
{
"fileName": "exam-1.doc",
"mimeType": "application/msword",
"content": "<html><head><meta charset=\"utf-8\"></head><body>...</body></html>"
}
```
Frontend usage:
1. Create a `Blob` from `content`.
2. Use `mimeType`.
3. Download with `fileName`.
Example:
```ts
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
1. Load chapters: `GET /api/courses/:courseId/chapters`.
2. Optionally upload an image: `POST /api/question-bank/questions/upload-image`.
3. Create question: `POST /api/question-bank/questions`.
4. Optionally add more attachments:
- existing file: `POST /api/question-bank/questions/:id/attachments`
- upload image: `POST /api/question-bank/questions/:id/attachments/upload-image`
5. Preview the returned `QuestionBankPrivateResponse`.
6. 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
1. Build an array of up to 50 questions.
2. Validate each question locally using the rules in Section 5.2.
3. Submit once: `POST /api/question-bank/questions/batch`.
4. If the request fails, show the backend message and let the instructor fix the batch.
### 9.3 Related/Grouped Question Workflow
1. Create group: `POST /api/question-bank/groups`.
2. Add grouped questions: `POST /api/question-bank/groups/:groupId/questions/batch`.
3. Reorder if needed: `PATCH /api/question-bank/groups/:groupId/questions/reorder`.
4. Render group prompt/file at top and questions below using `groups` in question response.
### 9.4 Exam Generation Workflow
1. Ensure enough approved questions exist. List questions by `status=approved`.
2. Build generation rules or sections.
3. Generate draft: `POST /api/exams/generate-preview`.
4. Show returned sections/items for instructor review.
5. Let instructor add, replace, remove, or reorder items.
6. Save: `POST /api/exams/drafts/:draftId/save`.
7. Optionally publish: `POST /api/exams/:id/publish`.
8. Export: `POST /api/exams/:id/export-word`.
### 9.5 Draft Editing UX Rules
Disable editing when:
1. `status !== 'open'`
2. `expiresAt` is in the past
Show warnings when:
1. User tries to remove the last draft item.
2. User replaces/adds a question outside the original generation rules and no `overrideReason` is entered.
3. Reorder payload does not include all items.
## 10. Frontend Field Reference
### 10.1 Required Question Form Fields
Always show:
1. Course selector
2. Chapter selector
3. Question type
4. Difficulty
5. Bloom level
6. Question text and/or image upload
Show for MCQ:
1. Options editor
2. At least two options
3. Correct option checkbox on each option
Show for true/false:
1. Two fixed options: True and False
2. Exactly one correct option
Show for fill blanks:
1. Blank key
2. Acceptable answer
3. Case-sensitive toggle
Show for written/essay:
1. Expected answer text
2. Optional hints
### 10.2 Attachment Form Fields
1. File/image
2. Caption
3. Alt text
4. Display order
5. Primary image toggle
### 10.3 Exam Generator Form Fields
Flat generator:
1. Course
2. Title
3. Total marks
4. Mark distribution mode
5. Rounding policy
6. Rules: chapter, count, weight per question, optional type/difficulty/Bloom
Sectioned generator:
1. Course
2. Title
3. Sections
4. Per-section title
5. Per-section instructions
6. Per-section total marks
7. Per-section answer policy
8. Per-section required answer count when answer policy is `answer_any`
9. Per-section rules
## 11. Important Backend Constraints For Frontend
1. Only instructor JWTs can use these APIs.
2. Course ownership is enforced server-side.
3. Only approved questions are used by exam generation.
4. Drafts expire after 24 hours.
5. Draft save creates immutable snapshots, so later question edits do not change saved exams.
6. Student exam delivery is not implemented in this feature scope.
7. `groupSelectionMode` must currently be omitted or set to `independent`.
8. Export currently returns document content directly, not a stored file URL.
9. Question list/get responses expose correct answers because they are instructor-only.
## 12. Minimal TypeScript Client Types
```ts
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:
1. `questionBankApi.ts`
2. `questionGroupsApi.ts`
3. `examGeneratorApi.ts`
4. `examDraftsApi.ts`
5. `examExportApi.ts`
## 13. Quick Endpoint Index
Question bank chapters:
```text
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:
```text
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:
```text
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:
```text
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:
```text
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:
```text
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:
```text
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:
```text
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:
```text
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
```