Exact wire contract for every screen → endpoint. Base /api/v1. Envelope per
00-shared/07 §2-3 . Endpoints from student.controller.ts, parent.controller.ts,
bulk.controller.ts, users.controller.ts; business rules from the services
cited. All endpoints behind JwtAuthGuard (student.controller.ts:32-35);
tenantId from JWT only — never in the body. RBAC enforcement (planned)
(OQ-4); client still gates UI on student.*.
Aspect Contract
Base https://api.<domain>/api/v1
Headers Authorization: Bearer <accessToken>; x-request-id (client UUID); Content-Type: application/json (multipart/form-data for uploads)
Success {success:true, message:"OK", data, meta?, timestamp, requestId} — paginated: data array + meta{page,limit,totalItems,totalPages,hasNext,hasPrevious} (pagination-query.dto.ts:32-39)
Error {success:false, message, error:{code, details?}, timestamp, requestId}
Codes 400 VALIDATION_ERROR · 401 UNAUTHENTICATED · 403 PERMISSION_DENIED · 404 RESOURCE_NOT_FOUND · 409 DUPLICATE_RESOURCE · 422 BUSINESS_RULE_VIOLATION · 429 RATE_LIMITED · 5xx INTERNAL_SERVER_ERROR
Pagination page ≥ 1 (default 1), limit 1–100 (default 20), sort (- = desc), q — students find ignores sort/q (OQ-2)
Caching client last-good + stale-while-revalidate (00-shared/06 §3.3 ); TTLs: roster 5 min (proposed), detail no client cache
Offline reads cached; all writes blocked (no offline queue defined for students)
Retry backoff on 5xx/network; no auto-retry on 429
Endpoint GET /students (student.controller.ts:41-43)
Query page, limit, sort?, q? (parsed; only page/limit applied — student.service.ts:104-108)
Response 200 data:[StudentDoc…], meta (page/limit/totalItems/totalPages/hasNext/hasPrevious)
StudentDoc shape _id, tenantId, userId, admissionNumber, rollNumber?, academicYearId, campusId?, gradeId, sectionId, classId, houseId?, admissionDate?, admissionType?, status, transportRequired, hostelRequired, medicalNotes?, metadata?, createdAt, updatedAt, version, isDeleted:false (student.schema.ts:15-64 + base.schema.ts)
Errors 401; 429; 5xx
Client infinite scroll on meta.hasNext; client-side filters/sort (server ignores q/sort — OQ-2)
GET /students/:id (student.controller.ts:44-46) → 200 data:StudentDoc;
404 RESOURCE_NOT_FOUND "Student not found." (student.service.ts:95-99).
Endpoint POST /students (student.controller.ts:38-40)
Body CreateStudentDto — see 08 §1 (userId*, admissionNumber*, academicYearId*, gradeId*, sectionId*, classId*; rest optional)
Response 200 data:StudentDoc with status:"active" (student.service.ts:65) + auto-enrolled ACTIVE class_enrollments row (student.service.ts:71-78)
Errors 400 validation; 409 DUPLICATE_RESOURCE "Admission number "x" already exists." (student.service.ts:56-62); 429; 5xx
Events StudentCreated {studentId, admissionNumber, classId} → in-app/student-enrolled (event-queue-map.ts:28); audit via event bus
PATCH /students/:id (student.controller.ts:56-58) — body UpdateStudentDto
(all optional; status is unvalidated string — UI sends enum values only).
200 data:StudentDoc; 404; 409 n/a; emits StudentUpdated {changes:[field…]}
→ audit-write/log-student-updated (event-queue-map.ts:29). Note version
increments on every write (base.repository.ts:57-66).
DELETE /students/:id (student.controller.ts:59-61) → 200 (data empty);
404; emits StudentDeleted → audit-write/log-student-deleted
(event-queue-map.ts:30). Soft delete: isDeleted:true, deletedAt, deletedBy
(base.repository.ts:68-74) — excluded from all queries. No cascade to
documents/enrollments/links (OQ-7). No restore endpoint for soft-deleted.
Endpoint POST /students/:id/enroll (student.controller.ts:50-55)
Body EnrollStudentDto {classId*, academicYearId*, rollNumber?}
Behaviour deactivates all ACTIVE enrollments (status:"transferred", leftAt:now) then creates new ACTIVE (student.service.ts:125-140)
Response 200 data:ClassEnrollmentDoc {studentId, classId, academicYearId, rollNumber?, joinedAt, leftAt?, status:"active"} (class-enrollment.schema.ts:14-39)
Errors 404 (student); 400; 429; 5xx
Endpoint POST /students/:id/transfer (student.controller.ts:62-65)
Body TransferStudentDto {classId*, academicYearId*, gradeId?, sectionId?, rollNumber?}
Behaviour guard status === "active" else 409 Cannot transfer a student with status "x". (student.service.ts:175-179) → enroll (same deactivation) → $set {classId, academicYearId, gradeId?, sectionId?} (student.service.ts:180-192)
Response 200 data:StudentDoc (updated)
Errors 404; 409 (wrong status); 400; 429; 5xx
Events StudentUpdated {changes:["transfer"]} → audit
Action Endpoint Guard Success → data Source
Graduate POST /students/:id/graduate409 "Student is already graduated." status:"graduated"student.controller.ts:66-69, student.service.ts:205-223
Archive POST /students/:id/archive409 "Student is already archived." status:"archived"student.controller.ts:70-73
Restore POST /students/:id/restore409 "Student is already active." status:"active"student.controller.ts:74-77
All: 404; 200 data:StudentDoc; emit StudentUpdated {changes:["graduate"|"archive"|"restore"]}.
GET /students/:id/enrollments (student.controller.ts:47-49) → 200
data:[ClassEnrollmentDoc…] active only (findActiveByStudent —
class-enrollment.repository.ts:20-24); 404.
GET /students/:id/academic-history (student.controller.ts:91-94) → 200
data:[ClassEnrollmentDoc…] all enrollments sorted joinedAt desc
(student.service.ts:281-287); 404.
List GET /students/:id/documents (student.controller.ts:78-81) → 200 data:[StudentDocumentDoc…] sorted createdAt desc (student.service.ts:273-279) — shape {studentId, fileName, mimeType, size, fileId, category?, uploadedBy?, createdAt} (student-document.schema.ts:8-29)
Upload POST /students/:id/documents (student.controller.ts:82-90) — multipart/form-data, file field file , text field category?; 200 data:StudentDocumentDoc; no server size/type validation (OQ-9)
Download none — only fileId metadata; local provider path /api/v1/files/<tenantId>/<uuid>--<name> (local-storage.provider.ts:47-51) — preview/download (planned)
Import POST /bulk/import/:entity where entity="students" (bulk.controller.ts:35-48; only adapter — bulk-import.service.ts:17-20) — multipart field file ; missing → 400 "CSV file is required (multipart field "file")."; 200 data:ImportReport {entity, totalRows, imported, failed, errors:[{rowNumber, errors[]}]} (import-adapter.interface.ts:14-25)
Parse errors 400 "Malformed CSV: could not parse file." / "CSV must include a header row and data." (bulk-import.service.ts:31-35)
Unknown entity 404 No import adapter for entity "x".
Export GET /bulk/export/:entity (bulk.controller.ts:50-60) → 200 text/csv, Content-Disposition: attachment; filename="students.csv" — columns admissionNumber, rollNumber, status, admissionDate sorted by admissionNumber (students-import.adapter.ts:87-98)
Per-row errors (exact strings) in 08 §6 ; row numbers are index+2 (header =
row 1) (bulk-import.service.ts:45-46). Synchronous; no job/progress endpoint
(OQ-12).
List links GET /parents/link/student/:studentId (parent.controller.ts:53-57) → 200 data:[StudentParentLinkDoc…] {studentId, parentId, relationship, isPrimaryGuardian, financialResponsibility, pickupAllowed, emergencyPriority, metadata?} (student-parent-link.schema.ts:17-41)
Create parent POST /parents (parent.controller.ts:29-31) — CreateParentDto; 409 "Parent profile already exists for this user." (parent.service.ts:30-34)
Link POST /parents/link/:studentId (parent.controller.ts:47-52) — LinkParentDto (relationship* required); 404 unknown student (student-parent-link.service.ts:27); parent existence not verified (OQ-11)
Unlink DELETE /parents/link/:linkId (parent.controller.ts:58-60) — soft delete; 404 "Link not found." (student-parent-link.service.ts:38-41)
Parent's children GET /parents/:id/students (parent.controller.ts:38-40) → link docs for that parent
Update link none — edit = unlink + re-link
Purpose Endpoint Source
Academic years GET /academic-years (page/limit)academics/controllers/academic-year.controller.ts:27-29
Grades GET /gradesgrade.controller.ts:27-29
Sections by grade GET /sections/by-grade/:gradeIdsection.controller.ts:30-33
Classes GET /classes ; GET /classes/by-year/:academicYearIdclass.controller.ts:27-33
Create user (identity step) POST /usersusers.controller.ts:36-40
Search users (reuse) GET /users?q= (server-side q handling per users module)users.controller.ts:42-46
Avatar POST /users/:id/avatar (multipart file)users.controller.ts:95-102
Screen Code UI
any 401 silent refresh → sessionExpired (00-shared/06 §3.6 )
any action 403 hide action / 403 screen (server RBAC (planned))
create 409 admission inline field "already exists"
create (users) 409 email step-1 inline + offer reuse
transfer 409 status read-only status banner
graduate/archive/restore 409 same state snackbar + refresh
documents upload 400/5xx tile error + retry
import 400 parse wizard error banner with message
import 404 entity generic (should not happen)
list/detail 404 empty-state "not found"
any 429 countdown, CTA disabled
any 5xx generic + requestId (00-shared/06 §5 )
Never optimistic on: create, enroll, transfer, graduate, archive, restore,
delete, link/unlink, document upload, import (all server-truth, side effects).
Row removal (unlink, delete) reflects only after server 200.
Undo: not offered (no restore endpoint for soft-delete; archive has explicit
Restore action instead).