Academic Sessions API Reference
Complete API contracts for the academic sessions lookup, including routes, auth, DTOs, responses, errors, and the split enforcement of the "one current session" rule.
Academic Sessions - API Reference
Audience: Frontend engineers, mobile engineers, backend engineers, QA, and API consumers.
Scope: Admin-facing API owned by AcademicSessionsModule. No public or mobile-facing route exists.
1. Documentation Evidence
| Area | Files Inspected | What Was Verified |
|---|---|---|
| Controller | apps/api/src/modules/academic-sessions/academic-sessions.controller.ts | Routes, methods, guards, permissions, decorators. |
| DTOs | apps/api/src/modules/academic-sessions/dto/academic-session.dto.ts | Request, query, response, validation, defaults. |
| Service | apps/api/src/modules/academic-sessions/academic-sessions.service.ts | Behavior, transactions, side effects, error translation. |
| Module | apps/api/src/modules/academic-sessions/academic-sessions.module.ts | Wiring and dependency on RoleModule. |
| Schema | packages/db/src/schema/school/academic-sessions.ts | Columns, indexes, check constraint. |
| Tests | apps/api/src/modules/academic-sessions/__tests__/academic-sessions.service.integration.spec.ts | Behaviours proven against a real database. |
| Errors | apps/api/src/common/types/error-codes.ts (lines 258-281) | Every error code this module can produce. |
| Permissions | packages/db/src/authorization/permission-catalog.ts (line 95) | AcademicSessions declared as a permission module. |
2. Module Summary
| Field | Value |
|---|---|
| Module name | AcademicSessionsModule |
| Module slug | academic-sessions |
| Primary actors | Admin (only role verified to hold any AcademicSessions_* permission in this codebase) |
| API surface | Admin only — no @Public() route on the controller |
| Base route prefix | /api/academic-sessions (global prefix api set in apps/api/src/main.ts; controller declares academic-sessions locally) |
| Auth model | JwtAuthGuard + RoleGuard, applied at the controller class level |
| Persistence | PostgreSQL, table academic_sessions; Redis cache on list reads |
| Runtime source of truth | The academic_sessions table, served from a 1-hour Redis cache keyed on the exact query and cleared on every write |
| Sibling docs | Backend, Features and flows |
3. Concepts and Terminology
| Term | Meaning | Source File | Used By |
|---|---|---|---|
| Academic session | A named school year (e.g. "2026-27") with a start and end date, that dated records hang off. | packages/db/src/schema/school/academic-sessions.ts | Every route below. |
| Current session | The one session the office is currently working in. | academic-sessions.ts (isCurrent column) | isCurrent filter and field. |
| The "exactly one current" rule | Split across two layers — see Section 5. | academic-sessions.ts, academic-sessions.service.ts | Create, update, delete. |
Retirement (isActive) | Removes a session from active pick lists without touching any existing reference to it. There is no deleted_at on this table. | academic-sessions.ts | isActive filter and field. |
Public ID (publicId) | The UUIDv7 every PATCH/DELETE route addresses a session by. The internal integer id is still returned in the response body. | academic-sessions.ts | Every route param below. |
4. API Surface Map
| Surface | Method | Path | Actor | Auth/Guard | Permission | Controller | Purpose |
|---|---|---|---|---|---|---|---|
| Admin | GET | /api/academic-sessions | Admin | JwtAuthGuard, RoleGuard | AcademicSessions_READ | AcademicSessionsController | List/search sessions, paginated. |
| Admin | POST | /api/academic-sessions | Admin | JwtAuthGuard, RoleGuard | AcademicSessions_CREATE | AcademicSessionsController | Create a session, optionally as current. |
| Admin | PATCH | /api/academic-sessions/:publicId | Admin | JwtAuthGuard, RoleGuard | AcademicSessions_UPDATE | AcademicSessionsController | Update fields, and/or make the session current. |
| Admin | DELETE | /api/academic-sessions/:publicId | Admin | JwtAuthGuard, RoleGuard | AcademicSessions_DELETE | AcademicSessionsController | Hard delete — refused for the current session. |
There is no separate "make current" route. Making a session current is PATCH /api/academic-sessions/:publicId with { "isCurrent": true } in the body — verified against academic-sessions.controller.ts, which declares only the four routes above.
5. The "Exactly One Current Session" Rule
This rule is split between the database and the service, and the two halves are enforced differently on purpose.
| Half | Rule | Enforced By | Why It Cannot Be Enforced Elsewhere |
|---|---|---|---|
| At most one current | No two sessions may both carry isCurrent = true. | The partial unique index academic_sessions_one_current_idx on (is_current) WHERE is_current, packages/db/src/schema/school/academic-sessions.ts | A regular unique index over is_current would also forbid every false row from coexisting with another; the partial predicate indexes only rows where the flag is true, so false rows never collide. |
| At least one current | A school always has a current session; the last one cannot be unset. | AcademicSessionsService.update, apps/api/src/modules/academic-sessions/academic-sessions.service.ts | No index or constraint can require that a row exist with a given value — a CHECK or unique index only ever restricts rows that ARE written, never guarantees a write happens. This half can only be enforced in application code, at the point where an unset is attempted. |
Both create and update unset the incumbent session inside the same database transaction as the write that sets the new one, and always in that order — unset first, then set. Setting a second isCurrent = true row before clearing the first would violate the partial unique index at the INSERT/UPDATE itself, regardless of which write the caller intended to win.
6. DTO and Model Reference
6.1 AcademicSessionDto (response)
| Field | Type | Required | Source |
|---|---|---|---|
id | number | Yes | academic-session.dto.ts:29 — internal serial id. |
publicId | string (UUID) | Yes | academic-session.dto.ts:30. |
name | string | Yes | academic-session.dto.ts:31, e.g. "2026-27". |
startDate | string (ISO date) | Yes | academic-session.dto.ts:32, e.g. "2026-04-15". |
endDate | string (ISO date) | Yes | academic-session.dto.ts:33, e.g. "2027-04-14". |
isCurrent | boolean | Yes | academic-session.dto.ts:34-38. Exactly one row carries true — see Section 5. |
isActive | boolean | Yes | academic-session.dto.ts:39. |
createdAt | Date | Yes | academic-session.dto.ts:40. |
updatedAt | Date | Yes | academic-session.dto.ts:41. |
6.2 CreateAcademicSessionDto (body)
| Field | Type | Required | Default | Validation | Source |
|---|---|---|---|---|---|
name | string | Yes | — | @IsString, @MinLength(1), @MaxLength(64) | academic-session.dto.ts:45-49 |
startDate | string | Yes | — | @IsISO8601, ISO date YYYY-MM-DD | academic-session.dto.ts:51-53 |
endDate | string | Yes | — | @IsISO8601 | academic-session.dto.ts:55-57 |
isCurrent | boolean | No | false | @IsOptional, @IsBoolean | academic-session.dto.ts:59-65 |
isActive | boolean | No | true | @IsOptional, @IsBoolean | academic-session.dto.ts:67-70 |
endDate must be strictly after startDate — enforced both by the service (AcademicSessionsService.assertDatesOrdered, a readable pre-check) and by the database CHECK constraint academic_sessions_dates_ordered (the actual guarantee). A duplicate name is rejected case-insensitively.
6.3 UpdateAcademicSessionDto (body, PATCH)
| Field | Type | Required | Notes | Source |
|---|---|---|---|---|
name | string | No | Same validation as create. | academic-session.dto.ts:74-79 |
startDate | string | No | Checked against the stored endDate when omitted — see edge cases below. | academic-session.dto.ts:81-84 |
endDate | string | No | Checked against the stored startDate when omitted. | academic-session.dto.ts:86-89 |
isCurrent | boolean | No | true makes this session current and unsets the incumbent in the same transaction. false on the only current session is refused. | academic-session.dto.ts:91-97 |
isActive | boolean | No | Retire/reactivate. | academic-session.dto.ts:99-102 |
6.4 ListAcademicSessionsQueryDto (query)
Extends the shared QueryDto (search, page, size, sort, order, pagination — inherited, never redeclared) and adds:
| Field | Type | Required | Source |
|---|---|---|---|
isActive | boolean | No | academic-session.dto.ts:113-116. Query-string boolean, coerced from "true"/"1". |
isCurrent | boolean | No | academic-session.dto.ts:118-121. Same coercion. |
sort accepts "name" or "endDate"; anything else (including the default) sorts by startDate — academic-sessions.service.ts, findAll. Every list is tie-broken by id ascending after the requested sort column.
7. Endpoint Reference
7.1 GET /api/academic-sessions
Purpose: Paginated, filterable list of sessions.
Auth: JwtAuthGuard, RoleGuard; permission AcademicSessions_READ.
Query: search, isActive, isCurrent, pagination, page, size, sort (name | endDate | default startDate), order.
Response:
{
"message": "Academic sessions fetched.",
"data": [
{
"id": 3,
"publicId": "018f2a1e-6b1e-7c3a-9d2e-1a2b3c4d5e6f",
"name": "2026-27",
"startDate": "2026-04-15",
"endDate": "2027-04-14",
"isCurrent": true,
"isActive": true,
"createdAt": "2026-04-01T00:00:00.000Z",
"updatedAt": "2026-04-01T00:00:00.000Z"
}
],
"errorCode": null,
"count": 1,
"currentPage": 1,
"totalPage": 1
}Side effects: A Redis cache read (getSoft) is tried first on a key folding the full query (search, isActive, isCurrent, sort, order, pagination, page, size); on a miss, a SELECT (plus a COUNT(*) when paginated) followed by a setSoft at a 3600-second TTL.
Errors: 401 AUTH_UNAUTHENTICATED, 403 PERMISSION_INSUFFICIENT, 400 VALIDATION_FAILED.
7.2 POST /api/academic-sessions
Purpose: Create a session, optionally making it current immediately.
Auth: permission AcademicSessions_CREATE.
Body: CreateAcademicSessionDto (see 6.2).
Side effects: If isCurrent: true, the existing current session (if any) is unset inside the same transaction before the insert. Always invalidates the list cache (delPatternSoft over academic-sessions:*) after commit.
Errors:
| HTTP Status | Error Code | Condition |
|---|---|---|
401 | AUTH_UNAUTHENTICATED | Missing/invalid JWT. |
403 | PERMISSION_INSUFFICIENT | Active role lacks AcademicSessions_CREATE. |
400 | VALIDATION_FAILED | Invalid body (bad DTO shape, non-ISO date, name too long, etc.). |
409 | ACADEMIC_SESSION_NAME_TAKEN | A session with that name already exists (case-insensitive). |
409 | ACADEMIC_SESSION_DATES_INVALID | endDate is not after startDate. |
409 | ACADEMIC_SESSION_CURRENT_CONFLICT | Two concurrent requests both tried to make a session current; the partial unique index refused the loser. Retryable. |
7.3 PATCH /api/academic-sessions/:publicId
Purpose: Update any subset of a session's fields. This is also the only way to make a session current — there is no separate "set current" route.
Auth: permission AcademicSessions_UPDATE.
Body: UpdateAcademicSessionDto (see 6.3).
Behavior:
{ "isCurrent": true }unsets the existing current session (excluding this row) and sets this one, in one transaction.{ "isCurrent": false }on the session that is currently the only current one is refused with409 ACADEMIC_SESSION_CURRENT_REQUIRED— a school always has exactly one current session, and this half of the rule cannot be expressed as a database constraint (see Section 5).- A PATCH that supplies only one of
startDate/endDateis checked against the stored value of the other field, not just the supplied one — otherwise a caller could invert the range across two separate requests and have the database CHECK constraint report an unmapped23514.
Errors:
| HTTP Status | Error Code | Condition |
|---|---|---|
401 | AUTH_UNAUTHENTICATED | Missing/invalid JWT. |
403 | PERMISSION_INSUFFICIENT | Active role lacks AcademicSessions_UPDATE. |
404 | ACADEMIC_SESSION_NOT_FOUND | No session with that publicId. |
400 | VALIDATION_FAILED | Invalid body. |
409 | ACADEMIC_SESSION_NAME_TAKEN | Renamed to a name already in use. |
409 | ACADEMIC_SESSION_DATES_INVALID | Resulting range (stored, dto-merged) ends before or on its start. |
409 | ACADEMIC_SESSION_CURRENT_REQUIRED | isCurrent: false on the only current session. |
409 | ACADEMIC_SESSION_CURRENT_CONFLICT | Concurrent handover race, same as create. |
7.4 DELETE /api/academic-sessions/:publicId
Purpose: Hard delete. There is no deleted_at on academic_sessions — retire a session with PATCH { "isActive": false } instead, which removes it from pick lists while leaving any existing reference to it intact.
Auth: permission AcademicSessions_DELETE.
Behavior: Refused when the session is the current one.
Errors:
| HTTP Status | Error Code | Condition |
|---|---|---|
401 | AUTH_UNAUTHENTICATED | Missing/invalid JWT. |
403 | PERMISSION_INSUFFICIENT | Active role lacks AcademicSessions_DELETE. |
404 | ACADEMIC_SESSION_NOT_FOUND | No session with that publicId. |
409 | ACADEMIC_SESSION_IS_CURRENT | This is the current session; make another one current first. |
As of this doc, no other schema references academic_sessions.id, so no foreign-key conflict (23503) can occur on delete yet — the service's own docblock (academic-sessions.service.ts, remove) notes this is expected to change once classes/enrolment land, at which point a 23503 guard is added.
8. Error Code Reference
| Error Code | HTTP Status | Meaning |
|---|---|---|
ACADEMIC_SESSION_NOT_FOUND | 404 | No session with the given publicId. |
ACADEMIC_SESSION_NAME_TAKEN | 409 | The case-insensitive unique index on name refused the write. |
ACADEMIC_SESSION_DATES_INVALID | 409 | endDate is not strictly after startDate. |
ACADEMIC_SESSION_IS_CURRENT | 409 | Delete refused: this is the current session. |
ACADEMIC_SESSION_CURRENT_REQUIRED | 409 | PATCH { isCurrent: false } on the only current session. |
ACADEMIC_SESSION_CURRENT_CONFLICT | 409 | Two operators raced to make different sessions current; the partial unique index refused the second. Retryable. |
Source: apps/api/src/common/types/error-codes.ts, lines 258-281.
See Also
- Backend doc:
/docs/developer/academic-sessions/backend - Feature and flows doc:
/docs/developer/academic-sessions/feature
Academic Sessions Backend Documentation
Backend architecture, data model, service behavior, and the split enforcement of the "one current session" rule for academic sessions.
Classes Features and Flows
Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for grades, sections, rooms, classes, and student class enrolments.