Skoolsewa - School Management System Docs
Developer ResourcesSubjects

Subjects API Reference

Complete API contracts for the subjects module, including routes, auth, DTOs, responses, errors, examples, and integration notes.

Subjects - API Reference

Audience: Frontend engineers, mobile engineers, backend engineers, QA, and API consumers. Scope: Admin routes for the subject catalogue and per-class subject assignments, plus the two audience-scoped consumer reads a pupil and a teacher use for their own subjects.

1. Documentation Evidence

AreaFiles InspectedWhat Was Verified
Controllersapps/api/src/modules/academic-structure/subjects/subjects.controller.ts, apps/api/src/modules/academic-structure/class-subjects/class-subjects.controller.ts, apps/api/src/modules/mobile/student/student-portal.controller.ts, apps/api/src/modules/mobile/teacher/teacher-portal.controller.tsRoutes, methods, guards, decorators, status codes, swagger tags.
DTOsapps/api/src/modules/academic-structure/dto/subject.dto.ts, apps/api/src/modules/academic-structure/dto/class-subject.dto.tsRequest, query, response, validation, examples.
Servicessubjects-read.service.ts, subjects-write.service.ts, class-subjects-read.service.ts, class-subjects-write.service.ts, student-portal.service.ts, teacher-portal.service.tsBehavior, side effects, response mapping, errors.
Schemapackages/db/src/schema/school/subjects.tsIDs, enums, persisted fields, constraints.
Error registryapps/api/src/common/types/error-codes.tsEvery SUBJECT_* and CLASS_SUBJECT_* code, plus the shared codes this surface throws.
Cacheapps/api/src/common/utils/cache-key.util.ts, apps/api/src/services/redis/redis.service.tsKey pattern, TTL, invalidation.
Module wiringapps/api/src/modules/academic-structure/academic-structure.module.ts, apps/api/src/modules/mobile/mobile.module.ts, apps/api/src/config/swagger/swagger-documents.tsWhich OpenAPI document each route lands in, and why.

2. Module Summary

FieldValue
Module nameSubjects (catalogue) and ClassSubjects (assignment)
Module slugsubjects
Primary actorssuperadmin (every write), staff/teacher (read-only on both), student (catalogue read-only, plus their own consumer route), guardian (none)
API surfacesAdmin (/api/subjects, /api/subjects/:publicId/assignments, /api/classes/:classPublicId/subjects, /api/class-subjects/:id), Mobile/consumer (/api/mobile/student/subjects, /api/mobile/teacher/subjects)
Base route prefixes/api/subjects, /api/classes (one nested route only), /api/class-subjects, /api/mobile/student, /api/mobile/teacher
Auth modelJwtAuthGuard + RoleGuard on every route. Admin routes additionally require a specific @Permissions() code; the two consumer routes carry no permission code and are gated entirely inside the service by an audience assertion.
PersistencePostgreSQL only (subjects, class_subjects tables); Redis for the catalogue list cache only. No MongoDB, no BullMQ, no realtime, no external API.
Runtime source of truthThe two Postgres tables; academic_sessions.is_current for "the current session" on the two consumer reads.
Sibling docsBackend, Features and flows

3. Concepts and Terminology

TermMeaningSource FileUsed By
SubjectA reusable, school-wide entity: "Mathematics", once. Carries a default type and default mark scheme.subjects.tsSubjectDto, SubjectOptionDto
Class subject assignmentOne class teaching one subject: its own teacher, its own EFFECTIVE type and mark scheme, copied from the subject at assignment time.subjects.ts (classSubjects)ClassSubjectDto
publicIdThe subject's externally-addressable UUID. The integer primary key never leaves the process.subjects.tsEvery subject route param and response field.
id (on an assignment)class_subjects.id is itself a UUIDv7 primary key AND the public identifier — there is no separate publicId on this table.subjects.tsclass-subjects/:id route param, ClassSubjectDto.id.
versionA server-owned optimistic-concurrency counter, bumped by a database trigger on every UPDATE. Required on every PATCH; a stale value is a 409.subjects.ts (bump_row_version trigger)UpdateSubjectDto.version, UpdateClassSubjectDto.version, AssignSubjectItemDto.version.
classCountA live count of a subject's ACTIVE assignments, computed per read as a correlated subquery, never stored.subjects-read.service.tsSubjectDto.classCount.
Suspended (assignment)class_subjects.is_active = false. Configuration is preserved; the class is simply not currently taught this subject through this row.subjects.ts docblockClassSubjectDto.isActive, AssignmentResultDto.skippedSuspended.
Archived (subject)subjects.is_active = false. Retirement, not deletion — the name/code become reusable by a new subject.subjects.ts docblockSubjectDto.isActive.
Label collisionTwo subject rows (one archived, one active, sharing a retired-then-reused name or code) both assigned to one class.class-subjects-write.service.tsCLASS_SUBJECT_DUPLICATE_LABEL.
Portal audienceThe scopeKind a session's active role carries — student, guardian, all, or self — asserted on the two consumer routes before any query runs.mobile/shared/portal-audience.tsassertPortalAudience, assertTeacherStaffRow.

4. API Surface Map

SurfaceMethodPathActorAuth/GuardPermissionControllerPurpose
AdminGET/api/subjectsStaff/Teacher/SuperadminJwtAuthGuard, RoleGuardSubjects_READSubjectsControllerList/search the subject catalogue.
AdminGET/api/subjects/optionsStaff/Teacher/SuperadminJwtAuthGuard, RoleGuardSubjects_READSubjectsControllerUnpaginated options for assignment pickers.
AdminGET/api/subjects/:publicIdStaff/Teacher/SuperadminJwtAuthGuard, RoleGuardSubjects_READSubjectsControllerRead one subject.
AdminPOST/api/subjectsSuperadminJwtAuthGuard, RoleGuardSubjects_CREATESubjectsControllerCreate a subject.
AdminPATCH/api/subjects/:publicIdSuperadminJwtAuthGuard, RoleGuardSubjects_UPDATESubjectsControllerUpdate, retire, or reactivate a subject.
AdminDELETE/api/subjects/:publicIdSuperadminJwtAuthGuard, RoleGuardSubjects_DELETESubjectsControllerHard-delete a subject with zero assignments.
AdminGET/api/subjects/:publicId/assignmentsStaff/Teacher/SuperadminJwtAuthGuard, RoleGuardClassSubjects_READClassSubjectsControllerList a subject's class assignments.
AdminPOST/api/subjects/:publicId/assignmentsSuperadminJwtAuthGuard, RoleGuardClassSubjects_CREATEClassSubjectsControllerAssign a subject to one or more classes (batch).
AdminGET/api/classes/:classPublicId/subjectsStaff/Teacher/SuperadminJwtAuthGuard, RoleGuardClassSubjects_READClassSubjectsControllerList a class's assigned subjects.
AdminPATCH/api/class-subjects/:idSuperadminJwtAuthGuard, RoleGuardClassSubjects_UPDATEClassSubjectsControllerUpdate/suspend/revive one assignment.
AdminDELETE/api/class-subjects/:idSuperadminJwtAuthGuard, RoleGuardClassSubjects_DELETEClassSubjectsControllerHard-delete one assignment.
MobileGET/api/mobile/student/subjectsPupilJwtAuthGuard, RoleGuardNone (audience-asserted in-service)StudentSubjectsPortalControllerA pupil's own subjects for their currently-enrolled class.
MobileGET/api/mobile/teacher/subjectsTeacherJwtAuthGuard, RoleGuardNone (audience-asserted in-service)TeacherSubjectsPortalControllerA teacher's own teaching load.

Rules applied: GET /api/subjects/options is declared ahead of GET /api/subjects/:publicId in the controller, since Nest matches in declaration order and the reverse would swallow the literal segment as a publicId value. ClassSubjectsController carries no base @Controller() path — every route spells its own full path, because the three path families it serves (subjects/:publicId/assignments, classes/:classPublicId/subjects, class-subjects/:id) cannot share one class-level tag under the swagger gate's "no tag shared by two controllers" rule, and would otherwise need three separate controller classes. StudentSubjectsPortalController/TeacherSubjectsPortalController are each a SECOND controller class sharing their sibling's @Controller("student")/@Controller("teacher") path, purely so the "My Subjects" operation gets its own swagger tag rather than concatenating with "My Enrolments"/"My Classes".

Swagger document membership: every admin route above ships in the admin document at /api/api-docs because AcademicStructureModule — the module that DECLARES SubjectsController and ClassSubjectsController — is named in ADMIN_MODULES. Both consumer routes ship in the mobile document at /api/mobile-docs because StudentPortalModule/TeacherPortalModule are named as concrete leaves in MOBILE_CHILDREN.

5. Auth, Identity, and Permissions

SurfaceGuard/DecoratorIdentity ShapePermissionGuest AllowedNotes
Admin read (Subjects_READ)JwtAuthGuard, RoleGuard, @Permissions("Subjects_READ")req.user, req.user.activeRoleSubjects_READNoHeld by superadmin, staff, teacher, student.
Admin write (Subjects_CREATE/_UPDATE/_DELETE)Same guards, @Permissions("Subjects_CREATE") etc.SameThe specific action codeNoHeld ONLY by superadmin in the seed — no admin role is seeded in this database; a custom role must be built and granted explicitly.
Admin read (ClassSubjects_READ)Same guards, @Permissions("ClassSubjects_READ")Same, plus PeopleAccessService.scopeFor row scopeClassSubjects_READNoHeld by superadmin, staff, teacher. Deliberately NOT student or guardian — see 6.x ClassTeacherDto.
Admin write (ClassSubjects_CREATE/_UPDATE/_DELETE)Same guards, specific action codeSameThe specific action codeNoSuperadmin only in the seed, same reasoning as the subject writes.
Mobile consumer (GET /api/mobile/student/subjects)JwtAuthGuard, RoleGuard; NO @Permissions()req.user, asserted activeRole.scopeKind === "student"NoneNoSits on NO_PERMISSION_ADMIN_HANDLERS; the actual gate is assertPortalAudience inside StudentPortalService.listMySubjects.
Mobile consumer (GET /api/mobile/teacher/subjects)Samereq.user, resolved live staff.id via assertTeacherStaffRowNoneNoRefuses any session not acting as the role named teacher (guardian, student, and a teacher acting as staff included) and a session with no live staff row (e.g. a dismissed teacher who kept another profile).

Whether headers are parsed but not trusted: N/A — no header beyond the standard Authorization: Bearer is consulted anywhere in this module.

Row scope for ClassSubjects_READ (both admin list routes): PeopleAccessService.scopeFor(actor, "classSubjects", heldPermissions) resolves to {kind: "all"} only for scopeKind: "all" roles that hold the permission (staff, teacher) or for superadmin; every other scopeKind — including student and guardian, who never hold the permission and so never reach this method through RoleGuard in the first place — resolves to a predicate of sql\false`(no rows), neverall`.

6. DTO and Model Reference

6.1 SubjectDto (response)

FieldTypeRequiredDefaultValidationExampleSource
publicIdstring (uuid)YesN/AN/A"a1b2..."subject.dto.ts
namestringYesN/AN/A"Mathematics"same
codestringYesN/AN/A"MATH101"same
type"compulsory" | "optional"YesN/AN/A"compulsory"same
defaultMaxMarksnumberYesN/AN/A100same
defaultPassMarksnumberYesN/AN/A40same
isActivebooleanYesN/AN/Atruesame
classCountnumberYesN/AN/A, live-computed6same
versionnumberYesN/AN/A3same
createdAtstring (ISO date)YesN/AN/A—same
updatedAtstring (ISO date)YesN/AN/A—same

6.2 SubjectOptionDto (response, nested)

FieldTypeRequiredDefaultValidationExampleSource
publicIdstringYesN/AN/A—subject.dto.ts
namestringYesN/AN/A"Mathematics"same
codestringYesN/AN/A"MATH101"same
typeSubjectTypeYesN/AN/A"compulsory"same
defaultMaxMarksnumberYesN/AN/A100same
defaultPassMarksnumberYesN/AN/A40same

No person data at all — the field that makes GET /api/subjects/options safe to read unpaginated, and the exact shape embedded in ClassSubjectDto.subject.

6.3 SubjectOptionsPayloadDto (response envelope for options)

FieldTypeRequiredDefaultValidationExampleSource
itemsSubjectOptionDto[]YesN/AN/A—subject.dto.ts
truncatedbooleanYesN/AN/Afalsesame

The rows live under items, not directly under data, specifically so truncated has somewhere to live — ResponseDto is a closed shape whose constructor only accepts pagination metadata, and this endpoint is unpaginated by definition.

6.4 CreateSubjectDto (body)

FieldTypeRequiredDefaultValidationExampleSource
namestringYesN/ANon-empty, max 128 chars, no surrounding whitespace (/^\S(.*\S)?$/), no control characters"Mathematics"subject.dto.ts
codestringYesN/A/^[A-Z0-9_]{1,16}$/"MATH101"same
type"compulsory" | "optional"YesN/AIsIn(SUBJECT_TYPES)"compulsory"same
defaultMaxMarksnumberNo100 (service-side)Int, 1-1000100same
defaultPassMarksnumberNo40 (service-side)Int, 0-100040same
isActivebooleanNotrueIsBooleantruesame

passMarks <= maxMarks is NOT expressible as a decorator — no class-validator rule compares two fields of one object — so it is enforced in the service against the fully-known values before the insert.

6.5 UpdateSubjectDto (body)

FieldTypeRequiredDefaultValidationExampleSource
namestringNoN/ASame as create—subject.dto.ts
codestringNoN/ASame as create—same
typeSubjectTypeNoN/ASame as create—same
defaultMaxMarksnumberNoN/ASame as create—same
defaultPassMarksnumberNoN/ASame as create—same
isActivebooleanNoN/AIsBoolean—same
versionnumberYesN/AInt, >= 03same

Every field is optional EXCEPT version. null is rejected for every field here — the one place null is meaningful in this module is teacherId on the assignment DTOs, which is why this DTO does not accept it anywhere.

6.6 ListSubjectsQueryDto (query, extends QueryDto)

FieldTypeRequiredDefaultValidationExampleSource
typeSubjectTypeNoN/AIsIn(SUBJECT_TYPES)?type=optionalsubject.dto.ts
isActivebooleanNoN/AQueryBoolean transform + IsBoolean?isActive=falsesame
(inherited) paginationbooleanNotrueTransform coerces "true"/""/absent to true?pagination=falsequery.dto.ts
(inherited) pagenumberNo1Int, >= 1?page=2same
(inherited) sizenumberNo20Int, 1-100?size=50same
(inherited) sortstringNo"updatedAt"Any stringAccepted but IGNORED — findAll's orderBy is fixed.same
(inherited) order"asc" | "desc"No"desc"IsEnumAccepted but IGNORED, same reason.same
(inherited) searchstringNoN/AMax 100 chars, trimmed?search=mathsame

6.7 SubjectOptionsQueryDto (query, NOT a QueryDto subtype)

FieldTypeRequiredDefaultValidationExampleSource
includeInactivebooleanNofalse (omit → active only)QueryBoolean transform + IsBoolean?includeInactive=truesubject.dto.ts

6.8 ClassSubjectClassDto (nested response)

FieldTypeRequiredDefaultValidationExampleSource
publicIdstringYesN/AN/A—class-subject.dto.ts
gradeGradeDtoYesN/AN/A—same
sectionSectionDtoYesN/AN/A—same
shift"morning" | "day"YesN/AN/A"morning"same
namestring | nullNo (nullable)N/AN/A"Grade 5A"same
academicSessionIdnumberYesN/AN/A4same

Deliberately composed from GradeDto/SectionDto/ClassShift rather than reusing ClassOptionDto, which carries an enrolledCount this list has no use for.

6.9 ClassSubjectDto (response)

FieldTypeRequiredDefaultValidationExampleSource
idstring (uuid)YesN/AN/A, IS the public identifier—class-subject.dto.ts
subjectSubjectOptionDtoYesN/AN/A—same
classClassSubjectClassDtoYesN/AN/A—same
teacherClassTeacherDto | nullNo (nullable)N/AN/A—class.dto.ts (reused)
typeSubjectTypeYesN/AN/A, EFFECTIVE value copied at assignment time"compulsory"class-subject.dto.ts
maxMarksnumberYesN/AN/A100same
passMarksnumberYesN/AN/A40same
isActivebooleanYesN/AN/Atruesame
versionnumberYesN/AN/A2same
createdAtstring (ISO date)YesN/AN/A—same
updatedAtstring (ISO date)YesN/AN/A—same

teacher is ClassTeacherDto, reused verbatim from class.dto.ts — it carries id, fullName, employeeCode, employmentStatus and a live-derived isTeaching. This is precisely the field set ClassSubjects_READ is withheld from pupils/guardians for.

6.10 MySubjectDto (response, consumer)

FieldTypeRequiredDefaultValidationExampleSource
subjectNamestringYesN/AN/A"Mathematics"class-subject.dto.ts
subjectCodestringYesN/AN/A"MATH101"same
typeSubjectTypeYesN/AN/A"compulsory"same
teacherNamestring | nullNo (nullable)N/AN/A, NAME only, no id/code/status"Sita Sharma"same
maxMarksnumberYesN/AN/A100same
passMarksnumberYesN/AN/A40same
classLabelstringYesN/AN/A, flattened server-side"Grade 9 A (Morning)"same

Deliberately NOT ClassSubjectDto and deliberately not a Pick<> of it — written field-by-field so an admin field can never silently widen this consumer shape.

6.11 MySubjectsPayloadDto (response envelope)

FieldTypeRequiredDefaultValidationExampleSource
itemsMySubjectDto[]YesN/AN/A—class-subject.dto.ts
truncatedbooleanYesN/AN/Afalsesame

6.12 MySubjectsQueryDto (query, consumer)

FieldTypeRequiredDefaultValidationExampleSource
academicSessionIdnumberNoCurrent session (server-resolved)Int, >= 1?academicSessionId=4class-subject.dto.ts

6.13 AssignSubjectItemDto (body, nested in a batch)

FieldTypeRequiredDefaultValidationExampleSource
classIdstring (uuid)YesN/AIsUUID—class-subject.dto.ts
teacherIdstring | nullNoAbsent = keep existing/unassigned on insertIsUUID when present; ValidateIf skips validation for null/absentExplicit null clears the teacher.same
typeSubjectTypeNoFalls back to the subject's own typeIsIn(SUBJECT_TYPES)—same
maxMarksnumberNoFalls back to the subject's defaultMaxMarksInt, 1-1000100same
passMarksnumberNoFalls back to the subject's defaultPassMarksInt, 0-100040same
isActivebooleanNoAbsent means "insert active, or leave a suspended row suspended"IsBooleanSet true to REVIVE a suspended row.same
versionnumberNoRequired in practice by the admin grid for every row it displaysInt, >= 02same

Absent is not null, and the difference is load-bearing: on INSERT an absent field takes the subject's default; on the CONFLICT (upsert) branch an absent field KEEPS the existing value, only a field actually supplied is written.

6.14 AssignSubjectToClassesDto (body)

FieldTypeRequiredDefaultValidationExampleSource
itemsAssignSubjectItemDto[]YesN/AArray, 1-100 items (ASSIGNMENT_BATCH_MAX), each validated—class-subject.dto.ts

6.15 AssignmentItemErrorDto (response, nested inside a 422)

FieldTypeRequiredDefaultValidationExampleSource
indexnumberYesN/AN/A2class-subject.dto.ts
classIdstringYesN/AN/A—same
errorCodestringYesN/AN/A"CLASS_SUBJECT_MARKS_INVALID"same
messagestringYesN/AN/A—same

6.16 AssignmentResultDto (response, 201)

FieldTypeRequiredDefaultValidationExampleSource
assignedClassSubjectDto[]YesN/AN/A—class-subject.dto.ts
skippedSuspendedstring[]YesN/AN/A, class_subjects.id values—same

6.17 UpdateClassSubjectDto (body)

FieldTypeRequiredDefaultValidationExampleSource
teacherIdstring | nullNoN/ASame ValidateIf/IsUUID pattern as AssignSubjectItemDtoExplicit null clears.class-subject.dto.ts
typeSubjectTypeNoN/AIsIn(SUBJECT_TYPES)—same
maxMarksnumberNoN/AInt, 1-1000—same
passMarksnumberNoN/AInt, 0-1000—same
isActivebooleanNoN/AIsBoolean; false → true re-runs assign()'s revival preconditions—same
versionnumberYesN/AInt, >= 02same

6.18 ListClassSubjectsQueryDto (query, extends QueryDto)

FieldTypeRequiredDefaultValidationExampleSource
isActivebooleanNoN/AQueryBoolean transform + IsBoolean?isActive=falseclass-subject.dto.ts
(inherited) paginationbooleanNotruepagination=false is REFUSED at the service layer for both list routes on this DTO — every row embeds staff identity.—query.dto.ts
(inherited) page/size/sort/order/searchas QueryDtoNoas QueryDtosort/order/search are accepted by the DTO but not consulted — both list methods use a fixed orderBy (grade sort order, section sort order, shift, id) and apply no free-text search.—same

6.19 QueryDto — shared base

FieldTypeRequiredDefaultValidationExampleSource
paginationbooleanNotrueTransform: undefined/null/"" → true; string "true"/"false" coerced—common/dto/query.dto.ts
pagenumberNo1Int, >= 1—same
sizenumberNo20Int, 1-100 (MAX_PAGE_SIZE)—same
sortstringNo"updatedAt"Any string, <= 100 chars—same
order"asc" | "desc"No"desc"IsEnum—same
searchstringNoN/A<= 100 chars, trimmed—same

7. Enum Reference

EnumValueMeaningRuntime EffectSource
SubjectType (subject_type in Postgres)compulsoryEvery enrolled pupil in the class must sit it.Default type for a subject and, unless overridden, for every assignment of it.subject.dto.ts (SUBJECT_TYPES), subjects.ts (subjectTypeEnum)
SubjectTypeoptionalA pupil may or may not be taking it.Same mechanism, opposite meaning.same
ClassShift (embedded via ClassSubjectClassDto.shift)morningThe class's morning shift.Rendered as-is; capitalized server-side only inside MySubjectDto.classLabel's composed string.class.dto.ts (CLASS_SHIFTS)
ClassShiftdayThe class's day shift.As above.same

8. Endpoint Reference

8.1 GET /api/subjects

Purpose

Lists and searches the school-wide subject catalogue. Called by the subject-management screen and by any admin surface offering a subject filter. Supports free-text search over name/code, a type filter, an isActive filter, and standard offset pagination; also servable unpaginated for a select box, capped at 1000 rows.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/academic-structure/subjects/subjects.controller.ts
DTOapps/api/src/modules/academic-structure/dto/subject.dto.ts (ListSubjectsQueryDto, SubjectDto)
Serviceapps/api/src/modules/academic-structure/subjects/subjects-read.service.ts (findAll)
Schemapackages/db/src/schema/school/subjects.ts
Testsapps/api/src/modules/academic-structure/__tests__/subjects-read.service.integration.spec.ts

Auth and Permissions

  • Auth: JWT required.
  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: Subjects_READ.
  • Guest support: No.
  • Rate limit: None module-specific.
  • Idempotency: N/A (read).

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <jwt>.
ParamsNo—
QueryNosearch, type, isActive, pagination, page, size (sort/order accepted, ignored).
BodyNo—
GET /api/subjects?search=math&type=compulsory&isActive=true&page=1&size=20 HTTP/1.1

Response

{
  "message": "Subjects fetched.",
  "data": [
    {
      "publicId": "0f2e...uuid",
      "name": "Mathematics",
      "code": "MATH101",
      "type": "compulsory",
      "defaultMaxMarks": 100,
      "defaultPassMarks": 40,
      "isActive": true,
      "classCount": 6,
      "version": 3,
      "createdAt": "2026-01-01T00:00:00.000Z",
      "updatedAt": "2026-01-10T00:00:00.000Z"
    }
  ],
  "count": 1,
  "currentPage": 1,
  "totalPage": 1,
  "errorCode": null
}

With pagination=false, the envelope omits count/currentPage/totalPage entirely.

Side Effects

  • Database reads: subjects, correlated class_subjects count.
  • Cache: reads academic-structure:subjects:list:*; writes it on a miss with a 300s TTL.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
403PERMISSION_INSUFFICIENTMissing Subjects_READ.Not authorized to view the subject catalogue.RoleGuard
403AUTH_ACTIVE_ROLE_REQUIREDMulti-role session, none selected.Choose a role first.RoleGuard

Edge Cases

  • Empty search term → treated as no filter.
  • search containing %/_ → escaped, matched literally.
  • pagination=false beyond the 1000-row cap → results silently truncated at the cap with no truncated flag on THIS endpoint (unlike options) — the list route has no such field in its response envelope.
  • Redis unavailable → falls through to Postgres transparently.

Example Requests

curl -X GET "$API_URL/api/subjects?search=math&isActive=true" \
  -H "Authorization: Bearer TOKEN"

8.2 GET /api/subjects/options

Purpose

Returns a lightweight, unpaginated, person-data-free subject list for populating an assignment picker's dropdown. Declared ahead of :publicId in the controller so it is never swallowed by the parameterised route.

Source Evidence

EvidencePath
Controllersubjects.controller.ts
DTOsubject.dto.ts (SubjectOptionsQueryDto, SubjectOptionsPayloadDto)
Servicesubjects-read.service.ts (findOptions)
Testssubjects-read.service.integration.spec.ts

Auth and Permissions

  • Auth: JWT required. Guard chain: JwtAuthGuard, RoleGuard. Permission: Subjects_READ. Guest support: No. Rate limit: None. Idempotency: N/A.

Request

PartRequiredDetails
QueryNoincludeInactive (default omitted = active only).
GET /api/subjects/options?includeInactive=false HTTP/1.1

Response

{
  "message": "Subject options fetched.",
  "data": {
    "items": [
      {
        "publicId": "0f2e...uuid",
        "name": "Mathematics",
        "code": "MATH101",
        "type": "compulsory",
        "defaultMaxMarks": 100,
        "defaultPassMarks": 40
      }
    ],
    "truncated": false
  },
  "errorCode": null
}

Note the double nesting: ResponseDto.data wraps SubjectOptionsPayloadDto, whose rows are under items. The inner field is items and not data deliberately — ClassOptionsPayloadDto established that shape for the same "unpaginated list plus truncated" problem, and a second envelope for one idea is how a consumer ends up reading the wrong key and silently rendering nothing.

Side Effects

Database read only. Not cached.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
403PERMISSION_INSUFFICIENTMissing Subjects_READ.Not authorized.RoleGuard

Edge Cases

  • Catalogue exceeds 1000 rows → truncated: true, rows beyond the cap withheld, no error raised.
  • includeInactive=true → archived subjects included, still capped.

8.3 GET /api/subjects/:publicId

Purpose

Reads a single subject's full detail, including its live classCount, for a subject's detail screen.

Source Evidence

EvidencePath
Controllersubjects.controller.ts
Servicesubjects-read.service.ts (findOne/loadDto)
Testssubjects-read.service.integration.spec.ts

Auth and Permissions

Auth: JWT required. Guard chain: JwtAuthGuard, RoleGuard. Permission: Subjects_READ. Guest support: No.

Request

PartRequiredDetails
ParamsYespublicId — the subject's uuid.
GET /api/subjects/0f2e...uuid HTTP/1.1

Response

Same shape as one row of 8.1's data.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404SUBJECT_NOT_FOUNDNo row with that publicId.The subject does not exist.subjects-read.service.ts
403PERMISSION_INSUFFICIENTMissing Subjects_READ.Not authorized.RoleGuard

Edge Cases

  • Archived subject: still readable, isActive: false in the payload.

8.4 POST /api/subjects

Purpose

Creates a new subject in the catalogue. Called from the "add subject" form on the subject-management screen.

Source Evidence

EvidencePath
Controllersubjects.controller.ts
DTOsubject.dto.ts (CreateSubjectDto)
Servicesubjects-write.service.ts (create)
Testssubjects-write.service.integration.spec.ts

Auth and Permissions

Auth: JWT required. Guard chain: JwtAuthGuard, RoleGuard. Permission: Subjects_CREATE (superadmin only in the seed). Guest support: No. Idempotency: None — a second identical call creates a second row unless refused by the uniqueness constraints.

Request

PartRequiredDetails
BodyYesCreateSubjectDto — see 6.4.

Minimal valid request:

{
  "name": "Mathematics",
  "code": "MATH101",
  "type": "compulsory"
}

Full valid request:

{
  "name": "Mathematics",
  "code": "MATH101",
  "type": "compulsory",
  "defaultMaxMarks": 100,
  "defaultPassMarks": 40,
  "isActive": true
}

Response

201, body shaped as one SubjectDto, classCount: 0.

Side Effects

  • Database write: subjects (insert), inside a transaction.
  • Cache: academic-structure:subjects:* prefix invalidated.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
400(validation, no errorCode)Malformed name/code/type.Fix the highlighted field.Global ValidationPipe
409SUBJECT_NAME_TAKENAn active subject already has this name.Choose a different name.subjects-write.service.ts
409SUBJECT_CODE_TAKENAn active subject already has this code.Choose a different code.same
409SUBJECT_MARKS_INVALIDdefaultPassMarks > defaultMaxMarks.Fix the mark scheme.same
403PERMISSION_INSUFFICIENTMissing Subjects_CREATE.Not authorized.RoleGuard

Edge Cases

  • Name/code matching an ARCHIVED subject: allowed — the partial unique indexes only guard active rows.
  • Marks omitted entirely: defaults to 100/40.
  • isActive: false on create: subject is created already archived.

Example Requests

curl -X POST "$API_URL/api/subjects" \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Mathematics","code":"MATH101","type":"compulsory"}'

8.5 PATCH /api/subjects/:publicId

Purpose

Edits a subject's name, code, type, default marks, or active state — including retiring or reactivating it.

Source Evidence

EvidencePath
Controllersubjects.controller.ts
DTOsubject.dto.ts (UpdateSubjectDto)
Servicesubjects-write.service.ts (update)
Testssubjects-write.service.integration.spec.ts

Auth and Permissions

Auth: JWT required. Guard chain: JwtAuthGuard, RoleGuard. Permission: Subjects_UPDATE. Idempotency: A no-op patch (version only) is safe to repeat, though version itself advances each time.

Request

PartRequiredDetails
ParamsYespublicId.
BodyYesUpdateSubjectDto — every field optional except version.

Full valid request (reactivating):

{
  "isActive": true,
  "version": 3
}

Response

200, updated SubjectDto, version advanced.

Side Effects

  • Database write: subjects (version-gated update, inside a transaction).
  • Cache: prefix invalidated.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404SUBJECT_NOT_FOUNDNo row with that publicId.The subject does not exist.subjects-write.service.ts
409SUBJECT_VERSION_CONFLICTSupplied version is stale.Reload and retry.same
409SUBJECT_MARKS_INVALIDMerged passMarks > maxMarks.Fix the mark scheme.same
409SUBJECT_NAME_TAKEN_ON_REACTIVATEReactivating onto a name held by a different active subject.Rename first, or archive the holder.same
409SUBJECT_CODE_TAKEN_ON_REACTIVATESame, for code.Same remedy.same
400(validation)Malformed field.Fix the highlighted field.Global pipe
403PERMISSION_INSUFFICIENTMissing Subjects_UPDATE.Not authorized.RoleGuard

Edge Cases

  • Empty patch ({ version } only): still succeeds, version still advances.
  • Retiring a subject with active assignments: allowed — no check against class_subjects on this route.

Example Requests

curl -X PATCH "$API_URL/api/subjects/0f2e...uuid" \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"defaultPassMarks":45,"version":3}'

8.6 DELETE /api/subjects/:publicId

Purpose

Permanently removes a subject that has never been assigned to a class, or has had every assignment removed.

Source Evidence

EvidencePath
Controllersubjects.controller.ts
Servicesubjects-write.service.ts (remove)
Testssubjects-write.service.integration.spec.ts

Auth and Permissions

Permission: Subjects_DELETE. Idempotency: none — a repeated call on an already-deleted id returns 404.

Request

PartRequiredDetails
ParamsYespublicId.

Response

200, { "message": "Subject deleted.", "data": null, "errorCode": null }.

Side Effects

  • Database write: subjects (hard delete).
  • Cache: prefix invalidated.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404SUBJECT_NOT_FOUNDNo row with that id.Already gone or never existed.subjects-write.service.ts
409SUBJECT_IN_USEAny class_subjects row references it, active or suspended.Message names total and active counts; remove assignments first, or archive instead.same
403PERMISSION_INSUFFICIENTMissing Subjects_DELETE.Not authorized.RoleGuard

8.7 GET /api/subjects/:publicId/assignments

Purpose

Lists every class currently or previously teaching one subject, for that subject's assignments tab.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/academic-structure/class-subjects/class-subjects.controller.ts
DTOclass-subject.dto.ts (ListClassSubjectsQueryDto, ClassSubjectDto)
Serviceclass-subjects-read.service.ts (findForSubject)
Testsclass-subjects-read.service.integration.spec.ts, class-subjects-authorization.integration.spec.ts

Auth and Permissions

Permission: ClassSubjects_READ. Row scope: PeopleAccessService.scopeFor(actor, "classSubjects", heldPermissions), applied before the query is built.

Request

PartRequiredDetails
ParamsYespublicId — the SUBJECT's publicId.
QueryNoisActive, page, size (pagination=false refused).

Response

{
  "message": "Assignments fetched.",
  "data": [
    {
      "id": "01930...uuid",
      "subject": { "publicId": "0f2e...uuid", "name": "Mathematics", "code": "MATH101", "type": "compulsory", "defaultMaxMarks": 100, "defaultPassMarks": 40 },
      "class": { "publicId": "...", "grade": { "id": 5, "publicId": "...", "name": "5", "code": "G5", "sortOrder": 5, "isActive": true, "createdAt": "...", "updatedAt": "..." }, "section": { "id": 1, "publicId": "...", "name": "A", "sortOrder": 0, "isActive": true, "createdAt": "...", "updatedAt": "..." }, "shift": "morning", "name": "Grade 5A", "academicSessionId": 4 },
      "teacher": { "id": "...uuid", "fullName": "Sita Sharma", "employeeCode": "EMP-0007", "employmentStatus": "active", "isTeaching": true },
      "type": "compulsory",
      "maxMarks": 100,
      "passMarks": 40,
      "isActive": true,
      "version": 2,
      "createdAt": "...",
      "updatedAt": "..."
    }
  ],
  "count": 1,
  "currentPage": 1,
  "totalPage": 1,
  "errorCode": null
}

Side Effects

Database read only, joined across six tables. Never cached — the result is scope-dependent per actor.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404SUBJECT_NOT_FOUNDNo subject with that publicId.Reload the subject list.class-subjects-read.service.ts
400PAGINATION_LIMIT_INVALIDpagination=false requested.Page through the result — the row embeds staff identity.same
403PERMISSION_INSUFFICIENTMissing ClassSubjects_READ.Not authorized.RoleGuard

8.8 POST /api/subjects/:publicId/assignments

Purpose

Assigns one already-created subject to one or more classes, each with its own optional teacher and mark scheme, in a single all-or-nothing transaction. The single-item case (the row-level "save" button in the admin grid) is simply a one-element batch through this same endpoint.

Source Evidence

EvidencePath
Controllerclass-subjects.controller.ts
DTOclass-subject.dto.ts (AssignSubjectToClassesDto, AssignSubjectItemDto, AssignmentResultDto)
Serviceclass-subjects-write.service.ts (assign)
Testsclass-subjects-write.service.integration.spec.ts

Auth and Permissions

Permission: ClassSubjects_CREATE (superadmin only in the seed). Idempotency: re-sending an identical batch is idempotent per-field; re-sending a SUBSET is a genuine partial edit, not a repeat.

Request

PartRequiredDetails
ParamsYespublicId — the subject's publicId.
BodyYes{ items: AssignSubjectItemDto[] }, 1-100 items.

Minimal valid request (single class, everything defaulted):

{ "items": [{ "classId": "class-uuid-1" }] }

Full valid request (batch, explicit override, one clearing a teacher):

{
  "items": [
    { "classId": "class-uuid-1", "teacherId": "staff-uuid-1", "maxMarks": 100, "passMarks": 40, "version": 1 },
    { "classId": "class-uuid-2", "teacherId": null, "isActive": true }
  ]
}

Response

201:

{
  "message": "Subject assigned.",
  "data": {
    "assigned": [ { "...": "ClassSubjectDto" } ],
    "skippedSuspended": []
  },
  "errorCode": null
}

Side Effects

  • Database writes: class_subjects (batch upsert on (classId, subjectId)), inside one transaction.
  • Cache: academic-structure:subjects:* invalidated on commit (the parent subject's classCount moved).

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
400CLASS_SUBJECT_BATCH_INVALIDitems is empty.Add at least one item.class-subjects-write.service.ts
404SUBJECT_NOT_FOUNDSubject publicId does not resolve.Reload the subject list.same
422SUBJECT_INACTIVEThe subject is archived.Reactivate it first.same
422CLASS_SUBJECT_BATCH_INVALIDOne or more items failed validation; carries errors: AssignmentItemErrorDto[].Fix every named index; nothing was written.same

Per-item codes that appear inside errors[]: CLASS_NOT_FOUND, CLASS_INACTIVE, CLASS_SUBJECT_DUPLICATE_CLASS, CLASS_SUBJECT_TEACHER_NOT_FOUND, CLASS_SUBJECT_TEACHER_NOT_TEACHING, CLASS_SUBJECT_MARKS_INVALID, CLASS_SUBJECT_DUPLICATE_LABEL, CLASS_SUBJECT_VERSION_CONFLICT.

Edge Cases

  • Existing suspended row, item omits isActive: left suspended, id appears in skippedSuspended.
  • Existing suspended row, item sets isActive: true: revived, other omitted fields keep their prior values.
  • Two items name the same class (by resolved integer id, regardless of uuid text casing): second flagged CLASS_SUBJECT_DUPLICATE_CLASS.
  • teacherId: null explicit: clears an existing assignment's teacher on the upsert branch, or leaves it unassigned on insert.

Example Requests

curl -X POST "$API_URL/api/subjects/0f2e...uuid/assignments" \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"classId":"class-uuid-1","teacherId":"staff-uuid-1"}]}'

8.9 GET /api/classes/:classPublicId/subjects

Purpose

Lists every subject a specific class currently or previously taught, for the class's own subjects tab.

Source Evidence

EvidencePath
Controllerclass-subjects.controller.ts
Serviceclass-subjects-read.service.ts (findForClass)
Testsclass-subjects-read.service.integration.spec.ts

Auth and Permissions

Permission: ClassSubjects_READ. Row scope: the class itself is resolved THROUGH the scope predicate before its assignments are queried.

Request

PartRequiredDetails
ParamsYesclassPublicId.
QueryNoisActive, page, size (pagination=false refused).

Response

Same row shape as 8.7.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404CLASS_NOT_FOUNDNo class with that publicId WITHIN the caller's scope — indistinguishable from a genuinely absent class.Reload the class list.class-subjects-read.service.ts
400PAGINATION_LIMIT_INVALIDpagination=false.Page through the result.same
403PERMISSION_INSUFFICIENTMissing ClassSubjects_READ.Not authorized.RoleGuard

Edge Cases

  • A class the caller's scope excludes: 404, never 403 — resolving through the scope predicate rather than after is what makes the id space non-enumerable.

8.10 PATCH /api/class-subjects/:id

Purpose

Reconfigures one assignment's teacher, effective type, marks, or active state — including suspending or reviving it.

Source Evidence

EvidencePath
Controllerclass-subjects.controller.ts
DTOclass-subject.dto.ts (UpdateClassSubjectDto)
Serviceclass-subjects-write.service.ts (update)
Testsclass-subjects-write.service.integration.spec.ts

Auth and Permissions

Permission: ClassSubjects_UPDATE (superadmin only in the seed).

Request

PartRequiredDetails
ParamsYesid — the assignment's uuid (its own primary key).
BodyYesUpdateClassSubjectDto, every field optional except version.

Full valid request (reviving with a new teacher):

{ "teacherId": "staff-uuid-2", "isActive": true, "version": 2 }

Response

200, updated ClassSubjectDto.

Side Effects

  • Database write: class_subjects, inside a transaction.
  • Cache: academic-structure:subjects:* invalidated.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404CLASS_SUBJECT_NOT_FOUNDNo row with that id.Reload the assignment list.class-subjects-write.service.ts
409CLASS_SUBJECT_VERSION_CONFLICTSupplied version stale.Reload and retry.same
404CLASS_SUBJECT_TEACHER_NOT_FOUNDteacherId does not resolve or is soft-deleted.Pick a different staff member.same
422CLASS_SUBJECT_TEACHER_NOT_TEACHINGDesignation is not a teaching one.Pick a teaching staff member.same
422SUBJECT_INACTIVEReviving against an archived subject.Reactivate the subject first.same
409CLASS_SUBJECT_DUPLICATE_LABELReviving into a name/code the class already actively teaches under a different subject row.Rename/retire the colliding subject, or do not revive this one there.same
409CLASS_SUBJECT_MARKS_INVALIDMerged passMarks > maxMarks.Fix the marks.same
403PERMISSION_INSUFFICIENTMissing ClassSubjects_UPDATE.Not authorized.RoleGuard

Edge Cases

  • false → true on isActive: runs the SAME preconditions the batch assign() runs — the precondition belongs to the transition, not to the endpoint.
  • teacherId: null: clears the teacher.

8.11 DELETE /api/class-subjects/:id

Purpose

Permanently removes one assignment row.

Source Evidence

EvidencePath
Controllerclass-subjects.controller.ts
Serviceclass-subjects-write.service.ts (remove)
Testsclass-subjects-write.service.integration.spec.ts

Auth and Permissions

Permission: ClassSubjects_DELETE.

Request

PartRequiredDetails
ParamsYesid.

Response

200, { "message": "Assignment deleted.", "data": null, "errorCode": null }.

Side Effects

  • Database write: class_subjects (hard delete).
  • Cache: prefix invalidated.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404CLASS_SUBJECT_NOT_FOUNDNo row with that id.Already gone.class-subjects-write.service.ts
409CLASS_SUBJECT_IN_USERegistered but UNREACHABLE today — nothing yet references class_subjects.N/A.same
403PERMISSION_INSUFFICIENTMissing ClassSubjects_DELETE.Not authorized.RoleGuard

8.12 GET /api/mobile/student/subjects

Purpose

A pupil's own "My Subjects" screen: what their currently-enrolled class is being taught this academic session.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/mobile/student/student-portal.controller.ts (StudentSubjectsPortalController)
DTOclass-subject.dto.ts (MySubjectsQueryDto, MySubjectsPayloadDto)
Serviceapps/api/src/modules/mobile/student/student-portal.service.ts (listMySubjects)
Testsapps/api/src/modules/mobile/__tests__/portal-subjects.integration.spec.ts

Auth and Permissions

  • Auth: JWT required. Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: None — no @Permissions() decorator; the handler sits on NO_PERMISSION_ADMIN_HANDLERS.
  • Guest support: No.
  • The real boundary: assertPortalAudience(actor, "student") inside the service, refusing any session whose active role's scopeKind is not "student".

Request

PartRequiredDetails
QueryNoacademicSessionId (defaults to the current session).
GET /api/mobile/student/subjects HTTP/1.1
Authorization: Bearer TOKEN

Response

{
  "message": "Subjects fetched.",
  "data": {
    "items": [
      {
        "subjectName": "Mathematics",
        "subjectCode": "MATH101",
        "type": "compulsory",
        "teacherName": "Sita Sharma",
        "maxMarks": 100,
        "passMarks": 40,
        "classLabel": "Grade 9 A (Morning)"
      }
    ],
    "truncated": false
  },
  "errorCode": null
}

Side Effects

Database read only, across student_class_enrollments, class_subjects, subjects, classes, grades, sections, staff, users, academic_sessions. Never cached.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
403PERMISSION_INSUFFICIENTSession not acting as student.This route is served to a pupil session.portal-audience.ts
403AUTH_ACTIVE_ROLE_REQUIREDNo active role selected.Choose a role first.same
403PERMISSION_ROLE_NOT_ASSIGNEDSession holds no role at all.Not user-fixable client-side.same

Edge Cases

  • No active enrolment: 200 { items: [], truncated: false }, not a 404.
  • No current academic session configured: same empty result.
  • Pupil soft-deleted or recordStatus not active: excluded by the enrolment subquery, same empty result.
  • Assignment with no teacher: teacherName: null.
  • Over 1000 rows: truncated: true.

Example Requests

curl -X GET "$API_URL/api/mobile/student/subjects" \
  -H "Authorization: Bearer TOKEN"

8.13 GET /api/mobile/teacher/subjects

Purpose

A teacher's own "My Subjects" screen: every class-subject assignment where they are the assigned teacher, for the current academic session.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/mobile/teacher/teacher-portal.controller.ts (TeacherSubjectsPortalController)
Serviceapps/api/src/modules/mobile/teacher/teacher-portal.service.ts (listMySubjects)
Testsportal-subjects.integration.spec.ts

Auth and Permissions

  • Permission: None — the boundary is assertTeacherStaffRow, which requires the active role named teacher, resolves the caller's own LIVE staff.id, and refuses any other active role or a session with no live staff row.

Request

Same query shape as 8.12.

Response

Same envelope shape as 8.12; teacherName on every row is always the caller's own name — the WHERE teacherId = :staffId clause already scopes the query to them, so no staff/users join is needed and actor.name is used directly.

Side Effects

Database read only, across class_subjects, subjects, classes, grades, sections, academic_sessions. Never cached.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
403PERMISSION_INSUFFICIENTSession acting as guardian/student, or no live staff row.This route is served to a staff session.portal-audience.ts
403AUTH_ACTIVE_ROLE_REQUIREDNo active role.Choose a role first.same

Edge Cases

  • No class_subjects row names this teacher: 200 { items: [], truncated: false }.
  • Dismissed teacher with another live profile (e.g. also a guardian): refused — staff.deleted_at is filtered explicitly, even though the full unique index on staff.user_id means the row still exists.
  • No current academic session: empty result.

9. Flow Diagrams

9.1 Route Ownership

9.2 Request Sequence — assign a subject to classes

9.3 Error Branch — mutation endpoints

EndpointPagination TypeDefault SizeMax SizeSort FieldsFiltersResult Cap
GET /api/subjectspage/size, or pagination=false20100Fixed: case-folded name, then id (sort/order accepted, ignored)search, type, isActive1000 when unpaginated
GET /api/subjects/optionsAlways unpaginatedN/AN/AFixed: case-folded name, then idincludeInactive1000, truncated flagged
GET /api/subjects/:publicId/assignmentspage/size only — pagination=false REFUSED20100Fixed: grade sort order, section sort order, shift, idisActiveN/A (always paginated)
GET /api/classes/:classPublicId/subjectspage/size only — pagination=false REFUSED20100Same fixed orderisActiveN/A
GET /api/mobile/student/subjectsAlways unpaginatedN/AN/AFixed: subjects.name, then subjects.idacademicSessionId1000, truncated flagged
GET /api/mobile/teacher/subjectsAlways unpaginatedN/AN/AFixed: classes.id, then subjects.name, then subjects.idacademicSessionId1000, truncated flagged

Shared pagination utility: PaginationUtil (common/utils/pagination.util.ts) — normalize(), getDrizzleParams(), buildMetadata(), constants DEFAULT_SIZE=20, MAX_PAGE_SIZE=100, UNPAGINATED_HARD_CAP=1000. Broad-search detection: none — search on GET /api/subjects is a plain ILIKE substring match, no relevance scoring. Cache behavior per query: only GET /api/subjects is cached, keyed deterministically on the full query shape. Empty result behavior: every list endpoint returns 200 with an empty data array — never a 404 for "no rows matched".

11. Caching, Jobs, and External Integrations

IntegrationUsed?DetailsSource
Redis cacheYes, for GET /api/subjects onlyKey academic-structure:subjects:list:..., 300s TTL, invalidated by every write on either tablesubjects-read.service.ts, subjects-write.service.ts, class-subjects-write.service.ts
BullMQNoNot applicable to this module.—
External APINoNot applicable.—

13. Mandatory Deep API Documentation Pack

13.1 Route-by-Route Completeness Matrix

RouteController MethodDTOsService MethodGuardsPermissionsCacheJobsDB TouchesErrorsTestsDocumented?
GET /api/subjectsSubjectsController.findAllListSubjectsQueryDto→SubjectDto[]SubjectsReadService.findAllJwtAuthGuard, RoleGuardSubjects_READacademic-structure:subjects:list:*N/AsubjectsPERMISSION_INSUFFICIENTsubjects-read.service.integration.spec.tsYes
GET /api/subjects/optionsSubjectsController.findOptionsSubjectOptionsQueryDto→SubjectOptionsPayloadDtoSubjectsReadService.findOptionssameSubjects_READN/AN/AsubjectsPERMISSION_INSUFFICIENTsameYes
GET /api/subjects/:publicIdSubjectsController.findOne→SubjectDtoSubjectsReadService.findOnesameSubjects_READN/AN/AsubjectsSUBJECT_NOT_FOUNDsameYes
POST /api/subjectsSubjectsController.createCreateSubjectDto→SubjectDtoSubjectsWriteService.createsameSubjects_CREATEinvalidates prefixN/AsubjectsSUBJECT_NAME_TAKEN, SUBJECT_CODE_TAKEN, SUBJECT_MARKS_INVALIDsubjects-write.service.integration.spec.tsYes
PATCH /api/subjects/:publicIdSubjectsController.updateUpdateSubjectDto→SubjectDtoSubjectsWriteService.updatesameSubjects_UPDATEinvalidates prefixN/AsubjectsSUBJECT_NOT_FOUND, SUBJECT_VERSION_CONFLICT, SUBJECT_MARKS_INVALID, SUBJECT_NAME_TAKEN_ON_REACTIVATE, SUBJECT_CODE_TAKEN_ON_REACTIVATEsameYes
DELETE /api/subjects/:publicIdSubjectsController.remove→nullSubjectsWriteService.removesameSubjects_DELETEinvalidates prefixN/Asubjects, class_subjects (read)SUBJECT_NOT_FOUND, SUBJECT_IN_USEsameYes
GET /api/subjects/:publicId/assignmentsClassSubjectsController.findForSubjectListClassSubjectsQueryDto→ClassSubjectDto[]ClassSubjectsReadService.findForSubjectsameClassSubjects_READN/AN/Aclass_subjects (joined)SUBJECT_NOT_FOUND, PAGINATION_LIMIT_INVALIDclass-subjects-read.service.integration.spec.tsYes
POST /api/subjects/:publicId/assignmentsClassSubjectsController.assignAssignSubjectToClassesDto→AssignmentResultDtoClassSubjectsWriteService.assignsameClassSubjects_CREATEinvalidates prefixN/Aclass_subjects (batch upsert)SUBJECT_NOT_FOUND, SUBJECT_INACTIVE, CLASS_SUBJECT_BATCH_INVALIDclass-subjects-write.service.integration.spec.tsYes
GET /api/classes/:classPublicId/subjectsClassSubjectsController.findForClassListClassSubjectsQueryDto→ClassSubjectDto[]ClassSubjectsReadService.findForClasssameClassSubjects_READN/AN/Aclasses (scoped), class_subjects (joined)CLASS_NOT_FOUND, PAGINATION_LIMIT_INVALIDclass-subjects-read.service.integration.spec.tsYes
PATCH /api/class-subjects/:idClassSubjectsController.updateUpdateClassSubjectDto→ClassSubjectDtoClassSubjectsWriteService.updatesameClassSubjects_UPDATEinvalidates prefixN/Aclass_subjects, subjects (revival check)CLASS_SUBJECT_NOT_FOUND, CLASS_SUBJECT_VERSION_CONFLICT, CLASS_SUBJECT_TEACHER_NOT_FOUND, CLASS_SUBJECT_TEACHER_NOT_TEACHING, SUBJECT_INACTIVE, CLASS_SUBJECT_DUPLICATE_LABEL, CLASS_SUBJECT_MARKS_INVALIDclass-subjects-write.service.integration.spec.tsYes
DELETE /api/class-subjects/:idClassSubjectsController.remove→nullClassSubjectsWriteService.removesameClassSubjects_DELETEinvalidates prefixN/Aclass_subjectsCLASS_SUBJECT_NOT_FOUNDsameYes
GET /api/mobile/student/subjectsStudentSubjectsPortalController.listMySubjectsMySubjectsQueryDto→MySubjectsPayloadDtoStudentPortalService.listMySubjectsJwtAuthGuard, RoleGuardNone (audience-asserted)N/AN/Astudent_class_enrollments, class_subjects (joined)PERMISSION_INSUFFICIENT, AUTH_ACTIVE_ROLE_REQUIREDportal-subjects.integration.spec.tsYes
GET /api/mobile/teacher/subjectsTeacherSubjectsPortalController.listMySubjectsMySubjectsQueryDto→MySubjectsPayloadDtoTeacherPortalService.listMySubjectssameNone (audience-asserted)N/AN/Aclass_subjects (joined)PERMISSION_INSUFFICIENT, AUTH_ACTIVE_ROLE_REQUIREDportal-subjects.integration.spec.tsYes

13.2 Request/Response Exhaustiveness

Present per endpoint above: minimal valid request (8.4, 8.8), full valid request (same two, plus 8.5, 8.10), authenticated request (every endpoint — none is public or guest-reachable), admin request (every write endpoint), success response (every endpoint), empty-list response (8.1, 8.12, 8.13), validation error (every body-accepting endpoint), domain error (every endpoint with a Source Evidence-linked error table), permission error (every endpoint's Error Cases table).

13.3 API Diagram Pack

Present: route ownership graph (9.1), a sequence diagram for the most complex family (9.2), an error decision tree (9.3). No cache-flow diagram beyond the backend doc's Section 8 table — a single cached endpoint does not warrant its own diagram. No async/job flow or realtime/event flow diagram — neither exists on this module.

13.4 Consumer Integration Notes

ConsumerRequired KnowledgeFailure HandlingContract Stability
Web/admin panelEvery admin route requires the specific Subjects_*/ClassSubjects_* code; pagination=false works on the two subject list/detail/options routes but is REFUSED on both assignment list routes.Map 409 SUBJECT_VERSION_CONFLICT/CLASS_SUBJECT_VERSION_CONFLICT to a "reload and retry" prompt; map 422 CLASS_SUBJECT_BATCH_INVALID to per-row grid error markers using errors[].index.Stable.
Mobile app (pupil)GET /api/mobile/student/subjects needs no permission, only an active student-scoped role; an empty result is normal, not an error state.A 403 here means the session is not acting as a pupil — prompt a role switch, not a retry.Stable.
Mobile app (teacher)GET /api/mobile/teacher/subjects needs a live staff row; a dismissed teacher with another live profile is refused even though their account still exists.Same 403 handling as the pupil route.Stable.
QAReproduce a batch failure by submitting a known-bad item (e.g. passMarks exceeding maxMarks) alongside valid ones and asserting NOTHING was written for the valid items either.class-subjects-write.service.integration.spec.ts is the reference fixture set.Stable.
Internal serviceNo internal service calls this module today — no queue, no webhook, no service token path exists.N/AN/A

13.5 API Tradeoffs and Rationale

DecisionChosen BehaviorAlternatives ConsideredWhy This TradeoffRiskMitigation
Pagination styleOffset (page/size), with an unpaginated escape hatch capped at 1000Cursor paginationMatches every other admin list in this codebase; a subject catalogue is small enough that offset drift is not a real concernVery lowUNPAGINATED_HARD_CAP bounds the worst case
Batch assignment endpoint accepts both single and multi-item bodiesOne endpoint, items: [...], min length 1A separate single-item POSTAvoids two code paths that could drift; a single-item request is simply a one-element arrayNone observedN/A
Assignment list routes refuse pagination=false while the subject list allows itDifferent defaults per endpoint, on purposeUniform pagination policy across the whole moduleThe assignment rows embed staff identity (employee code, employment status); the subject rows do notLow — documented explicitly in both the DTO and the servicePAGINATION_LIMIT_INVALID is a clear, named refusal rather than a silent truncation
Consumer routes carry no permission codeAudience assertion in-service instead of @Permissions()Grant Subjects_READ/ClassSubjects_READ-shaped codes to pupils/teachers for these routesA permission-gated version would need EITHER pupils holding ClassSubjects_READ (leaking staff identity) or a third permission code just for this narrow sliceLow — the audience assertion is a hard 403, equally strictCovered by its own authorization integration spec

13.6 API Change Impact

ChangeAffected ConsumersBackend ImpactData ImpactMigration Needed?Compatibility Plan
Adding a guardian-facing subjects routeAdmin panel (new screen), mobile appNew controller + service, no schema changeNoneNoAdditive; would need its own permission code and audience assertion, following the pattern in portal-audience.ts
Renaming defaultMaxMarks/defaultPassMarksAdmin panel, any exportDTO field rename, no schema rename required (already a separate concern)NoneNoVersioned rollout: add the new field alongside the old for one release, then remove
Adding a RESTORE route for either tableAdmin panelWould require adding a deleted_at column to both tables — a genuine schema changeMigrationYesOut of scope until soft-delete is actually adopted for this module

14. Zero-Omission API Checklist

  • Every controller route is documented — 13 routes across four controllers.
  • Every parent route prefix and runtime URL is documented.
  • Every DTO field, nested field, enum, default, transform, and validator is documented.
  • Every response field, nullable field, generated field is documented; no raw entity field is ever returned unmapped — every service maps field-by-field.
  • Every auth, guard, permission, and guest identity branch is documented, including the two no-permission consumer routes and their audience assertion.
  • Every success, validation, auth, permission, not-found, conflict, and server-error branch is documented.
  • Every database read/write and cache hit/miss/write/invalidation is documented; there are no queue jobs, realtime events, notifications, or external calls on this module, stated explicitly rather than omitted.
  • Every route has minimal/full/success/failure examples where a body applies.
  • Route ownership, one representative sequence, and an error decision tree are present.
  • Every tradeoff and compatibility risk is documented.
  • Links to backend and features/flows docs are present below.

15. Integration Checklist

  • Every route from controllers is documented.
  • Every DTO field is documented.
  • Every enum value is documented.
  • Every response envelope is documented, including the double-nested options/options-payload and my-subjects/payload shapes.
  • Every error code is documented.
  • Every auth guard and permission is documented.
  • The one cache key is documented; there are no queue jobs, realtime events, or external calls.
  • Every diagram matches the current code.
  • This page links to backend and features/flows docs. No TDD document exists for this module.

See Also

On this page

Subjects - API Reference1. Documentation Evidence2. Module Summary3. Concepts and Terminology4. API Surface Map5. Auth, Identity, and Permissions6. DTO and Model Reference6.1 SubjectDto (response)6.2 SubjectOptionDto (response, nested)6.3 SubjectOptionsPayloadDto (response envelope for options)6.4 CreateSubjectDto (body)6.5 UpdateSubjectDto (body)6.6 ListSubjectsQueryDto (query, extends QueryDto)6.7 SubjectOptionsQueryDto (query, NOT a QueryDto subtype)6.8 ClassSubjectClassDto (nested response)6.9 ClassSubjectDto (response)6.10 MySubjectDto (response, consumer)6.11 MySubjectsPayloadDto (response envelope)6.12 MySubjectsQueryDto (query, consumer)6.13 AssignSubjectItemDto (body, nested in a batch)6.14 AssignSubjectToClassesDto (body)6.15 AssignmentItemErrorDto (response, nested inside a 422)6.16 AssignmentResultDto (response, 201)6.17 UpdateClassSubjectDto (body)6.18 ListClassSubjectsQueryDto (query, extends QueryDto)6.19 QueryDto — shared base7. Enum Reference8. Endpoint Reference8.1 GET /api/subjectsPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.2 GET /api/subjects/optionsPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.3 GET /api/subjects/:publicIdPurposeSource EvidenceAuth and PermissionsRequestResponseError CasesEdge Cases8.4 POST /api/subjectsPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.5 PATCH /api/subjects/:publicIdPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.6 DELETE /api/subjects/:publicIdPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError Cases8.7 GET /api/subjects/:publicId/assignmentsPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError Cases8.8 POST /api/subjects/:publicId/assignmentsPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.9 GET /api/classes/:classPublicId/subjectsPurposeSource EvidenceAuth and PermissionsRequestResponseError CasesEdge Cases8.10 PATCH /api/class-subjects/:idPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.11 DELETE /api/class-subjects/:idPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError Cases8.12 GET /api/mobile/student/subjectsPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.13 GET /api/mobile/teacher/subjectsPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases9. Flow Diagrams9.1 Route Ownership9.2 Request Sequence — assign a subject to classes9.3 Error Branch — mutation endpoints10. Pagination, Sorting, Filtering, and Search11. Caching, Jobs, and External Integrations13. Mandatory Deep API Documentation Pack13.1 Route-by-Route Completeness Matrix13.2 Request/Response Exhaustiveness13.3 API Diagram Pack13.4 Consumer Integration Notes13.5 API Tradeoffs and Rationale13.6 API Change Impact14. Zero-Omission API Checklist15. Integration ChecklistSee Also