12 — API Mapping (Academics Module)
- 0. Module-wide request envelope & client policy
- 1. Academic Years —
academic-year.controller.ts - 2. Grades —
grade.controller.ts - 3. Sections —
section.controller.ts - 4. Classes —
class.controller.ts - 5. Subjects —
subject.controller.ts - 6. Subject Assignments —
subject-assignment.controller.ts - 7. Loading / streaming / realtime
- 8. Client-side error mapping table (module)
- 9. Pagination & sort (per list)
- 10. Optimistic / undo
Exact wire contract for every screen → endpoint. Base
/api/v1; envelope per 00-shared/07 §2-§3. All endpoints fromsrc/modules/academics/controllers/*; business rules fromservices/*; field contracts fromdto/*; defaults/enums fromschemas/*. Guard today:JwtAuthGuardonly (academics.module.ts:46, every controller@UseGuards(JwtAuthGuard)); RBAC(planned)— OQ-1.
0. Module-wide request envelope & client policy
| Aspect | Contract |
|---|---|
| Base | https://api.<domain>/api/v1 |
| Headers | Authorization: Bearer <accessToken>; x-request-id client-generated; Content-Type: application/json |
| Success | {success:true, message:"OK", data, meta?, timestamp, requestId} |
| Error | {success:false, message, error:{code, details?}, timestamp, requestId} |
| Tenancy | tenantId from JWT claim only — never in body/query (base.repository.ts:20-30) |
| IDs | _id, createdAt, updatedAt, __v returned raw (Mongoose docs, no whitelisting) — client maps explicitly (base.schema.ts:7-35) |
| Pagination | page (≥1, default 1), limit (1–100, default 20), sort (-field = desc), q (parsed, unused by services — OQ-7); meta {page,limit,totalItems,totalPages,hasNext,hasPrevious} (pagination-query.dto.ts:5-30,32-55) |
| Retry | backoff on 5xx/network; no auto-retry on 429 (00-shared/07 §4) |
| Offline | writes blocked + banner; reads from 24 h cache (13 §3) |
1. Academic Years — academic-year.controller.ts
| Method & Path | Body / Params | Success data | Rules & Errors |
|---|---|---|---|
POST /academic-years (:24) | {name*, startDate*, endDate*, status?} | year doc | 409 DUPLICATE_RESOURCE "Academic year "X" already exists." (academic-year.service.ts:25-29); status omitted → upcoming (:30-33) |
GET /academic-years (:27) | page,limit,sort,q | array + meta | default sort=createdAt:-1 (:45-47); sort maps - prefix → desc (:45-47) |
GET /academic-years/:id (:30) | — | year doc | 404 "Academic year not found." (:36-40) |
PATCH /academic-years/:id (:33) | any of {name?, startDate?, endDate?, status?, isCurrent?} | year doc | partial $set (:59-67); 404 if missing; no duplicate-name check on update (OQ-2) |
PATCH /academic-years/:id/set-current (:39) | — | year doc (isCurrent:true, status:"active") | un-sets isCurrent on all others (:69-83); 404 if missing |
DELETE /academic-years/:id (:42) | — | {message:"OK"} | soft delete (:85-88); 404 if missing; no cascade (OQ-3) |
DTO sources: create-academic-year.dto.ts:4-21 (name IsString, startDate/endDate
IsDateString ISO YYYY-MM-DD, status IsString optional); update-academic-year.dto.ts:4-28
(all optional incl. isCurrent bool).
Client notes: isCurrent editable via PATCH body but the UI uses set-current
(which also forces status:"active", academic-year.service.ts:78-80); treat
isCurrent:true as a tenant-wide singleton (blueprint rule, COLLECTIONS.md:1023).
2. Grades — grade.controller.ts
| Method & Path | Body / Params | Success data | Rules & Errors |
|---|---|---|---|
POST /grades (:24) | {name*, academicYearId?, code?, displayOrder?} | grade doc | 409 DUPLICATE_RESOURCE "Grade "X" already exists." (grade.service.ts:21-23); displayOrder default 0 |
GET /grades (:27) | page,limit,sort,q | array + meta | default sort=displayOrder:1 (:37-39) |
GET /grades/:id (:30) | — | grade doc | 404 "Grade not found." (:28-32) |
PATCH /grades/:id (:33) | any of {academicYearId?, name?, code?, displayOrder?, status?} | grade doc | partial $set (:51-56); no rename duplicate check (OQ-2) |
DELETE /grades/:id (:36) | — | {message:"OK"} | soft delete (:58-61); no cascade — sections/classes orphaned (OQ-3) |
DTO sources: create-grade.dto.ts:4-24 (name IsString; academicYearId IsMongoId
optional — schema optional grade.schema.ts:9-10; code IsString; displayOrder
IsInt Min(0) default 0); update-grade.dto.ts:4-30 (adds status IsString).
3. Sections — section.controller.ts
| Method & Path | Body / Params | Success data | Rules & Errors |
|---|---|---|---|
POST /sections (:24) | {gradeId*, name*, capacity?, classTeacherId?, roomId?} | section doc | no duplicate guard (OQ-4) (section.service.ts:16-18); capacity default 40 |
GET /sections (:27) | page,limit,sort,q | array + meta | insertion order (:41-48) |
GET /sections/by-grade/:gradeId (:30) | page,limit | array + meta | scoped {gradeId} (:26-39) |
GET /sections/:id (:36) | — | section doc | 404 "Section not found." (:20-24) |
PATCH /sections/:id (:39) | any of {gradeId?, name?, capacity?, classTeacherId?, roomId?, status?} | section doc | partial $set; reparenting orphans classes (class.schema.ts:18-19 required) — client confirm (OQ-3) |
DELETE /sections/:id (:42) | — | {message:"OK"} | soft delete (:62-65); no cascade |
DTO sources: create-section.dto.ts:4-28 (gradeId IsMongoId; name IsString e.g. "A";
capacity IsInt Min(1) default 40; classTeacherId IsMongoId optional — user ID;
roomId IsString optional); update-section.dto.ts:4-35 (adds status).
4. Classes — class.controller.ts
| Method & Path | Body / Params | Success data | Rules & Errors |
|---|---|---|---|
POST /classes (:24) | {academicYearId*, gradeId*, sectionId*, name*, capacity?, campusId?, classTeacherId?, roomId?} | class doc | no duplicate guard (OQ-4) (class.service.ts:16-18); capacity default 40 |
GET /classes (:27) | page,limit,sort,q | array + meta | insertion order (:41-53) |
GET /classes/by-year/:academicYearId (:30) | page,limit | array + meta | scoped {academicYearId} (:26-39) |
GET /classes/:id (:36) | — | class doc | 404 "Class not found." (:20-24) |
PATCH /classes/:id (:39) | any of {academicYearId?, campusId?, gradeId?, sectionId?, name?, capacity?, classTeacherId?, roomId?, status?} | class doc | partial $set (:55-60) |
DELETE /classes/:id (:42) | — | {message:"OK"} | soft delete (:62-65); downstream refs (timetable/attendance/assignments) remain (OQ-3) |
DTO sources: create-class.dto.ts:4-41 (academicYearId,gradeId,sectionId IsMongoId
required; name IsString e.g. "Grade 10 - A"; campusId IsMongoId optional — ref
points at Class schema, likely Campus typo, class.schema.ts:12-13, OQ-6;
classTeacherId IsMongoId optional user ID; roomId IsString optional free text);
update-class.dto.ts:4-50 (adds status).
5. Subjects — subject.controller.ts
| Method & Path | Body / Params | Success data | Rules & Errors |
|---|---|---|---|
POST /subjects (:24) | {code*, name*, shortName?, credits?, maximumMarks?, passingMarks?, theoryMarks?, practicalMarks?} | subject doc | 409 "Subject with code "X" already exists." (subject.service.ts:21-25) + DB unique index fallback (subject.schema.ts:38); defaults 0/100/33/80/20 |
GET /subjects (:27) | page,limit,sort,q | array + meta | insertion order (:35-47) |
GET /subjects/:id (:30) | — | subject doc | 404 "Subject not found." (:29-33) |
PATCH /subjects/:id (:33) | any of {code?, name?, shortName?, credits?, maximumMarks?, passingMarks?, status?} | subject doc | partial $set (:49-54); theoryMarks/practicalMarks NOT updatable (update-subject.dto.ts:4-42, OQ-5); duplicate code on update → unique index error → 409-mapped by filter (http-exception.filter.ts:32, OQ-2) |
DELETE /subjects/:id (:36) | — | {message:"OK"} | soft delete (:56-59); assignments referencing it stay (OQ-3) |
DTO sources: create-subject.dto.ts:4-47 (code,name IsString; shortName IsString;
credits IsInt Min(0) default 0; maximumMarks IsInt Min(1) default 100;
passingMarks IsInt Min(1) default 33; theoryMarks IsInt Min(0) default 80;
practicalMarks IsInt Min(0) default 20); update-subject.dto.ts:4-42 as above.
No cross-field validation (theory+practical ≤ max, passing ≤ max) server-side (OQ-5).
6. Subject Assignments — subject-assignment.controller.ts
| Method & Path | Body / Params | Success data | Rules & Errors |
|---|---|---|---|
POST /subject-assignments (:21) | {teacherId*, subjectId*, classId*, academicYearId*} (all IsMongoId) | assignment doc | no duplicate/conflict check (OQ-4) (subject-assignment.service.ts:13-17) |
GET /subject-assignments/by-class/:classId (:24) | ?academicYearId= | bare array (no meta) | {classId, academicYearId} (:19-24) |
GET /subject-assignments/by-teacher/:teacherId (:30) | ?academicYearId= | bare array (no meta) | {teacherId, academicYearId} (:26-31) |
DELETE /subject-assignments/:id (:36) | — | {message:"OK"} | soft delete; 404 "Assignment not found." (:33-36) |
No GET /:id and no update endpoint — remove + recreate is the only correction
path (see 08 §11 "Replace" flow). Assignment tuples are not unique per
(classId, subjectId) (subject-assignment.schema.ts:22-25 indexes are non-unique).
7. Loading / streaming / realtime
| Screen | Loading | Streaming | Realtime |
|---|---|---|---|
| All lists | AppSkeleton rows ≤ 200 ms, infinite-scroll footer | — | — |
| Detail | header skeleton + section skeletons | — | — |
| Explorer | per-level skeletons as children load | — | — |
| Any | — | — | (planned) WS push on set-current/structure change (00-shared/07 §8) — until then refresh-on-focus |
8. Client-side error mapping table (module)
| Screen | code | UI |
|---|---|---|
| Any create/update | 400 VALIDATION_ERROR | per-field errorText from error.details[] (keys = DTO field names); focus first invalid |
| Any | 401 UNAUTHENTICATED | silent refresh → replay once → sessionExpired |
| Any write | 403 PERMISSION_DENIED | hidden CTAs + guarded routes (planned) — OQ-1 |
| Year/Grade create | 409 DUPLICATE_RESOURCE | inline AppBanner(error) with server message verbatim |
| Subject create/update code | 409 | "Code already in use" + open existing row |
| Class/Section/Assignment | (server never 409s) | client duplicate guard only (OQ-4) |
| Delete / set-current | 404 RESOURCE_NOT_FOUND | treat as already-removed → refresh; snackbar |
| Any | 429 RATE_LIMITED | countdown banner; no auto-retry |
| Any | 422 BUSINESS_RULE_VIOLATION | (planned) — no service throws it today; map as generic business error |
| Any | 5xx INTERNAL_SERVER_ERROR | generic + requestId; never server internals |
9. Pagination & sort (per list)
| List | Default sort | Server filter support | q |
|---|---|---|---|
| Years | createdAt:-1 (academic-year.service.ts:45-47) | — | unused (OQ-7) |
| Grades | displayOrder:1 (grade.service.ts:37-39) | — | unused |
| Sections | insertion (section.service.ts:41-48) | by-grade/:gradeId | unused |
| Classes | insertion (class.service.ts:41-53) | by-year/:academicYearId | unused |
| Subjects | insertion (subject.service.ts:35-47) | — | unused |
| Assignments | insertion (bare array, subject-assignment.service.ts:19-31) | by-class, by-teacher (+ academicYearId) | n/a |
Client policy: preserve server order always; pagination via meta{hasNext} infinite
scroll; by-class/by-teacher render all rows (no meta).
10. Optimistic / undo
- No optimistic writes anywhere: creates/updates/deletes/set-current are server-first; response doc is the canonical state (00-shared/07 §9).
- Exceptions: filter chips + year switcher are local UI state (optimistic by nature, re-scope queries); client duplicate pre-checks are advisory only.
- Delete/remove confirmations are destructive and never undoable (soft delete —
restoration
(planned)).