Repository navigation
Course Units API
Abstract course units and their concrete occurrences. Status: current. Part of the API Catalog.
Both routes require a session; anonymous callers get 401 unauthorized.
| Status | Method | Path | Description |
|---|---|---|---|
| ✅ | GET |
/v1/course-units/:id |
One abstract course unit |
| ✅ | GET |
/v1/course-units/:id/occurrences |
Every occurrence of the unit, newest year first |
This is the distinction that causes the most confusion, so state it once:
| Concept | What it is | Example |
|---|---|---|
| Course unit | Year-agnostic template of a subject |
BD — Bases de Dados (id 42) |
| Occurrence | The unit actually taught, in one course, in one year |
BD in L.EIC, 2025 (id 9001) |
A course unit owns no classes, periods or enrolments. Those belong to the occurrence. The same
unit taught in two courses or two years has one CourseUnit row and two Occurrence rows.
GET /v1/course-units/42
→ 200 CourseUnit| Status | code |
When |
|---|---|---|
200 |
— | Found |
401 |
unauthorized |
No session |
404 |
course-unit-not-found |
The id does not exist |
422 |
validation-error |
:id is not an integer |
CourseUnit
{ "id": 42, "courseId": 22841, "acronym": "BD", "name": "Bases de Dados", "semester": 1 }| Field | Type | Notes |
|---|---|---|
id |
number | Unit id |
courseId |
number | The course this unit row belongs to |
acronym |
string | e.g. BD
|
name |
string | Full name |
semester |
1 | 2 |
Narrowed at the repository boundary |
The same subject in another course or year is a different CourseUnit row with its own id.
GET /v1/course-units/42/occurrences
→ 200 [Occurrence]- The unit is checked first: an unknown id is
404 course-unit-not-found. - A unit that has never run returns
[]. - Ordered by year descending, then
idascending — newest teaching instance first.
Occurrence
{
"id": 9001,
"courseUnitId": 42,
"courseId": 22841,
"year": 2025,
"ects": 6,
"url": "https://sigarra.up.pt/feup/pt/ucurr_geral.ficha_uc_view?pv_ocorrencia_id=9001",
"hash": "…"
}| Field | Type | Notes |
|---|---|---|
id |
number | SIGARRA occurrence id, unique per academic year |
courseUnitId |
number | The abstract unit |
courseId |
number | The course it ran in |
year |
number | Academic year |
ects |
number | ECTS credits for that occurrence |
url |
string | SIGARRA occurrence page |
hash |
string | Content hash used to detect catalog changes |
ects lives on the occurrence, not the unit: the same unit can carry different credits in different
courses.
Classes, lesson types, teachers and schedule slots hang off an occurrence and are not implemented:
CourseUnit ──< Occurrence ──< Class ──< Slot
│
└── lesson type, teachers, vacancies
endpoints.md designs GET /courses/:id/occurrences, GET /occurrences/:id/classes and
GET /occurrences/hashes. None are registered. Today the only way to reach an occurrence is through
this page's second route. The Occurrences page is 🚧.
| Concern | File |
|---|---|
| Routes | apps/backend/src/interface/routes/course-units.routes.ts |
| Response schemas | apps/backend/src/interface/routes/catalog.schemas.ts |
| Ports |
domain/ports/course-unit.repository.ts, domain/ports/occurrence.repository.ts
|
| Prisma repositories |
infrastructure/database/repositories/prisma-course-unit.repository.ts, prisma-occurrence.repository.ts
|
| Tests | apps/backend/tests/interface/routes/catalog/course-units.test.ts |
The repository narrows the semester int column to 1 | 2 at the boundary, so a corrupt row throws
there instead of producing a response that fails its own schema.
- Courses API — the course → units edge
- Glossary — unit vs occurrence vs class
- API Catalog — conventions and error contract
- Occurrences 🚧 · Domain model 🚧
Last verified: 2026-10-27