# Backend (Postman) vs Frontend — fields & endpoints to decide on Source of truth: the Postman collection covering Terms, Courses, Sessions, Exams, Homeworks, Media (2026-05). This doc lists every place where backend and FE disagree on shape, plus FE-side concepts the backend doc has no slot for. Each item needs a product/design call before we wire it up. Conventions in the rest of this doc: - **B → FE** means the backend exposes a field/endpoint that the FE doesn't surface yet. - **FE → B** means the FE shows/sends a field the backend doc doesn't accept. - **shape diff** means both sides handle the concept but in different shapes (enum values, nesting, naming). --- ## Terms ### Endpoints | Backend | FE today | Status | |---|---|---| | `GET /terms?per_page&active_only` | `getTermsList` | aligned | | `GET /terms/:id` | `showTerm` | aligned | | `POST /terms` | `addNewTerm` | aligned | | `PATCH /terms/:id` | `updateTerm` | aligned | | `DELETE /terms/:id` | `deleteTerm` | aligned | | — | `cloneTerm` (`POST /admin/terms/:id/clone`) | **FE-only; backend has nothing.** Decide: drop the clone button, or ask backend to add it. | | — | `changeStatusTerm` (`POST /admin/terms/:id/status`) | **Use `PATCH /terms/:id` with `is_active`** — dedicated status endpoint dropped. | | — | `listUserTerm`, `addUserTerm`, `removeUserTerm`, `changeLeaveStatus` | **FE-only.** Term-students subtab + leave toggle. Backend exposes nothing equivalent. Keep mock-only until backend adds. | | — | `listCourseTerm`, `addCourseTerm`, `removeCourseTerm` | **FE-only.** Can be replaced by `GET /courses?term_id=` for the list; the attach/detach side has no backend equivalent (course `term_id` is set at create-time). | ### Fields | Backend → FE | FE has it as | Notes | |---|---|---| | `starts_at`, `ends_at` (ISO datetime) | `startDate`, `endDate` | Naming diff; we send `starts_at`/`ends_at` in the mock already — confirm the FE form should rename or keep an adapter. | | `cover_url` (read) + `cover_media_id` (write) | `coverUrl` / `coverMediaId` | aligned in shape, just camelCase. | | `created_at` | `createdAt` | aligned. | | `is_active` | `isActive` | aligned. | | `description` | `description` | aligned. | **FE → B** (need decision): - `studentsCount`, `coursesCount` — computed UI counts. Backend response doesn't include them. Either compute client-side or ask backend for `?include=counts`. - `image` (UI alias of cover) — FE keeps this for a fallback. Backend response only has `cover_url`. --- ## Courses ### Endpoints | Backend | FE today | Status | |---|---|---| | `GET /courses?term_id&per_page` | `getCoursesList` | aligned (drop `/admin/` prefix in `endpoints.js`). | | `GET /courses/:id` | `showCourse` | aligned. Backend response includes nested `term` and `teacher` — FE already reads `course.term` / `course.teacher`, good. | | `POST /courses` | `addNewCourse` | aligned. Backend currently requires `term_id` (422 example), but per user instruction the standalone (template-tab) flow must allow `term_id: null`. **Decide:** ask backend to make `term_id` nullable, or refuse to submit until a term is picked. | | `PATCH /courses/:id` | `updateCourse` | aligned. Used now for status toggle too (drops dedicated `/status` endpoint). | | `DELETE /courses/:id` | `deleteCourse` | aligned. | | — | `changeStatusCourse` (`/admin/courses/:id/status`) | **Dropped.** Use `PATCH /courses/:id { is_active }`. | | — | `listCourseStudents`, `addCourseStudent`, `removeCourseStudent` | **FE-only.** CourseDetailsModal "students" tab + `AddCourseStudentModal`. Backend doesn't expose course-students; either add a sub-resource or remove the UI. Kept mock-only for now. | | — | `listCourseSessions`, `attachCourseSession`, `detachCourseSession` | **FE-only.** CourseDetailsModal "sessions" tab can be served by `GET /sessions?course_id=`. The attach/detach side (M:N) has no backend equivalent — backend uses session.course_id (1:N). Kept mock-only for now; recommend swapping the list tab to the regular sessions query. | ### Fields | Backend ↔ FE | Notes | |---|---| | `term_id` ⇄ `termId` | aligned. Nullable in CourseFormPage, required in AddOfferedCourseModal — schema reflects this. | | `teacher_id` ⇄ `teacherId` | aligned. | | `capacity` ⇄ `capacity` | aligned. | | `is_active` ⇄ `isActive` | aligned. | | `cover_media_id` (write) / `cover_url` (read) ⇄ `coverMediaId` / `coverUrl` | aligned. | | `description` ⇄ `description` | aligned. | | Nested `term`, `teacher` on show response | FE already reads. | **FE → B** (need decision): - `sessionsCount` — number-of-sessions field on the create form. Backend has nothing. Either compute server-side from related sessions, or drop the field. - `prerequisites` (array of { courseId, course }) — backend has no prerequisite relation. Drop or ask backend for it. - `contentType` (video/voice/text) + `contentMediaId` — single course-level content file. Backend treats files only as session media. Decide whether course-level content should move to "intro session" or stay a course concept. - `image` (UI alias of cover_url) — kept as a fallback alongside `coverUrl`. --- ## Sessions ### Endpoints | Backend | FE today | Status | |---|---|---| | `GET /sessions?course_id&per_page` | `getSessionsList` | aligned. | | `GET /sessions/:id` | `showSession` | aligned. | | `POST /sessions` | `addNewSession` | aligned. | | `PATCH /sessions/:id` | `updateSession` | aligned. Used for status toggle now. | | `DELETE /sessions/:id` | `deleteSession` | aligned. | | — | `changeStatusSession` (`/admin/sessions/:id/toggle-status`) | **Dropped.** Use PATCH with `is_active`. | | — | `getSessionsAttendance` (`/admin/sessions/:sessionId/attendances`) | **FE-only.** `SessionAttendanceModal` depends on this. Kept mock-only. | ### Fields — biggest gap of all five resources | Backend | FE today | Notes | |---|---|---| | `type` enum: `online \| offline \| content` | `sessionType` enum: `in_person`, `online`, `video`, `audio`, `text`, `slide`, `pdf` | **Shape diff.** FE has 7 values; backend has 3. There's an existing `SESSION_TYPE_TO_SPEC` mapper in `services/mock/fixtures/admin-sessions.js`. Decide whether the FE keeps the richer 7-value enum (and we map down to backend's 3) or collapses. | | `starts_at`, `location`, `link` | All three live **inside** `form.sessionConfig.*` plus also derived to top-level `startsAt` / `location` / `link` in the mock | **Structural diff.** Backend wants flat fields; FE form nests them under `sessionConfig` keyed by `sessionType`. The mock derives top-level from `sessionConfig.*` for show payloads. Decide whether the FE form should flatten the schema to match backend or keep the conditional-by-type config UI. | | `media_ids[]` (write) / `media[]` (read with `collection_name`, `file_name`, `mime_type`, `file_size`, `url`, `download_url`) | `materials[]` with `{ fileId, isRequired, type, title, order }` | **Shape diff.** Backend's media rows are typed by upload-purpose (video/voice/pdf/slide/attachment); FE has its own `type` enum. Decide which shape the FE keeps. | | — | `durationMinutes`, `order`, `sessionConfig.minWatchedPercent`, `sessionConfig.minReadPercent`, `sessionConfig.mustCompleteBeforeNext`, `sessionConfig.platform` | **FE → B**, all UI-only fields. Backend has nothing equivalent. Drop, move into a `metadata` JSON, or ask backend to add. | | — | `usedInTerms` | UI-only count, no backend. | | — | `image` (vs `media`) | UI-only thumbnail; backend doesn't separate. | --- ## Exams ### Endpoints | Backend | FE today | Status | |---|---|---| | `GET /exams/:id` (with questions+options) | `showExam` | aligned. | | `POST /exams` | `addNewExam` | aligned. | | `PATCH /exams/:id` | `updateExam` | aligned. | | `DELETE /exams/:id` | `deleteExam` | aligned. | | `POST /exams/:examId/questions` | — | **Backend → FE.** New endpoint. Today the FE submits the whole question list inside the exam create/update payload. Decide whether to keep "all-in-one" submission (and ask backend to accept it) or switch to add-questions-after-create. | | `POST /questions/:questionId/options` | — | **Backend → FE.** Same as above — backend lets you add options one at a time. FE today bundles all options with the question. | | `POST /exams/:examId/submit` | — (student-side feature) | **Backend → FE.** Student-side; not in current admin UI. | | — | `getExamsList` (`/admin/exams`) | **FE-only.** ExamsListPage depends on it. Kept mock-only; ask backend to add a list endpoint. | | — | `getExamParticipants`, `showExamParticipant` | **FE-only.** ExamParticipantsModal + ExamParticipantDetailsModal depend on these. Kept mock-only. | ### Fields | Backend ↔ FE | Notes | |---|---| | `session_id` ⇄ `sessionId` | aligned. | | `title` ⇄ `title` | aligned. | | `description` ⇄ `description` | aligned. | | `pass_score` ⇄ `passingScore` | aligned (naming diff). | | `is_active` | **Backend → FE.** Exam form has no active toggle. Decide whether to add it. | | Backend question shape: `{ question_text, position, options: [{ option_text, is_correct }] }` | FE: `{ title, score, correctAnswerId, answers: [{ id, title }] }` | **Shape diff.** Backend hides `is_correct` from public reads (only on add). FE concept of `score` (per-question weighting) has no backend slot. Decide: keep FE scoring (ask backend to store) or drop. | **FE → B** (need decision): - `durationMinutes` — no backend slot. - `randomize` — no backend slot. - `endDate` / `startDate` — exam validity window, no backend slot. - `usedInTerms` — derived UI count. --- ## Homeworks (FE calls them "assignments") ### Endpoints | Backend | FE today | Status | |---|---|---| | `POST /homeworks` | `addNewAssignment` | aligned (URL renamed to `/homeworks`; FE key name kept). | | `PATCH /homeworks/:id` | `updateAssignment` | aligned. | | `DELETE /homeworks/:id` | `deleteAssignment` | aligned. | | `POST /homeworks/:homeworkId/submit` (student) | — | **Backend → FE.** Student submit, not in admin UI yet. | | `PATCH /homework-submissions/:submissionId/review` | `reviewAssignmentSubmission` | aligned (URL renamed). | | — | `getAssignmentsList` | **FE-only.** AssignmentsListPage depends on it. Kept mock-only. | | — | `showAssignment`, `getAssignmentSubmissions`, `showAssignmentSubmission` | **FE-only.** Detail + submissions list — kept mock-only. | ### Fields | Backend ↔ FE | Notes | |---|---| | `session_id` ⇄ `sessionId` | aligned. | | `title`, `description` | aligned. | | `deadline` (single datetime) | FE: `startDate` + `endDate` + computed `durationDays` | **Shape diff.** Backend has one deadline; FE has a window. Decide: drop start/end and use single deadline, or ask backend to add a window. | | `is_active` | **Backend → FE.** FE form has no active toggle. | | Submission `status`: `accepted \| denied` | FE: `pending \| approved \| rejected \| needs_revision` | **Shape diff.** Backend has two states; FE has four. The FE `pending/needs_revision` have no backend slot. | | Submission `media_id` (single) | FE `attachments[]` (multiple) | **Shape diff.** Backend allows one file per submission; FE expects many. | | Submission `teacher_feedback` ⇄ `reviewerNote` | naming diff. | | Submission `reviewed_at` (read) | — | Backend provides; FE doesn't surface. | **FE → B** (need decision): - `priority` enum (`mandatory \| optional`) — no backend slot. - `submissionsCount` — derived count, no backend slot. --- ## Media ### Endpoints | Backend | FE today | Status | |---|---|---| | `POST /media` (multipart with `purpose` + `file`) | `uploadMedia` | aligned URL & method. | | `GET /media/:id/download` | — | **Backend → FE.** Add as `downloadMedia`. | | `DELETE /media/:id` | — | **Backend → FE.** Add as `deleteMedia`. | ### Fields - Backend response: `{ id, collection_name, file_name, mime_type, file_size, url, download_url }`. FE today reads `payload.id`, `payload.url`, sometimes `payload.uploadId` (a now-stale field). **Cleanup needed:** drop `uploadId`, use `id` everywhere. - Backend `purpose` enum: `avatar | cover | video | voice | pdf | slide | attachment | homework_file`. FE upload calls currently hardcode `purpose: 'cover'` or `purpose: 'content'`. **`content` is not in the backend enum.** Decide which of the backend purposes each FE uploader should send (e.g., session video → `video`, course PDF → `pdf`, homework upload → `homework_file`). - Pending media TTL: backend deletes unreferenced pending uploads after 24h. FE doesn't track this; if a user uploads a cover, abandons the form, and comes back next day, the `cover_media_id` reference will 404 on submit. Document the failure mode. --- ## Cross-cutting ### Snake_case vs camelCase Backend wire format is snake_case throughout. FE today reads camelCase (e.g., the mock returns `coverUrl`, `isActive`). When the real backend lands, the FE will either need: - a HTTP-layer transformer (camelize on response, snake_case on request), or - camelCase field aliases on the backend serializer. Decide before swapping the mock for the real API. Affects every screen. ### `/admin/` URL prefix Backend doc has none — endpoints live at `/terms`, `/courses`, etc. The FE previously had `/admin/courses`, `/admin/sessions`, `/admin/exams`, `/admin/assignments`. **Aligned to backend (prefix dropped).** Auth context (admin role) is implicit in the token, not the URL. ### Sub-features that depend on missing backend endpoints UI screens that work today against the mock but have no backend equivalent in the current doc (kept mock-only with TODOs in `endpoints.js`): - Term: clone term, term status toggle, students-in-term subtab, leave toggle, courses-in-term subtab (could swap to `GET /courses?term_id=`) - Course: status toggle (swapped to PATCH), students-in-course subtab, attach/detach sessions (M:N), `AddCourseStudentModal`, `AddSessionToCourseModal` - Session: status toggle (swapped to PATCH), attendance roster - Exam: list page, participants list, participant detail - Assignment/Homework: list page, detail show, submissions list, submission detail For each of these we need to either (a) ask backend to expose the endpoint, or (b) drop the UI surface.